GERLA.CC

tutorial · game boy advance

The light that cast a shadow

On the Game Boy Advance there is no lighting engine: there is an adder. Ask it for a light without knowing how it adds, and it hands you back a dirt stain. It really happened, writing VECTOR, and the fault was a double one. Let's see how colour blending works on this machine, line by line, and why getting it wrong raises no error at all.

A VECTOR chamber, with neon tubes around the threshold

the neon tubes around the doorway and the lamp cones are sprites blended with the background by the hardware: they cost thirty-two tiles in total, and every lamp shares them.

01 · the mechanism

Two layers, two weights, one sum

The machine can blend two layers only at a time, and it calls them first and second target. The first is the one on top, the second the one behind it. In one register you say which layers are targets and what effect you want; in another you say with what weight they enter the sum.

/* BLDCNT, 0x04000050: chi si fonde con chi, e come. bit 0..5 primo bersaglio (BG0, BG1, BG2, BG3, sprite, sfondo nudo) bit 6..7 effetto: 0 niente, 1 fusione, 2 schiarisci, 3 scurisci bit 8..13 secondo bersaglio, nello stesso ordine */ #define BLDCNT (*(volatile unsigned short*)0x04000050) #define BLDALPHA (*(volatile unsigned short*)0x04000052) #define PRIMO_SPRITE (1 << 4) /* gli sprite stanno sopra */ #define FUSIONE (1 << 6) /* effetto 1: alpha blending */ #define SECONDO_BG0 (1 << 8) /* il muro sta dietro */ BLDCNT = PRIMO_SPRITE | FUSIONE | SECONDO_BG0; BLDALPHA = 8 | (12 << 8); /* EVA = 8/16, EVB = 12/16 */
the blending registers

The two weights are called EVA and EVB, they run from 0 to 16 sixteenths, and above 16 they stop. The sum the hardware does, channel by channel, is this:

risultato = min(31, sopra × EVA/16 + sotto × EVB/16)

Read it carefully, because everything is in there: it's a weighted sum, not a multiply and not a "lighten". The two weights don't have to add up to 16: they can add up to more, and the result stops at 31, the maximum of a five-bit channel. That freedom is what lets you light something up, and it's the same freedom that lets you dirty it.

02 · first fault

A colour that never arrived

The cones came out darker than the wall. They looked like dirt stains. The first cause wasn't in the art and wasn't in the blending: it was in loading the palette. The loop that copied it carried 96 entries out of 128, and the cones sat in bank 7, that is in the last sixteen. Those sixteen entries were never written, so they stayed at zero: black.

And a black blended with a wall, by the formula above, does exactly what you'd expect: 0 × EVA/16 adds nothing, and the wall enters the sum with a weight below one. The result is the wall, darker. The light had become a shadow, arithmetically.

/* in 4 bit per punto la tavolozza e' fatta di 16 banchi da 16 colori: il banco 7 sono le voci da 112 a 127. Copiarne 96 vuol dire fermarsi al banco 5 e lasciare gli ultimi due a zero. */ unsigned short *tavolozza = (unsigned short*)0x05000200; /* sprite */ for(int i = 0; i < 96; i++) tavolozza[i] = sorgente[i]; /* il baco */ for(int i = 0; i < 128; i++) tavolozza[i] = sorgente[i]; /* il rimedio */

moral: a wrong colour on screen isn't necessarily an art problem. It can be a colour that never reached memory, and nobody tells you: a half-loaded palette isn't an error to the machine, it's just a palette with zeroes in it.

03 · second fault

Why a light has to be nearly white

With the palette fixed, the cones were brown. And they still didn't light anything. The reason is the formula again: if the colour on top is darker than the one below in a channel, that channel goes down. A mid brown has very low blue, so it takes blue away from the wall, and the wall turns into a brown rag instead of a lit area.

With the five-bit-per-channel colours this machine uses, and both weights at a half, the sum comes out like this:

muro R 6 G 7 B 10 scuro, bluastro cono marrone R 14 G 9 B 5 fuso a meta' R 10 G 8 B 7 <- il BLU e' SCESO: sporco cono quasi bianco R 30 G 28 B 24 fuso a meta' R 18 G 17 B 17 <- tutti e tre saliti: luce
the same wall, two different cones

Hence the rule, which holds on any machine that blends by adding: the warmth of a light lives in the hue, not in the value. The cone has to be nearly white, and warm only because red sits just above green and green just above blue. If you put the warmth in by lowering the blue, you're not switching on a lamp: you're laying down a brown gel.

04 · the trap

"Sprites among the targets" means ALL the sprites

This is the part that's really specific to this machine, and it cost me an afternoon. To make the cone blend I had switched on the sprite bit among the first targets, in BLDCNT. It sounds reasonable: "sprites take part in blending". Except that on this hardware that bit doesn't mean blend the marked ones. It means blend every sprite.

Result: the character came out olive instead of orange, and the portal vanished, because they were being blended with the wall too. The mechanism for choosing which sprite blends is a different one, and it lives in the sprite's own attribute, not in the global register: it's called semi-transparent mode, and it works on its own, with no need to touch BLDCNT.

/* SBAGLIATO: fonde ogni sprite a schermo, personaggio compreso */ BLDCNT = (1 << 4) | (1 << 6) | (1 << 8); /* GIUSTO: nessuno sprite fra i bersagli globali, e il singolo sprite del cono si dichiara semitrasparente nel suo attributo 0 (bit 10..11 = 01). Quel bit basta da solo. */ BLDCNT = (1 << 6) | (1 << 8); #define SEMITRASPARENTE (1 << 10) oam[i].attr0 = y | SEMITRASPARENTE; /* solo il cono */ oam[j].attr0 = y; /* il personaggio resta pieno */
the global bit and the per-sprite bit

moral: two controls that seem to say the same thing are often not two ways of saying it. One is a master switch, the other a switch per room, and whoever reads the documentation in a hurry lights up the whole house.

how i found it

Read the numbers, don't look at the screen

The symptom said "the character is the wrong colour", and the first instinct is to go and look at the art or the palette. What I did instead was actually read them: the game runs inside an emulator driven by a script, which can read memory while the machine runs. The palette in memory was right, orange. So I read the pixels on screen where the helmet is: a quarter orange over three quarters wall.

That number doesn't say "there's a colour problem": it says exactly which operation was performed, and on what. From there to the blend register is a single step. A pixel's colour, a sprite's slot in the table, the contents of a tile in video memory: on these machines they are all things you can read rather than infer, and more than once the symptom said one thing and the number said another.

← All tutorials The VECTOR page →