dietro le quinte · FIDATI.behind the scenes · FIDATI.
Bug e pezzi di codiceBugs and pieces of code
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.
Bringing the same game to the Commodore 64 and the Sega Mega Drive means arguing with two very different pieces of hardware, and each one bites in its own way. No theory here: real problems met during development, and the small code fragment that solved each one. Every snippet is told in plain words.
Il gracchio fantasmaThe phantom crackle
la musica di sottofondo «gracchia», un pop a ogni notathe background music crackles, a pop on every note
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.
The SID gives each voice an envelope: when you "press the key" (the gate), the volume rises from zero. The obvious way to play a melody is to re-press the key on every note. But every re-attack restarts the volume from zero at once, and that abrupt jump in the wave is heard as a click. Over a continuous melody it becomes a burst of pops: the crackle. The fix is playing legato, like a violinist changing note without lifting the bow: the gate stays on and only the frequency changes. The key is re-pressed only after a real rest.
; 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 musicalemusic engine
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.
moral: the bug wasn't in the notes but in the attacks. On 1982 hardware, the silence between two notes is a design choice, not a detail.
Il registro che cancella gli indiziThe register that erases the clues
le piattaforme a intermittenza risultano sempre solide, anche da spentethe blinking platforms are always solid, even when off
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.
The 6502 has three registers to its name (A, X, Y), and every instruction that touches them also updates the flags, the clues that conditional branches rely on. Here the code reads a blinking platform's state from the grid, then restores the X and Y it had set aside. Except that restore overwrites the flags: the branch right after no longer decided on the value just read, but on the last register restored.
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 tempotimed platform collisions
morale: in assembly non esistono variabili locali: ogni ripristino è un'istruzione come le altre, con effetti collaterali come le altre.
moral: in assembly there are no local variables: every restore is an instruction like any other, with side effects like any other.
La memoria che trabocca in silenzioThe memory that overflows in silence
aggiunto il secondo mondo, il gioco si blocca all'avvio senza un errorewith the second world added, the game freezes at boot with no error
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.
On the C64, code, graphics, levels and music share the same 64 KB, each block at its own fixed address. When the level data grew (six screens instead of three), it spilled into the character set. And the assembler said nothing: to it, bytes are just bytes. The game asked "how many blinking platforms are there?" and found a piece of font: 108. The real fix was giving each block its own area; the lesson was putting guards in the source: if a block grows past its boundary, the build fails with a clear message, today and every time.
; 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 memoriamemory map
morale: il bug peggiore non è quello che fa crashare: è quello che corrompe piano e lontano. La difesa è far esplodere la build, non il gioco.
moral: the worst bug isn't the one that crashes: it's the one that corrupts quietly and far away. The defense is making the build explode, not the game.
Lo schermo violaThe purple screen
la ROM con l'audio nuovo non parte: schermo viola fissothe ROM with the new audio won't start: solid purple screen
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.
The Mega Drive has two brains: the 68000 (the game) and a Z80 (usually, the audio). Here the audio is driven directly by the 68000, so the Z80 must be asked for the bus: "stop, I'm taking over". There is, however, a rule written in small print: a Z80 held in reset never grants the bus. The code waited for the grant before releasing the reset: an infinite wait, before the screen was even on. The purple was the background color of a game that never started.
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 audioaudio init
morale: e da quel giorno ogni attesa sull'hardware ha un contatore: al peggio parte il gioco senza audio, mai uno schermo viola.
moral: and since that day every hardware wait has a counter: worst case the game starts without audio, never a purple screen.
La striscia che non voleva crollareThe stripe that wouldn't crumble
il pavimento finto crolla, ma a schermo resta una striscia di bloccothe fake floor crumbles, but a stripe of the block stays on screen
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.
On the Mega Drive the screen is a mosaic of 8×8 tiles, and each game block takes four of them. The put16 function draws a block by writing four consecutive tiles: number base, base+1, base+2, base+3. To erase, instinct says "draw block zero". But put16(0) writes tiles 0, 1, 2 and 3, and tiles 1 to 3 are pieces of the first block. Hence the stripe.
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 livellolevel drawing
morale: «cancella» e «disegna il nulla» sembrano la stessa cosa. Su una console a tile, non lo sono.
moral: "erase" and "draw nothing" look like the same thing. On a tile-based console, they are not.
Collaudare una console senza la consoleTesting a console without the console
come si verifica il codice audio se non puoi «vedere» dentro il chip?how do you verify audio code if you can't see inside the 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.
The trick holding the whole port together: the exact same C source is compiled twice. Once for the Mega Drive's 68000, where writes go to the chips' real registers; once for the development machine, where the same writes end up in a log. On the log, a test program checks everything: that the bass plays legato, that effects last the right number of frames, that channels shut off. If the tests pass, the ROM ships that same code, not a copy.
#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 bersagliodual target
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.
moral: the same idea holds for the game physics: the Mega Drive logic is verified frame by frame, bit by bit, against the original. The ear judges the timbre; math judges everything else.
← Torna a FIDATI.← Back to FIDATI.
Il devlogThe devlog