tutorial · commodore 64
Come scorre la strada
In Ruote Fumanti la strada scende dall'alto verso il basso, pixel per pixel, cinquanta volte al secondo, e non trema mai. Sembra la cosa più semplice del gioco. Su un Commodore 64 è quella che ha richiesto più ingegno, perché la macchina non ha niente di pronto per farlo: né una memoria video in cui disegnare liberamente, né un comando che «sposti l'immagine». Questo è il modo in cui ci si arriva, con i pezzi veri del programma.
la strada in corsa, registrata in VICE a cinquanta fotogrammi al secondo.
Il codice è assembler 6510, la lingua del processore del C64, e non serve saperlo leggere: ogni pezzo è spiegato riga per riga subito dopo. I commenti dentro il codice, quelli dopo il punto e virgola, sono gli originali, e sono in parte in inglese e in parte in italiano come li ho scritti.
Il C64 non disegna pixel: incolla mattonelle
Lo schermo è una griglia di 40 colonne per 25 righe. In ogni casella non si mette un'immagine, ma il numero di un carattere: un disegnino di 8 per 8 pixel preso da una tavola di 256. La strada, l'erba, i cordoli e l'acqua sono tutti caratteri ridisegnati apposta.
È un sistema velocissimo, perché per cambiare quello che si vede basta scrivere un numero in una casella. Ma per un gioco di guida ha un difetto grosso: le caselle sono fisse. Un carattere sta nella riga 5 o nella riga 6, non a metà. Se la strada si spostasse solo di caselle intere scatterebbe di 8 pixel alla volta, e a bassa velocità si vedrebbe benissimo.
Il trucco dei sette pixel
Il chip video, il VIC-II, ha un registro chiamato YSCROLL: vale da 0 a 7 e sposta tutta l'immagine verso il basso di quel numero di pixel. Lo scorrimento allora si fa in due tempi. Per i primi sette pixel non si tocca nessuna casella: si aumenta YSCROLL e l'intero schermo scivola giù. Quando si arriverebbe all'ottavo, si riporta YSCROLL indietro di 8 e nello stesso istante si ricopia tutta la strada una riga più in basso, mettendo una riga nuova in cima.
otto fotogrammi: nei primi sette cambia solo un registro, all'ottavo la strada si ricopia di una riga e il registro torna a zero. La riga scesa di una casella risale di otto pixel, e l'occhio non vede niente.
Chi guarda non vede il salto, perché le due cose si compensano esattamente. Restano due problemi, e il resto del tutorial è la loro soluzione. Il primo: ricopiare la strada richiede tempo, e durante la copia lo schermo sarebbe mezzo vecchio e mezzo nuovo. Il secondo: con YSCROLL a 7, in cima comparirebbe una fascia di pixel vuoti. Questo si risolve subito, perché il C64 ha una modalità a 24 righe invece di 25, che copre col bordo mezza riga in alto e mezza in basso: la riga che sta entrando resta nascosta dietro il bordo.
Quanto scorre in un fotogramma
Prima di tutto bisogna sapere di quanti pixel far scendere la strada, e dipende dalla velocità.
Il processore del C64 non sa moltiplicare: sa solo raddoppiare un numero (asl, «sposta a sinistra») e sommare (adc). Allora «sei volte la velocità» diventa due volte più quattro volte. lda fis_speed_hi legge la velocità in miglia orarie; i due asl tmp / rol tmp2 la raddoppiano e poi la raddoppiano ancora, e il valore da «due volte» viene messo da parte in count; le due somme mettono insieme due volte e quattro volte.
Il risultato non è in pixel ma in duecentocinquantaseiesimi di pixel: il byte alto, step_px, sono i pixel interi, il byte basso è la frazione. E la frazione non si butta: si somma a fis_frac, che la conserva da un fotogramma all'altro, e quando le frazioni accumulate fanno un pixel intero quel pixel viene aggiunto. È questo che rende lo scorrimento regolare anche a velocità «strane»: a 50 miglia orarie la strada scende di 1,17 pixel a fotogramma, cioè di norma 1 e circa un fotogramma su sei 2, sempre con la stessa cadenza. Senza l'accumulo scenderebbe di 1 pixel e basta, e andare a 50 o a 80 sembrerebbe uguale.
Sette pixel, poi la copia
Adesso che si sa di quanto scendere, questo è il pezzo che decide se basta YSCROLL o se serve copiare.
lda nextfine / adc step_px prende la posizione fine attuale, il valore di YSCROLL da 0 a 7, e ci somma i pixel di questo fotogramma. cmp #8 / bcs .coarse: se il totale arriva a 8 o più serve una copia, altrimenti sta nextfine salva il nuovo YSCROLL e rts esce. Nella maggior parte dei fotogrammi finisce qui: sette istruzioni, e la strada è già scesa.
Sotto .coarse si conta quante righe intere sono passate, una oppure due se il totale arriva a 16; in corsa, anche a 220 miglia orarie, i pixel per fotogramma sono al massimo sei, quindi di norma è una riga sola. and #7 è il «riporta YSCROLL indietro di 8»: tiene solo il resto, quindi se si era a 6 e si aggiungono 5 il totale è 11, si copia una riga e YSCROLL diventa 3. lda front / eor #1 / sta nextfront è il trucco del prossimo capitolo: front dice quale dei due schermi si sta guardando, ed eor #1 sceglie l'altro.
Due schermi, come un blocchetto animato
Il problema della copia a metà si risolve così: gli schermi sono due. Il C64 permette di dire al chip video in quale zona della memoria sta lo schermo, e Ruote Fumanti ne tiene due, uno a $4000 e uno a $4400. Uno è quello che si vede, l'altro è nascosto. Il programma scrive sempre in quello nascosto, con calma, e quando il disegno è finito dice al chip video di mostrare l'altro. È il principio del blocchetto animato: non si cancella il foglio che si sta guardando, si prepara il successivo e poi si gira pagina.
Questo non è codice che gira: è una ricetta per l'assemblatore, il programma che traduce l'assembler in codice macchina. !for vuol dire «ripeti»: per ognuna delle 24 righe e ognuna delle 25 colonne di strada scrivi due istruzioni. lda $4000+... legge il carattere nella riga .r dello schermo A, sta $4400+(.r+1)*40... lo scrive nello schermo B una riga più sotto. PANEL_COLUMNS salta le prime undici colonne, dove sta il pannello, che non scorre.
Il risultato sono 600 coppie di istruzioni una dopo l'altra, senza nessun ciclo. Oggi si scriverebbe un ciclo con un contatore, molto più corto. Qui però conta il tempo, non la lunghezza: sul 6510 spostare un byte con l'indirizzo indicizzato costa 9 cicli di processore, contro gli 8 con l'indirizzo scritto per esteso, e il ciclo stesso ne costa altri. Il commento originale, nel sorgente, lo dice così: «The scroll copy, unrolled. Indexed addressing costs nine cycles a byte and a loop on top; plain absolute costs eight and nothing on top.»
In numeri: 600 caratteri per 8 cicli fanno 4800 cicli. Un fotogramma PAL ne ha 19656, cioè 312 righe video per 63 cicli ciascuna, quindi la copia da sola si prende quasi un quarto del fotogramma. A tutta velocità, con poco più di cinque pixel a fotogramma, capita circa due fotogrammi su tre.
Una cosa che la copia non fa: non copia i colori. Nel C64 i colori stanno in un'altra memoria, e lì non c'è uno schermo di riserva. Per questo la strada è disegnata con quattro colori soli, e il colore proprio di ogni casella è lo stesso dappertutto: così i colori non vanno spostati, e scorrono gratis. L'unica eccezione è la bandiera a scacchi del traguardo, che ha un pezzetto di codice suo per il bianco e nero.
La pista non esiste, si compone
Dopo la copia, in cima allo schermo nascosto manca una riga. Una tappa è lunga 2304 righe, cioè 18432 pixel, e tenerla in memoria già disegnata costerebbe troppo: per una sola pista servivano dieci chilobyte, e le tappe sono otto. Allora la strada viene composta al momento, come facevano i cabinati da sala.
Funziona come un mosaico a tre livelli. La mappa della tappa è una lista di blocchi: ogni blocco è un pezzo di strada di 32 righe, con un numero che dice che forma ha, dritta, stretta, con il guado. Ogni blocco è una griglia di mattonelle larghe due caratteri e alte due. Ogni mattonella dice quali quattro caratteri disegnare. L'ultima riga del commento è la formula per trovare tutto a partire dal numero di riga: block = q>>5 vuol dire «dividi per 32», (q>>1)&15 trova la riga di mattonelle dentro il blocco, e q&1 dice se si sta disegnando la metà alta o quella bassa della mattonella. Di nuovo spostamenti di bit al posto di divisioni, perché dividere il processore non lo sa fare.
Così tutte e otto le tappe costano una tabellina di 128 byte più questa routine. E c'è un vantaggio che con lo spazio non c'entra: la fisica legge la strada dagli stessi caratteri disegnati sullo schermo. Quello che vedi sotto le ruote è esattamente quello su cui stai guidando, perché è la stessa cosa. Il pezzo che chiama la routine, subito dopo la copia:
screen_row_lo / screen_row_hi sono una tabella con l'indirizzo di ogni riga dello schermo, così non serve moltiplicare per 40. ldx nextfront / adc #4: se lo schermo di destinazione è il secondo, il suo indirizzo è quattro «pagine» più in là, $4400 invece di $4000. jsr decode_row compone la riga e la scrive lì, e dec native_rows_due / bne .incoming ripete se le righe da comporre erano due.
Girare pagina al momento giusto
Nello schermo nascosto adesso c'è il fotogramma nuovo, e c'è il nuovo valore di YSCROLL. Resta la parte più delicata: farli vedere insieme, e nel momento in cui il chip video non sta disegnando. Il VIC-II disegna l'immagine dall'alto in basso, una riga alla volta: se si cambia schermo mentre è a metà, la parte alta viene dal vecchio e quella bassa dal nuovo, e si vede uno strappo. Per evitarlo il C64 può chiedere al chip video di interrompere il programma quando arriva a una certa riga: sono gli interrupt raster.
un fotogramma PAL steso in orizzontale, da riga 0 a riga 311. Il lavoro comincia appena dopo la consegna, nel bordo in basso, e di norma finisce a metà della parte visibile.
Ruote Fumanti usa un'interruzione alla riga 304, sotto la parte visibile, dove il chip sta disegnando il bordo. È lì che si consegna il fotogramma.
lda ready / beq .miss: il programma principale alza ready solo quando ha finito tutto il lavoro del fotogramma. Se non l'ha finito non si consegna niente, si tiene il fotogramma precedente e si conta un fotogramma perso. Meglio ripetere un'immagine intera che mostrarne una a metà. lda nextfront / sta front: lo schermo preparato diventa quello da mostrare. I quattro asl e l'ora #8 costruiscono il valore del registro $d018, la cui metà alta dice al chip dove sta lo schermo e quella bassa dove stanno i caratteri: con front a 0 esce $08, schermo a $4000; con front a 1 esce $18, schermo a $4400. lda nextfine / sta fine: il nuovo YSCROLL.
I valori finiscono davvero nel chip un attimo dopo, in cima al quadro successivo, con un'altra interruzione:
lda fine / ora #$10 / sta $d011: in un solo registro ci vanno YSCROLL, nei tre bit bassi, e le impostazioni del video. Qui vale la pena essere precisi, perché è un registro che confonde tutti: il bit che ora #$10 accende è il quarto, e vuol dire «schermo acceso»; la modalità a 24 righe è il bit subito sotto, il terzo, e si ottiene lasciandolo a zero, che è proprio quello che fa scrivere $10 invece del solito $18. Il commento originale descrive l'effetto: è la finestra a 24 righe che nasconde la riga in entrata, ed è questo che rende lo scorrimento senza giunture. sta $d018 è il cambio di schermo. Tutto accade nel bordo, prima che il chip cominci a disegnare la strada: chi guarda vede sempre un'immagine completa.
Quaranta righe regalate
C'è un dettaglio nella consegna che vale la pena raccontare, perché è stato trovato misurando e non ragionando.
tick è il via al programma principale: «comincia il prossimo fotogramma». All'inizio il via veniva dato in cima al quadro, alla riga 32. Ma la consegna avviene alla riga 304, e fra la 304 e la 32, passando dal fondo e dal bordo in alto, ci sono quaranta righe in cui il processore restava fermo ad aspettare. Sono righe preziose, perché mentre il chip video disegna la parte visibile ogni tanto «ruba» tempo al processore per leggere i caratteri, mentre nel bordo non ruba niente.
Spostare quelle due istruzioni, lda #1 e sta tick, dalla riga 32 a subito dopo la consegna ha dato al lavoro circa 2500 cicli in più a ogni fotogramma. Misurato su tutte e otto le tappe a ingressi fissi, i fotogrammi persi sono scesi dall'1,93% allo 0,11%. Due istruzioni spostate, e il gioco ha smesso di perdere colpi.
morale: nessun ragionamento avrebbe trovato quelle quaranta righe. Le ha trovate un numero, misurato sempre allo stesso modo, prima e dopo.
Perché il pannello non balla
YSCROLL sposta tutto lo schermo, non solo la strada. Se punteggio, velocità e tempo fossero scritti con i caratteri, ballerebbero su e giù di sette pixel insieme alla strada. Per questo le scritte del pannello non sono caratteri ma sprite, che YSCROLL non tocca.
Solo che gli sprite sono otto, e servono anche per le auto: il pannello ne usa due soli. Ogni volta che il chip video arriva a una nuova fascia del pannello, un'altra interruzione sposta quei due sprite più in basso e cambia le cifre che mostrano. Il fondo del pannello invece è fatto di caratteri, ma scelti apposta, pieni o a strisce verticali per le cornici: sono uguali in ogni riga, quindi quando scendono di qualche pixel non se ne accorge nessuno.
Un fotogramma di Ruote Fumanti
- riga 304, bordo in basso se è pronto, si consegna: schermo e YSCROLL nuovi, e subito il via al lavoro
- programma principale fisica, e con la velocità i pixel da scorrere, frazioni comprese
- meno di otto pixel si aggiorna YSCROLL e basta
- otto o più copia nello schermo nascosto e riga nuova composta in cima
- riga 32, in cima il chip riceve schermo e YSCROLL, prima di disegnare la strada
- durante il disegno le interruzioni del pannello spostano i due sprite delle scritte
Misurato in VICE con ingressi fissi su 9600 fotogrammi e otto tappe: lo 0,1% di fotogrammi persi, e sei tappe su otto che non ne perdono nemmeno uno.