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.


# #![allow(unused_variables)]
#fn main() {
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:


# #![allow(unused_variables)]
#fn main() {
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.


# #![allow(unused_variables)]
#fn main() {
(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?


# #![allow(unused_variables)]
#fn main() {
    (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.