mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-27 12:41:30 +11:00
114 lines
4.6 KiB
Markdown
114 lines
4.6 KiB
Markdown
# Video Memory Intro
|
|
|
|
The GBA's Video RAM is 96k stretching from `0x0600_0000` to `0x0601_7FFF`.
|
|
|
|
The Video RAM can only be accessed totally freely during a Vertical Blank (aka
|
|
"VBlank", though sometimes I forget and don't capitalize it properly). At other
|
|
times, if the CPU tries to touch the same part of video memory as the display
|
|
controller is accessing then the CPU gets bumped by a cycle to avoid a clash.
|
|
|
|
Annoyingly, VRAM can only be properly written to in 16 and 32 bit segments (same
|
|
with PALRAM and OAM). If you try to write just an 8 bit segment, then both parts
|
|
of the 16 bit segment get the same value written to them. In other words, if you
|
|
write the byte `5` to `0x0600_0000`, then both `0x0600_0000` and ALSO
|
|
`0x0600_0001` will have the byte `5` in them. We have to be extra careful when
|
|
trying to set an individual byte, and we also have to be careful if we use
|
|
`memcopy` or `memset` as well, because they're byte oriented by default and
|
|
don't know to follow the special rules.
|
|
|
|
## RGB15
|
|
|
|
As I said before, RGB15 stores a color within a `u16` value using 5 bits for
|
|
each color channel.
|
|
|
|
```rust
|
|
pub const RED: u16 = 0b0_00000_00000_11111;
|
|
pub const GREEN: u16 = 0b0_00000_11111_00000;
|
|
pub const BLUE: u16 = 0b0_11111_00000_00000;
|
|
```
|
|
|
|
In Mode 3 and Mode 5 we write direct color values into VRAM, and in Mode 4 we
|
|
write palette index values, and then the color values go into the PALRAM.
|
|
|
|
## Mode 3
|
|
|
|
Mode 3 is pretty easy. We have a full resolution grid of rgb15 pixels. There's
|
|
160 rows of 240 pixels each, with the base address being the top left corner. A
|
|
particular pixel uses normal "2d indexing" math:
|
|
|
|
```rust
|
|
let row_five_col_seven = 5 + (7 * SCREEN_WIDTH);
|
|
```
|
|
|
|
To draw a pixel, we just write a value at the address for the row and col that
|
|
we want to draw to.
|
|
|
|
## Mode 4
|
|
|
|
Mode 4 introduces page flipping. Instead of one giant page at `0x0600_0000`,
|
|
there's Page 0 at `0x0600_0000` and then Page 1 at `0x0600_A000`. The resolution
|
|
for each page is the same as above, but instead of writing `u16` values, the
|
|
memory is treated as `u8` indexes into PALRAM. The PALRAM starts at
|
|
`0x0500_0000`, and there's enough space for 256 palette entries (each a `u16`).
|
|
|
|
To set the color of a palette entry we just do a normal `u16` write_volatile.
|
|
|
|
```rust
|
|
(0x0500_0000 as *mut u16).offset(target_index).write_volatile(new_color)
|
|
```
|
|
|
|
To draw a pixel we set the palette entry that we want the pixel to use. However,
|
|
we must remember the "minimum size" write limitation that applies to VRAM. So,
|
|
if we want to change just a single pixel at a time we must
|
|
|
|
1) Read the full `u16` it's a part of.
|
|
2) Clear the half of the `u16` we're going to replace
|
|
3) Write the half of the `u16` we're going to replace with the new value
|
|
4) Write that result back to the address.
|
|
|
|
So, the math for finding a byte offset is the same as Mode 3 (since they're both
|
|
a 2d grid). If the byte offset is EVEN it'll be the high bits of the `u16` at
|
|
half the byte offset rounded down. If the offset is ODD it'll be the low bits of
|
|
the `u16` at half the byte.
|
|
|
|
Does that make sense?
|
|
|
|
* If we want to write pixel (0,0) the byte offset is 0, so we change the high
|
|
bits of `u16` offset 0. Then we want to write to (1,0), so the byte offset is
|
|
1, so we change the low bits of `u16` offset 0. The pixels are next to each
|
|
other, and the target bytes are next to each other, good so far.
|
|
* If we want to write to (5,6) that'd be byte `5 + 6 * 240 = 1445`, so we'd
|
|
target the low bits of `u16` offset `floor(1445/2) = 722`.
|
|
|
|
As you can see, trying to write individual pixels in Mode 4 is mostly a bad
|
|
time. Fret not! We don't _have_ to write individual bytes. If our data is
|
|
arranged correctly ahead of time we can just write `u16` or `u32` values
|
|
directly. The video hardware doesn't care, it'll get along just fine.
|
|
|
|
## Mode 5
|
|
|
|
Mode 5 is also a two page mode, but instead of compressing the size of a pixel's
|
|
data to fit in two pages, we compress the resolution.
|
|
|
|
Mode 5 is full `u16` color, but only 160w x 128h per page.
|
|
|
|
## In Conclusion...
|
|
|
|
So what got written into VRAM in `hello1`?
|
|
|
|
```rust
|
|
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
|
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
|
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
|
```
|
|
|
|
So at pixels `(120,80)`, `(136,80)`, and `(120,96)` we write three values. Once
|
|
again we probably need to [convert them](https://www.wolframalpha.com/) into
|
|
binary to make sense of it.
|
|
|
|
* 0x001F: 0b0_00000_00000_11111
|
|
* 0x03E0: 0b0_00000_11111_00000
|
|
* 0x7C00: 0b0_11111_00000_00000
|
|
|
|
Ah, of course, a red pixel, a green pixel, and a blue pixel.
|