dietro le quinte · FIDATI.
Bug e pezzi di codice
Portare lo stesso gioco su Commodore 64 e Sega Mega Drive significa litigare con due hardware molto diversi, e ognuno morde a modo suo. Qui non c'è teoria: problemi reali incontrati durante lo sviluppo, e il frammento di codice, piccolo, che li ha risolti. Ogni pezzo è raccontato in parole semplici.
Il gracchio fantasma
la musica di sottofondo «gracchia», un pop a ogni nota
Il SID dà a ogni voce un inviluppo: quando «premi il tasto» (il gate), il volume sale da zero. Il modo ovvio di suonare una melodia è ri-premere il tasto a ogni nota. Ma ogni ri-attacco fa ripartire il volume da zero di colpo, e quel salto brusco nell'onda si sente come un click. Su una melodia continua diventa una raffica di pop: il gracchiare. La soluzione è suonare legato, come un violinista che cambia nota senza staccare l'arco: il gate resta acceso e cambia solo la frequenza. Il tasto si ri-preme soltanto dopo una pausa vera.
; melodia (voce 3): la nota nuova entra senza staccare l'arco
lda tmp
sta $d40e ; frequenza, byte basso
lda tmp+1
sta $d40f ; frequenza, byte alto
lda m1g ; il gate e' gia' acceso?
bne mtb ; si': solo frequenza, niente pop
lda #$59 ; no: attacco lento (56 ms), decay dolce
sta $d413
game2.asm · motore musicale
morale: il bug non era nelle note ma negli attacchi. Su hardware del 1982 il silenzio tra due note è una scelta di design, non un dettaglio.
Il registro che cancella gli indizi
le piattaforme a intermittenza risultano sempre solide, anche da spente
Il 6502 ha tre registri in croce (A, X, Y) e ogni istruzione che li tocca aggiorna anche i flag, gli indizi su cui si basano i salti condizionati. Qui il codice legge dalla griglia lo stato di una piattaforma lampeggiante e poi ripristina X e Y che aveva messo da parte. Peccato che quel ripristino sovrascriva i flag: il salto subito dopo non decideva più sul valore letto, ma sull'ultimo registro ripristinato.
tay
lda BS,y ; stato del blocco: 1 = acceso
ldy tmp2 ; ripristino Y...
ldx tmp3 ; ...e X, che pero' RISCRIVE i flag!
; bne hyes ; il baco: decideva sul valore di X
cmp #1 ; il fix: ri-confronta A prima di saltare
beq hyes ; solido solo se davvero acceso
game2.asm · collisioni con le piattaforme a tempo
morale: in assembly non esistono variabili locali: ogni ripristino è un'istruzione come le altre, con effetti collaterali come le altre.
La memoria che trabocca in silenzio
aggiunto il secondo mondo, il gioco si blocca all'avvio senza un errore
Sul C64 codice, grafica, livelli e musica convivono negli stessi 64 KB, e ogni blocco ha il suo indirizzo fisso. Quando i dati dei livelli sono cresciuti (sei schemi invece di tre), hanno sconfinato dentro il set di caratteri. E l'assemblatore non ha detto nulla: per lui erano solo byte su byte. Il gioco leggeva «quante piattaforme lampeggianti ci sono?» e trovava un pezzo di font: 108. Il fix vero è stato dare a ogni blocco un'area dedicata; la lezione è stata mettere guardie nel sorgente: se un blocco cresce oltre il suo confine, la build fallisce con un messaggio chiaro, oggi e ogni volta.
; confini di memoria: se si sfora, la build FALLISCE (mai piu' in silenzio)
.cerror * > $2000, "CODICE oltre $2000: invade il charset"
.cerror * > INPUT, "LDATA oltre l'area input script"
.cerror * > $4400, "TESTI oltre $4400: invadono ldata"
game2.asm · mappa di memoria
morale: il bug peggiore non è quello che fa crashare: è quello che corrompe piano e lontano. La difesa è far esplodere la build, non il gioco.
Lo schermo viola
la ROM con l'audio nuovo non parte: schermo viola fisso
Il Mega Drive ha due cervelli: il 68000 (il gioco) e uno Z80 (di solito, l'audio). Qui l'audio lo guida direttamente il 68000, quindi allo Z80 va chiesto il bus: «fermati, passo io». C'è però una regola scritta in piccolo: uno Z80 tenuto in reset non concede mai il bus. Il codice aspettava la concessione prima di togliergli il reset: attesa infinita, prima ancora di accendere lo schermo. Il viola era il colore di sfondo di un gioco mai partito.
static void z80_stop(void)
{
uint16_t n = 16384;
/* prima si toglie il RESET: uno Z80 in reset
non concede mai il bus */
Z80_BUSREQ = 0x100; /* bus al 68000, per sempre */
Z80_RESET = 0x100; /* PRIMA di aspettare! */
while ((Z80_BUSREQ & 0x100) && --n) {}
}
audio.c · inizializzazione audio
morale: e da quel giorno ogni attesa sull'hardware ha un contatore: al peggio parte il gioco senza audio, mai uno schermo viola.
La striscia che non voleva crollare
il pavimento finto crolla, ma a schermo resta una striscia di blocco
Sul Mega Drive lo schermo è un mosaico di tessere 8×8, e ogni blocco del gioco ne occupa quattro. La funzione put16 disegna un blocco scrivendo quattro tessere consecutive: la numero base, base+1, base+2, base+3. Per cancellare, l'istinto dice «disegna il blocco zero». Ma put16(0) scrive le tessere 0, 1, 2 e 3, e le tessere 1-3 sono pezzi del primo blocco. Ecco la striscia.
static void put16(/* ... */ uint16_t base, uint16_t pal)
{
plane_cell(PLANE_A, col, row, (pal<<13)|(base));
plane_cell(PLANE_A, col+1, row, (pal<<13)|(base+1));
/* ...con base=0 scrive le tile 1-3: pezzi di blocco! */
}
static void clear16(uint16_t col, uint16_t row) /* il fix */
{
plane_cell(PLANE_A, col, row, 0); /* quattro zeri
veri: vuoto, non "blocco numero zero" */
main.c · disegno del livello
morale: «cancella» e «disegna il nulla» sembrano la stessa cosa. Su una console a tile, non lo sono.
Collaudare una console senza la console
come si verifica il codice audio se non puoi «vedere» dentro il chip?
Il trucco che regge tutto il porting: lo stesso identico sorgente C viene compilato due volte. Una per il 68000 del Mega Drive, dove le scritture vanno ai registri veri dei chip; una per il computer di sviluppo, dove le stesse scritture finiscono in un log. Sul log un programma di verifica controlla tutto: che il basso suoni legato, che gli effetti durino i frame giusti, che i canali si spengano. Se il collaudo passa, la ROM monta quello stesso codice, non una copia.
#ifdef AUDIO_TEST /* collaudo su PC */
#define YMW(part,reg,val) test_ym(part,reg,val) /* -> log */
#else /* ROM vera */
#define YM_A0 (*(volatile uint8_t*)0xA04000) /* -> chip */
#endif
audio.c · doppio bersaglio
morale: la stessa idea vale per la fisica del gioco: la logica del Mega Drive è verificata frame per frame, bit per bit, contro la versione originale. L'orecchio giudica il timbro; la matematica, tutto il resto.
← Torna a FIDATI.
Il devlog