GERLA.CC

tutorial · Mega Drive

Assembler e C, perché tutti e due?

Il gioco per Mega Drive è scritto un po' in assembler e un po' in C. Perché due linguaggi per un gioco solo? Perché servono a cose diverse, e usati insieme fanno un lavoro migliore. Vediamo la differenza con qualche esempio di codice: per seguirlo bastano le figure e le note in verde.

01 · la CPU

La CPU capisce un linguaggio solo

Dentro il Mega Drive c'è una CPU, il Motorola 68000: è il processore che esegue il gioco. Una CPU capisce soltanto il suo linguaggio macchina, fatto di istruzioni elementari: sposta un dato, sommalo, confrontalo, salta a un altro punto. L'assembler è il modo leggibile di scrivere quelle istruzioni, una per una. Per esempio, mettere un colore nella tavolozza (la palette) si fa così.

assembler · linguaggio della CPU

; scrivi il colore bianco nella palette move.w #$0EEE, d0 ; d0 = bianco move.w d0, (a1) ; scrivilo nel colore n.0

 

Due istruzioni per un solo colore. Preciso, ma scrivere un gioco intero a questo livello sarebbe lentissimo e facile da sbagliare. Serve uno strato più comodo.

02 · il compilatore

Il C e il compilatore

Ecco lo strato comodo: il C. Il C è un linguaggio più vicino al nostro modo di ragionare, dove le cose si dicono in modo corto e chiaro. La CPU però il C non lo esegue: serve un compilatore, un programma che traduce il C nelle tante istruzioni macchina che la CPU capisce. Il percorso è questo:

codice Ccome si scrive
→
compilatoretraduce
→
codice macchinale istruzioni
→
CPU 68000esegue

A sinistra il C come si scrive, a destra il codice macchina che il compilatore produce.

C · come si scrive

/* riempi di bianco tutti e 16 i colori */ for (int i = 0; i < 16; i++) { palette[i] = 0x0EEE; }

assembler · cosa genera il compilatore

moveq #0, d0 ; i = 0 .loop: move.w #$0EEE, (a0) ; palette[i] = bianco addq.w #2, a0 ; punta al colore dopo addq.w #1, d0 ; i = i + 1 cmp.w #16, d0 ; i < 16 ? blt .loop ; si': ripeti

Tre righe di C diventano sei istruzioni macchina, e le scrive il compilatore, senza sbagliare. Per questo la maggior parte del gioco è in C: più corto, più chiaro, più veloce da correggere.

03 · la domanda

Allora perché non tutto in C?

Se il compilatore è così bravo, perché non scrivere tutto in C ed eliminare l'assembler? Perché ci sono tre punti in cui il compilatore non basta e l'assembler va scritto a mano. Vediamoli.

03a · l'avvio

L'avvio, prima che il C possa girare

All'accensione la CPU parte da uno stato grezzo: la memoria non è preparata e, soprattutto, non esiste ancora lo stack, l'area di memoria che il C usa per le chiamate di funzione e le variabili locali. Senza stack, il C non può girare. Quindi il primissimo codice, quello che imposta lo stack e poi passa il controllo al programma, è per forza in assembler. Per convenzione questo file si chiama crt0.

ACCENSIONE
↓
la CPU legge la tabella dei vettori
in cima alla ROM
↓ salta a _start
assembler · crt0
imposta lo stack, spegne le interruzioni
↓ chiama main()
C · main()
init e game loop: il gioco parte

crt0.s · assembler

_start: move.w #$2700, sr ; disabilita le interruzioni lea stack_top, sp ; imposta lo stack pointer jsr main ; passa il controllo al C .stop: bra .stop ; se main torna, resta qui

main.c · C

int main(void) { /* da qui in poi lavoro comodo, in C: lo stack e' pronto grazie al crt0 */ init_game(); game_loop(); return 0; }

L'assembler prepara il terreno; poi entra il C. Senza quelle poche righe iniziali, il C non potrebbe nemmeno partire.

03b · i vettori

La tabella dei vettori

Appena accesa, la CPU legge in cima alla ROM una tabella dei vettori: una lista di indirizzi. Il primo dice dov'è lo stack, il secondo da quale indirizzo iniziare, altri dicono quale routine chiamare quando arriva un'interruzione o un errore. Deve stare in una posizione e in un ordine precisi, decisi dall'hardware: si scrive a mano in assembler, perché il compilatore non saprebbe collocarla esattamente lì.

↑ la CPU parte dall'indirizzo 0, in cima
tabella dei vettoriasm
crt0 · avvioasm
codice del gioco (C → compilato)C
dati: grafica, livelli, musica

assembler · la tabella dei vettori

dc.l stack_top ; 0) indirizzo dello stack dc.l _start ; 1) da dove iniziare dc.l err, err ; errori di bus / indirizzo dc.l err, err ; istruzione illegale, ... ; (e cosi' via, in ordine fisso)

 

dc.l significa «qui va un valore a 32 bit». Ogni riga è una voce della tabella. L'hardware la legge sempre in questo ordine: spostare le voci manda in crash l'avvio.

03c · velocità

Quando conta la velocità

Il compilatore genera codice buono, ma in certi punti serve la massima velocità: per esempio copiare molti dati nella VRAM (la memoria video) entro il tempo di un frame, altrimenti si vede uno scatto. In questi punti critici l'assembler scritto a mano, scegliendo le istruzioni migliori, batte il compilatore. Sono poche righe, ma decisive per la fluidità.

assembler · scritto a mano, per la velocità

; copia veloce verso la VRAM: ; 4 byte per istruzione, senza sprechi move.l (a0)+, (a1) ; blocco 1 move.l (a0)+, (a1) ; blocco 2 move.l (a0)+, (a1) ; blocco 3 move.l (a0)+, (a1) ; blocco 4

 

Qui il controllo dev'essere totale: le istruzioni sono scelte a mano per essere le più rapide. È l'unico caso in cui l'assembler batte il compilatore, sfruttando un accorgimento che il compilatore non applica.

04 · la logica

Tutto il resto: la logica, in C

Tolti quei tre punti, tutta la logica di gioco (movimento del personaggio, salti, nemici, collisioni, punteggio) si scrive in C. È la parte che cambia più spesso, e in C è più leggibile e più veloce da modificare.

game.c · il salto del personaggio

void aggiorna_giocatore(void) { if (premuto(FUOCO)) player.vy = -FORZA_SALTO; /* salta su */ player.vy += GRAVITA; /* accelera verso il basso */ player.y += player.vy; /* aggiorna la posizione */ if (player.y > PAVIMENTO) player.y = PAVIMENTO; /* fermati al suolo */ }

 

Si legge quasi come pseudocodice: se si preme fuoco, salta; la gravità accelera verso il basso; aggiorna la posizione; fermati al pavimento. In assembler puro sarebbe il triplo delle righe e più difficile da modificare.

in sintesi

Due strumenti, un lavoro

assembler · il basso livello

- l'avvio e lo stack (crt0) - la tabella dei vettori - le routine critiche per la velocita' (es. VRAM)

C · la logica di gioco

- movimento del personaggio - nemici, salti, collisioni - vite, punteggio, game over - tutto cio' che cambia spesso

Assembler e C non sono in competizione: si dividono i compiti. L'assembler gestisce l'avvio e i punti a basso livello; il C esprime la logica in modo chiaro. Insieme sono più efficienti di ciascuno da solo. In più, lo stesso codice C si può compilare anche sul computer di sviluppo: così si collauda prima di metterlo nella ROM.

← Tutti i tutorial I muri superati sul Mega Drive