GERLA.CC

tutorial · game boy advance

La luce che faceva ombra

Sul Game Boy Advance non c'è un motore di luci: c'è un sommatore. Se gli chiedi una luce senza sapere come somma, ti restituisce una macchia di sporco. È successo davvero, scrivendo VECTOR, e il difetto era doppio. Vediamo come funziona la fusione dei colori su questa macchina, riga per riga, e perché sbagliarla non dà nessun errore.

Una camera di VECTOR, con i tubi al neon attorno alla soglia

i tubi al neon attorno alla soglia e i coni delle lampade sono sprite fusi con lo sfondo dall'hardware: costano trentadue tessere in tutto, e tutte le lampade se le passano.

01 · il meccanismo

Due strati, due pesi, una somma

La macchina sa fondere due soli strati per volta, e li chiama primo e secondo bersaglio. Il primo è quello che sta sopra, il secondo quello che gli sta dietro. In un registro dici quali strati sono bersaglio e che effetto vuoi; in un altro dici con che peso entrano nella somma.

/* 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 */
i registri della fusione

I due pesi si chiamano EVA e EVB, vanno da 0 a 16 sedicesimi, e sopra 16 si fermano. Il conto che l'hardware fa, canale per canale, è questo:

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

Va letto bene, perché è tutto lì: è una somma pesata, non una moltiplicazione e non uno «schiarisci». I due pesi non devono fare 16 insieme: possono farne di più, e il risultato si ferma a 31, che è il massimo di un canale a cinque bit. È questa libertà che permette di illuminare, ed è sempre questa che permette di sporcare.

02 · primo difetto

Un colore che non era mai arrivato

I coni venivano più scuri del muro. Sembravano macchie di sporco. La prima causa non era nel disegno e non era nella fusione: era nel caricamento della tavolozza. Il ciclo che la copiava ne portava 96 voci su 128, e i coni stavano nel banco 7, cioè nelle ultime sedici. Quelle sedici voci non venivano scritte mai, quindi restavano a zero: nero.

E un nero fuso con un muro, secondo la formula di prima, fa esattamente quello che ti aspetti: 0 × EVA/16 non aggiunge niente, e il muro entra nella somma con un peso minore di uno. Il risultato è il muro, più scuro. La luce era diventata un'ombra per via aritmetica.

/* 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 */

morale: un colore sbagliato a schermo non è per forza un problema di disegno. Può essere un colore che non è mai arrivato in memoria, e nessuno te lo dice: una tavolozza a metà non è un errore per la macchina, è solo una tavolozza con dentro degli zeri.

03 · secondo difetto

Perché una luce deve essere quasi bianca

Sistemata la tavolozza, i coni erano marroni. E continuavano a non illuminare. Il motivo è di nuovo la formula: se il colore sopra è più scuro di quello sotto in un canale, quel canale scende. Un marrone di mezzo ha il blu bassissimo, quindi al muro toglie blu, e il muro diventa uno straccio marrone invece che una zona illuminata.

Con i colori a cinque bit per canale che usa questa macchina, e due pesi a metà, il conto è questo:

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
lo stesso muro, due coni diversi

Da cui la regola, che vale su qualunque macchina fonda per somma: il calore di una luce sta nella tinta, non nel valore. Il cono deve essere quasi bianco, e caldo solo per il fatto che il rosso è appena sopra il verde e il verde appena sopra il blu. Se metti il calore abbassando il blu, non stai accendendo una lampada: stai passando una velina marrone.

04 · la trappola

«Sprite fra i bersagli» vuol dire TUTTI gli sprite

Questa è la parte davvero specifica di questa macchina, e mi è costata un pomeriggio. Per far fondere il cono avevo acceso il bit degli sprite fra i primi bersagli, in BLDCNT. Sembra ragionevole: «gli sprite partecipano alla fusione». Solo che su questo hardware quel bit non vuol dire fondi quelli marcati. Vuol dire fondi tutti gli sprite.

Risultato: il personaggio veniva fuori oliva invece che arancione, e il varco spariva, perché anche loro erano fusi col muro. Il meccanismo per scegliere quale sprite si fonde è un altro, e sta nell'attributo dello sprite, non nel registro globale: si chiama modo semitrasparente, e vale da solo, senza bisogno di toccare 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 */
il bit globale e il bit del singolo sprite

morale: due comandi che sembrano dire la stessa cosa spesso non sono due modi di dirla. Uno è un interruttore generale, l'altro è un interruttore per stanza, e chi legge la documentazione di fretta accende la casa intera.

come l'ho trovato

Leggere i numeri, non guardare lo schermo

Il sintomo diceva «il personaggio è del colore sbagliato», e il primo istinto è andare a guardare il disegno o la tavolozza. Quello che ho fatto invece è stato leggerli davvero: il gioco gira dentro un emulatore comandato da uno script, che può leggere la memoria mentre la macchina va. La tavolozza in memoria era giusta, arancione. Allora ho letto i punti sullo schermo dove sta il casco: un quarto di arancione su tre quarti di muro.

Quel numero non dice «c'è un problema di colore»: dice esattamente quale operazione è stata fatta, e su chi. Da lì al registro della fusione è un passo solo. Il colore di un punto, la posizione di uno sprite nella tavola, il contenuto di una tessera in memoria video: su queste macchine sono tutte cose che si possono leggere invece che dedurre, e diverse volte il sintomo diceva una cosa e il numero ne diceva un'altra.

← Tutti i tutorial La scheda di VECTOR →