tutorial · game boy advance
Compila, gira, e non funziona
Quando scrivi per una macchina nuda, senza sistema operativo e senza libreria standard, «compila e gira» non vuol dire «funziona». Vuol dire soltanto che nessuno si è lamentato. Questi sono cinque guasti veri incontrati scrivendo VECTOR per Game Boy Advance: nessuno dei cinque ha prodotto un errore, e solo per alcuni c'era qualcuno che avrebbe potuto avvisare, se glielo avessi chiesto.
Una variabile su tre non cambiava mai
Per un pezzo il gioco è stato muto. Il codice del suono era giusto, i registri erano quelli. Il difetto stava nello script del collegatore: mancava la sezione .data, quella delle variabili che partono da un valore.
Perché è grave proprio su questa macchina: la cartuccia sta a 0x08000000 ed è sola lettura; la memoria scrivibile sta altrove. Una variabile inizializzata deve stare in due posti: il valore di partenza dentro la cartuccia, la variabile vera in memoria. All'accensione qualcuno copia il primo nella seconda. Se la sezione non c'è, quel qualcuno non copia niente e la variabile vive dentro la ROM: si legge benissimo, e le scritture cadono nel vuoto senza un lamento.
Il rimedio sta nello script del collegatore: si dichiara la sezione con due indirizzi diversi, quello dove la variabile vive e quello da cui il suo valore parte, e all'avvio si copia.
morale: il collegatore non si era lamentato. Aveva messo tutto in ROM e il programma girava. Un guasto che non impedisce l'avvio è più costoso di uno che lo impedisce, perché ti fa cercare nel posto sbagliato per giorni.
Il ramo scritto due volte
La schermata di congedo non appariva mai. Il codice era questo, ed è il guasto più insidioso che abbia incontrato in questo progetto:
È C perfettamente valido. Il primo ramo cattura la condizione e non fa niente; il secondo, che contiene il lavoro, non viene eseguito mai. Nessun errore, e con le opzioni normali nemmeno un avviso.
Qui però qualcuno che avvisa c'è, e va solo acceso. GCC ha un'opzione fatta esattamente per questo: guarda le catene di if / else if e segnala quando la stessa condizione compare due volte, perché il secondo ramo è codice morto per costruzione.
C'era poi un secondo difetto sovrapposto, e vale la pena dirlo perché è tipico del passaggio fra schermate. I registri della luminosità li scriveva solo il ramo del gioco. Il passaggio da una camera all'altra avviene dentro un lampo bianco, che è il vecchio trucco per nascondere il caricamento: uscendo dal gioco proprio lì, i registri restavano fermi sull'ultimo valore. Schermo bianco per sempre. Adesso, fuori dal gioco, si rimettono a posto una volta per quadro.
morale: quando lo stato della macchina lo scrive un solo ramo del programma, quel ramo diventa obbligatorio. E prima o poi ne uscirai da un'altra parte.
Due cose nello stesso posto
Gli sprite pescano i loro disegni da un magazzino di tessere, e ogni sprite dichiara da quale posizione parte. Quelle posizioni erano somme di lunghezze scritte a mano: il personaggio prende tanto, poi il cubo comincia lì, poi il raggio comincia lì.
Una somma sbagliata non dà errore: dà due cose nello stesso posto. A schermo si vede una che mostra i pezzi dell'altra. È successo col raggio laser, che passando da due quadri a quattro è finito dentro le tessere della piattaforma.
morale: qualunque numero che dipenda da tutti i numeri scritti sopra di lui è una bomba a tempo. La difesa non è stare attenti: è far calcolare le posizioni a chi prepara i dati, e farlo fermare con il nome dei due blocchi che si pestano i piedi.
L'edera che cresceva nelle camere pulite
Ogni camera ha una tabella di piante: quattordici caselle, e le camere che non ne hanno le lasciano vuote. «Vuoto» però non è un tipo di dato, è un accordo, e i due lati dell'accordo non erano d'accordo. Chi scriveva la tabella marcava la casella vuota con -1. Chi la leggeva saltava le caselle sotto -100.
Preso da solo, ciascuno dei due è ragionevole. Messi insieme vogliono dire che -1 non è vuoto: è un valore buono. Così ogni camera in ordine, quelle della prima metà del gioco dove il cemento è pulito, si disegnava in silenzio quattordici rampicanti, tutti nello stesso posto, accatastati contro la parete di sinistra.
Il compilatore qui non può fare niente, ed è la ragione per cui questo guasto è nella lista. Non c'è nessun errore di tipo: -1 è uno short valido come tutti gli altri, e < -100 è un confronto legittimo. Il vincolo che è stato violato non esiste nel linguaggio, esiste solo nella testa di chi ha scritto le due righe, e fra le due righe passavano parecchi mesi.
Il rimedio non è ricordarsi meglio: è togliere l'accordo di mezzo. O il valore convenuto lo decide un posto solo, e tutti e due i lati usano quello, oppure non c'è nessun valore convenuto e la tabella porta con sé quante caselle sono piene, che è un numero e non una convenzione.
Ma la parte che mi ha fatto perdere più tempo non è il difetto: è che non si vedeva. In un gioco dove la seconda metà è un impianto abbandonato invaso dall'edera, dell'edera in più non grida che c'è un errore. A colpo d'occhio sembrava perfino al posto giusto. Un guasto che produce qualcosa di plausibile è più caro di uno che produce qualcosa di assurdo, e va cercato con lo stesso metodo di tutti gli altri: leggendo la tabella in memoria invece che guardando il muro.
morale: un valore convenuto è un contratto che nessuno fa rispettare. Se il linguaggio non può controllarlo, prima o poi le due parti si allontanano, e te ne accorgi solo se il risultato è abbastanza assurdo da farsi notare.
Finisce, e finisce in silenzio
Le camere, arrivate a sei, non ci stavano più. Il tetto su questa macchina non è la dimensione della memoria video: è che la casella della mappa dedica dieci bit al numero della tessera. Dieci bit sono 1024 numeri, e quindi uno sfondo può nominare al massimo 1024 tessere diverse, per quanta memoria tu abbia. Le camere ne volevano milleseicento.
La causa non era la quantità di disegni: era la grana. I blocchi del muro avevano una grana di rumore unica per ogni posizione, quindi ogni blocco produceva quattro tessere sue e nessuna si poteva riusare. La cura è stata farla ripetere ogni quattro blocchi: a sessantaquattro punti di periodo la ripetizione va cercata per accorgersene, e nel frattempo le tessere identiche si fondono in una sola. Da 1600 a 900.
E il guasto, di nuovo, non si annuncia: se sfori, la casella della mappa non ha i bit per il numero che le stai dando, e a schermo compare la tessera sbagliata. Nessun avviso, nessun blocco, solo un muro che ha addosso il pezzo di un'altra cosa.
Il guasto che invece si lamenta
Per equilibrio vale la pena chiudere con l'unico che non sta zitto. Il processore di questa macchina non sa dividere: non esiste un'istruzione che lo faccia. Quando scrivi a / b con b qualunque, il compilatore non sbaglia e non inventa: mette una chiamata a una routine di libreria. E siccome qui la libreria standard non c'è, il programma non si collega più, con un bel riferimento non risolto e il nome della routine mancante.
Per questo in VECTOR ogni ciclo, ogni cadenza e ogni rampa è una potenza di due o un contatore che si riavvolge: uno spostamento di bit lo sa fare il processore, una divisione no. È un vincolo che all'inizio sembra una scomodità e dopo un po' diventa un modo di pensare.
morale: quando qualcosa si lamenta, ringrazia. I guasti che ti fermano subito sono i più economici che ti capiteranno.