ALAN JAHNEL / Software engineering

Writing

GBA Assembly — 4. Pixels, colors and rectangles

Learn how coordinates become VRAM addresses and how five-bit RGB colors form a rectangle in Mode 3.

In the first ROM, we wrote a pixel's color directly. Now we will draw a green rectangle on a black screen. There is still no player, enemy, or win condition: the only problem is converting an area of the screen into memory writes.

Use the complete retangulo.s file, also included in the series package. It uses the same gba.ld and rom.py as before. The blocks below highlight the routine we are studying; do not replace the entire file with one fragment.

Your route through this chapter

Before starting: build and open the red-screen ROM from chapter 3. Keep the working source.

  1. Save the complete retangulo.s beside gba.ld and rom.py. Open it in your text editor; leave the previous .s file separate.
  2. Read the coordinate and color examples below, then find fill_rect and its call in the complete file. The call supplies the values; the routine does the drawing.
  3. Run this chapter's four build commands in the same terminal folder, then open retangulo.gba in mGBA.
  4. After the original result works, change only the rectangle call's width from 32 to 64. Keep the height at 32, save, and repeat all four commands. Leave the separate full-screen clearing call unchanged.

Checkpoint: first you see a green 32-by-32 square on black; after the edit it is 64 pixels wide and 32 high. If the whole background changes, check which call you edited. Restore the original width before continuing.

The screen as a sequence of numbers

In Mode 3, an image has 240 columns and 160 rows. Each pixel occupies a halfword, or two bytes. Memory stores one row after another:

Plain text
pixel (0,0)       -> 0x06000000
pixel (1,0)       -> 0x06000002
pixel (239,0)     -> 0x060001DE
pixel (0,1)       -> 0x060001E0

A row occupies 240 × 2 = 480 bytes. Moving right means advancing two bytes. Moving to the same column in the next row means advancing 480 bytes. The general formula is:

Plain text
address(x,y) = 0x06000000 + 2 × (y × 240 + x)

This relationship depends on the video mode. With tiles and sprites, writing to the same region means something different. The circuitry interprets data according to the configuration selected in DISPCNT.

Fitting three intensities into two bytes

Each color component has five bits and ranges from 0 to 31. Bits 0 through 4 hold red, 5 through 9 hold green, and 10 through 14 hold blue. Bit 15 is not a transparency channel in this mode.

Plain text
color = red | (green << 5) | (blue << 10)
ColorRedGreenBlueValue
Black0000x0000
Red31000x001F
Green03100x03E0
Blue00310x7C00
White3131310x7FFF

Do not copy a CSS color such as #FF0000 straight into VRAM: its format and bit count are different. This is not specific to assembly. Any language controlling this mode must respect its format.

A drawing function of our own

Consult BL and BX for calls and returns, and PUSH and POP for the values the routine preserves.

We want to pass a position, size, and color to a routine. Our convention is:

RegisterParameter
r0, r1X and Y of the upper-left corner
r2, r3Width and height in pixels
r416-bit color

This is an agreement between our routines, not a promise of compatibility with any C function. We call with bl fill_rect. bl saves a return address in lr, and bx lr returns to the caller.

Here is the complete drawing routine:

<!-- source: en/retangulo.s#pixels -->
ARM assembly
@ r0=x, r1=y, r2=width, r3=height, r4=color.
@ Requires positive width/height and a rectangle entirely inside the screen.
fill_rect:
    push {r5-r8, lr}
    mov r5, #240
    mul r6, r1, r5
    add r6, r6, r0
    ldr r5, =VRAM
    add r5, r5, r6, lsl #1
    rsb r7, r2, #240
    mov r7, r7, lsl #1
    mov r6, r3
row:
    mov r8, r2
pixel:
    strh r4, [r5], #2
    subs r8, r8, #1
    bne pixel
    add r5, r5, r7
    subs r6, r6, #1
    bne row
    pop {r5-r8, lr}
    bx lr

push {r5-r8, lr} saves the values the routine wants to preserve on the stack; pop restores them at the end. Startup configured sp. Since the stack occupies RAM, saving and restoring are also memory operations.

mul r6, r1, r5 calculates Y × 240; we add X and multiply by two using lsl #1. Adding that to the VRAM base produces the first pixel's address.

Why two loops?

The routine combines MUL, ADD, SUB, and RSB, shifts, and post-indexed addressing.

A 32-by-32 rectangle has 32 pixels per row and 32 rows. The pixel loop writes one row; the row loop repeats for the remaining rows.

After writing 32 pixels, the pointer has advanced 64 bytes. Reaching the beginning of the rectangle's next row requires another (240 - 32) × 2 = 416 bytes. We keep that distance in r7.

Without that adjustment, the next pixels would be written beside the previous ones as a long strip. The CPU has no built-in concept of a rectangle: we implement it with a sequence of addresses and two counters.

The routine requires positive width and height and an area entirely inside the screen. It does not automatically clip invalid coordinates. Our examples obey 0 ≤ X, 0 ≤ Y, X + width ≤ 240, and Y + height ≤ 160. A width of zero would also be an error because the loop writes before decreasing the counter.

Calling the routine

In the complete file, startup clears the screen with a black 240-by-160 rectangle and then prepares this call:

ARM assembly
mov r0, #104
mov r1, #64
mov r2, #32
mov r3, #32
ldr r4, =0x03e0
bl fill_rect

The rectangle occupies X from 104 to 135 and Y from 64 to 95. Its center matches the image's center. Video remains in forced blank during preparation and is enabled afterward. Since the image is static, execution can stay in the final loop.

Build the example with:

Shell
arm-none-eabi-as -mcpu=arm7tdmi -g -o retangulo.o retangulo.s
arm-none-eabi-ld -T gba.ld -o retangulo.elf retangulo.o
arm-none-eabi-objcopy -O binary retangulo.elf retangulo.gba
python3 rom.py retangulo.gba

Open retangulo.gba in mGBA. You should see a green 32-by-32 square on a black background.

The example in mGBA: a green 32-by-32-pixel square on a black screen

From a simple shape to a game scene

A road can be a long rectangle. A health bar can be a rectangle whose width depends on a variable. A board can convert columns and rows to X and Y before drawing. Our program gives these elements meaning; to the hardware, they remain colors in VRAM.

We will keep using the simple square in the following chapters. It lets us study timing and input without also organizing an entire map.

For an exercise, change only the width to 64. The object should become a horizontal rectangle. Then change its color to blue. Finally, call the routine twice before enabling the screen to draw overlapping rectangles: the last write wins where they overlap. Mode 3 does not maintain an object list that automatically preserves the rectangle underneath.

Tonc's bitmap-mode reference describes image formats and the differences between modes. The connection between color, memory, and video configuration is what we need to take from this chapter.

Previous: your first ROM. Next: making the square blink in sync with the display. Series index.

Back to home