4.6 KiB
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.
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:
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.
(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
- Read the full
u16
it's a part of. - Clear the half of the
u16
we're going to replace - Write the half of the
u16
we're going to replace with the new value - 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 ofu16
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 ofu16
offsetfloor(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
?
(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 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.