Completata la panoramica dei casi d’uso per sistema operativo e applicazioni dotate di interfaccia grafica, passiamo adesso a un argomento più succulento: i videogiochi 2D.
Molte delle considerazioni già fatte per s.o. e applicazioni (dotate di interfaccia grafica, ovviamente) valgono anche per i giochi 2D (che rappresentano la stragrande maggioranza del parco Amiga. D’altra parte l’hardware è votato proprio a questo tipo di grafica), ma in questo caso le prestazioni e il consumo di memoria assumono un ruolo molto più importante a causa dell’impatto che hanno l’aggiornamento della grafica, la sua varietà, e la ricchezza cromatica sui fruitori di tali opere.
Infatti il consumo di memoria stabilisce, ad esempio, quanti personaggi diversi possano esserci in un gioco (o livello di gioco), quante animazioni siano per essi a disposizione, quanto grande possa essere una mappa / scenario, quanti blocchi (tile, in gergo) si possano utilizzare per costruirle, ecc.. Mentre le prestazioni (banda di memoria, nello specifico) stabiliscono a quanto possa viaggiare l’aggiornamento di tali elementi (ad esempio 60 o 50FPS, a seconda che si tratti di sistemi NTSC o PAL. Oppure 30 o 25. Per giochi tecnicamente ancora più scarsi si scende sotto questi valori, con artefatti visivi particolarmente fastidiosi), ossia la sua “fluidità”. Quindi un buon gioco dovrà bilanciare tutti questi elementi per portare a un risultato soddisfacente e apprezzabile per gli utenti.
Per Amiga è disponibile un panorama molto variegato di giochi che hanno, quindi, motori grafici anche estremamente diversi (com’è anche giusto che sia: all’epoca si spingeva moltissimo sulle ottimizzazioni più disparate a causa della carenza di risorse a disposizione), dunque non si può fare un discorso specifico per ogni singola tipologia (o anche su precisi giochi), per cui tratterò l’argomento in maniera quanto più generica possibile perché alcuni aspetti più di basso livello sono comuni e validi per tutti i giochi.
Disegno e aggiornamento di fondali
Il primo, importante, elemento che condiziona tutte le operazioni che coinvolgono il fondale rimane sempre la dimensione del bus dati che, essendo a 16 bit, costringe a usare elementi grafici che orizzontalmente siano un multiplo di questa potenza del due, partendo dalla risoluzione dello schermo (o playfield, nel caso della modalità Dual Playfield che consente di utilizzare due schermi completamente indipendenti, come abbiamo già visto) fino ad arrivare alle “piastrelle” (tile, in gergo tecnico. Ed è quello che utilizzerò di seguito, al posto dell’equivalente italiano) utilizzati per comporre la grafica (per i giochi che impiegano tale tecnica, mimando il tipico hardware delle console o dei giochi da sala).
Per i fondali interamente disegnati a mano non ci sono particolari problemi né differenze fra grafica planare e packed, poiché le risoluzioni utilizzate all’epoca erano di 320 o 256 pixel orizzontali, entrambi largamente multipli di 16, per cui valgono esattamente le medesime considerazioni (ovviamente il discorso cambia con risoluzioni che non rispettino questo requisito, ma per Amiga non ce ne sono stati per ovvi motivi: non era possibile!).
Fondali con tile
Invece cambia, e non di poco, la situazione con la grafica che faceva uso di tile, poiché il bus dati costringe, per tali elementi, a utilizzare dimensioni che fossero orizzontalmente multipli di 16. Significa che si potevano utilizzare soltanto tile del tipo 16×16, 32×32, 64×64, ecc. (la risoluzione verticale era e rimane completamente libera, ma usualmente le tile erano quadrate e, quindi, le dimensioni orizzontale e verticale coincidevano) e non un formato molto più comune e vantaggioso qual era l’8×8.
I vantaggi delle tile 8×8 erano due. Il primo riguardava la possibilità di avere grafica più varia, in quanto spesso una singola tile 8×8 era sufficiente per coprire un particolare motivo grafico, come ad esempio quello per riempire il cielo, il mare, il terreno, e l’erba, tanto per fare un esempio. Per coprire questi casi con tile 16×16 (probabilmente le più diffuse su Amiga) sarebbe servito il quadruplo dello spazio.
Il secondo e non meno importante motivo riguardava la banda di memoria utilizzata per disegnare le tile sullo sfondo, che ovviamente è quadruplo per quelle 16×16 rispetto alle 8×8, ma è anche vero che viene renderizzata anche un’area quadrupla, per cui non cambierebbe assolutamente nulla, da questo punto di vista. Ma, al solito, il diavolo si nasconde nei dettagli.
Scroll di fondali facenti uso di tile
E il primo dettaglio (problema) si rivela, infatti, quando si deve implementare lo scroll. Quello orizzontale funziona in maniera semplice: ci sono colonne di pixel (pari alla larghezza di una tile) che sono nascoste a destra o a sinistra (a seconda della direzione in cui viene fatta muovere la grafica), le quali vengono riempite man mano che ci si sposta a destra o a sinistra. Quindi il motore grafico del gioco si occuperà di disegnare le tile in quest’area nascosta, per poi “scoprirle” non appena ciò sarà necessario. Una volta “scoperta” l’area, se ne “aggiungerà” (lasciatemi semplificare il concetto) un’altra in cui verrà ripetuta nuovamente la stessa operazione, e così via, dando l’illusione al giocatore che lo schermo sia molto grande.
Quello verticale funziona allo stesso modo, ma con la differenza che l’area nascosta si trova sopra o sotto lo schermo visualizzato, e viene aggiornata a seconda che ci si muova sopra o sotto, per l’appunto. Infine lo scroll multidirezionale si ottiene banalmente combinando i due tipi di scroll (quindi usando contemporaneamente aree nascoste a destra/sinistra e in alto/basso).
Il problema naturalmente viene fuori quando queste aree nascoste debbono essere aggiornate, perché richiedono un consistente lavoro per il Blitter, che si traduce in un bel po’ di banda di memoria necessaria / utilizzata. Prendendo il classico schermo 320×200 (NTSC) e supponendo di avere le classiche (per Amiga) tile 16×16, ciò significa che per lo scroll orizzontale servirà aggiornare una colonna di 16×200 pixel, copiando 200 / 16 = 13 (arrotondato) tile. Per lo scroll verticale servirà aggiornare invece, una riga di 320×16 pixel, copiando 320 / 16 = 20 tile. Infine per lo scroll multidirezionale servirà aggiornare entrambe le aree, quindi 14 (perché ci sono 16 righe in più a causa dello scroll verticale, quindi 216 / 16 = 14) + 21 (perché ci sono 16 colonne in più a causa dello scroll orizzontale, quindi 336 / 16 = 21) – 1 (perché una tile è in comune) = 34 tile. Per uno schermo PAL (320×256) la situazione peggiora, perché serve aggiornare 256 / 16 = 16 tile per lo scroll orizzontale e 17 + 21 – 1 = 37 tile per quello multidirezionale.
Magari può sembrare non molto, ma tenendo conto del fatto che i fondali erano generalmente costituiti da 5 bitplane (32 colori), il carico di lavoro del Blitter era parecchio e idem il consumo della preziosa banda di memoria. Per schermi Dual Playfield, invece, si parla di soli 3 bitplane (8 colori) da aggiornare (perché i due schermi indipendenti potevano avere al massimo 8 colori. 16 con AGA), con un buon risparmio, ma comunque significativo, se si tiene conto che diversi giochi aggiornavano la grafica ogni 60 (NTSC) o 50 (PAL) fotogrammi al secondo e, quindi, con poco tempo a disposizione per aggiornare tutta la grafica a ogni fotogramma.
I problemi con le tile 8×8
Con la grafica packed, invece, si possono utilizzare tile da 8×8 pixel (in realtà si potrebbero utilizzare anche con la grafica planare, ma verrà trattato meglio dopo), dimezzando la richiesta di banda di memoria per lo scroll verticale, ma con alcuni problemi per quello orizzontale quando di mezzo ci siano profondità di colore che non fossero potenze del due (per potenze del due è tutto a posto e, anche in questo caso, la richiesta di banda di memoria risulta dimezzata. Ovviamente in questo caso anche tile monocromatiche = a due colori creano gli stessi problemi).
Vediamo adesso quali sono le rogne che saltano fuori con grafica che abbia profondità di colore non potenze del due, come ad esempio con 8 colori = 3 bit per pixel. In questo caso ogni riga di una tile 8×8 richiede 8 x 3 = 24 bit = 3 byte di spazio. Poiché il bus dati è sempre di 16 bit, è chiaro che spunta un grosso problema: non possiamo copiare soltanto questi 24 bit (per lo meno col Blitter), ma ogni volta dobbiamo copiarne sempre 32 (2 word a 16 bit), avendo cura di “scartarne” 8 per ogni riga.
Il che significa ricorrere nuovamente al famigerato e arcinoto cookie-cut, provocando un impatto a livello prestazionale. Infatti e guardando al diagramma della pipeline del Blitter, possiamo vedere che operazioni che coinvolgano i canali B, C, e D (il canale A viene utilizzato, ma non deve caricare niente dalla memoria, mentre le sue due maschere iniziali e finali dovranno essere impostate opportunamente. Evito i dettagli per non appesantire ulteriormente la discussione) richiedano 22 cicli di clock (11 Slot / ColorClock) per essere completati, effettuando 6 accessi alla memoria. Questo ovviamente soltanto per la prima riga della tile, in quanto per copiare i suoi 24 bit serviranno 2 accessi alla memoria ogni volta (cioè 2 Slot) per ogni canale utilizzato.
Per confronto, copiare una riga di una tile 16×16 richiede 8 cicli di clock (4 Slot) e 2 accessi alla memoria per singolo bitplane, che moltiplicato per 3 (per grafica a 8 colori) fanno 24 cicli di clock e 6 accessi alla memoria. Con la pregevole differenza che, rispetto alla tile 8×8, è stato copiato il doppio della grafica!
Sempre per confronto, se volessimo comunque utilizzare tile 8×8 anche con la grafica planare (finora abbiamo parlato soltanto di quella packed in questo contesto), l’operazione di copia della prima riga richiederebbe 26 cicli di clock (13 Slot) e 9 accessi alla memoria. Quindi non molto distante (ma più lenta) rispetto all’equivalente packed in termini di tempo d’esecuzione, ma decisamente più onerosa per quanto riguarda il consumo della preziosa banda di memoria (ben il 50% in più!).
Per chiudere questa parte, le tile 8×8 potrebbero essere un’opzione per la grafica packed, perché consentono almeno di risparmiare banda nel caso di scroll verticale, senza dover ricorrere a formati difficili da gestire per i grafici (artisti), come il 16×8, mentre sono sconsigliati per la grafica planare (c’è oggettivamente troppo spreco di banda: il triplo rispetto alle tile 16×16!).
Ma sono soprattutto da prendere in considerazione in caso di problemi di spazio, perché possono occupare fino a 1/4 rispetto alle equivalenti 16×16 (ovviamente per rappresentare la stessa grafica). Viceversa, lo stesso spazio può, invece, essere utilizzato per ottenere grafica più variegata (mappe più dettagliate), perché lo spazio risparmiato si potrebbe impiegare per altre tile.
E’, poi, da sottolineare che, usando tile 8×8, la CPU può benissimo procedere in parallelo col Blitter, magari preparando tutto il necessario per disegnare la successiva tile. Ciò è possibile in quanto tale coprocessore lascia un bel po’ di slot (di memoria = accessi) liberi che la CPU può tranquillamente impiegare per prelevare istruzioni da eseguire o per leggere/scrivere dati da/in memoria. Anche questo è un aspetto da non sottovalutare nella decisione da prendere per la realizzazione di un gioco che voglia spremere al meglio la macchina.
Fermo restando, e ne approfitto per rimarcarlo ulteriormente, che le problematiche discusse sono valide per la grafica packed esclusivamente nel caso di grafica monocromatica o con profondità di colore che non siano potenze del due. Diversamente (4, 16 e 256 colori), le tile 8×8 sono perfettamente utilizzabili senza alcuna penalizzazione e con tempi di elaborazione e richieste di banda che, come si ci aspetterebbe, sono all’incirca 1/4 rispetto alle tile 16×16, rendendole la scelta di riferimento / preferenziale per i giochi che ne facciano uso.
Disegno di oggetti/personaggi (“Sprite” realizzati col Blitter)
Molte delle considerazioni già fatte si applicano anche quando si utilizza il Blitter per emulare il funzionamento degli sprite (che in questo caso vengono chiamati BOB, in gergo amighista), quindi dovendosi occupare di:
- conservare la parte del fondale dove il BOB verrà disegnato;
- disegnare il BOB;
- ripristinare il fondale sporcato dal BOB.
Il primo punto in genere è evitato nei giochi che prendono il pieno controllo del sistema, poiché con tecniche di doppio o triplo buffer si tiene una copia del fondale proprio per eliminare l’impatto prestazionale dovuto alla copia della parte interessata da un BOB (ma al prezzo, anche molto elevato, di dover tenere una copia completa del fondale). Per i pochi giochi che usano le API del s.o. per gestire i BOB, un buffer per salvare lo sfondo è richiesto (altrimenti la loro gestione si complica).
Qui cominciano a emergere delle differenze significative e anche pesanti per quanto riguarda la grafica planare e quella packed. Come ben sappiamo ormai, tutto il sistema Amiga è condizionato / dominato dal bus dati a 16 bit, il quale forza ogni operazione di accesso in memoria a questa dimensione (in realtà si potrebbe accedere anche a singoli byte, ma buttando via metà della banda di memoria in questo caso. Cosa tutt’altro che desiderabile, sebbene la CPU abbia i suoi vantaggi nel farlo).
Per tale motivo, anche il Blitter lavora sempre e soltanto con word (16 bit) e ciò obbliga la grafica a dover mantenere sostanzialmente dimensioni e allineamento che rispettino tale valore. Il che ha comportato il fatto che, come per le tile, anche i BOB debbano avere una larghezza che sia un multiplo di 16, a prescindere dalla larghezza reale. Per cui i BOB avranno larghezza di 16, 32, 48, 64, ecc., mentre e fortunatamente l’altezza rimane arbitraria (come gli sprite di cui abbiamo già parlato).
La grafica packed non è esente dai limiti imposti dal bus dati, ma risulta nettamente avvantaggiata dal fatto che i bit degli indici dei colori siano tutti impacchettati assieme, per l’appunto, per cui il vincolo dei 16 bit da rispettare si applica collettivamente. Quindi, e sempre tornando all’esempio della grafica a 8 colori, poiché in 16 bit si possono impacchettare 5 pixel e un terzo (cioè un bit dei tre di un altro pixel), il limite (minimo) sarà rozzamente rappresentato da questo: tutti gli oggetti grafici dovranno essere un multiplo di tale quantità.
Passando dalla teoria alla pratica, significa che, ad esempio, è possibile avere BOB di 5 pixel (circa) di larghezza senza sprecare praticamente nessuna memoria (e relativa banda, per le operazioni coinvolte), a parte il singolo bit “orfano” del sesto pixel di cui abbiamo parlato già diverse volte. Mentre con la grafica planare i BOB debbono avere una larghezza minima di 16 pixel, come abbiamo visto, anche se poi effettivamente la grafica da visualizzare prenderebbe 5 pixel, tanto per fare un altro esempio, con annesso spreco di memoria e di banda (tutte le volte che il BOB dovrà essere disegnato, e lo sfondo del fondale prima salvato e poi ripristinato).
Situazione che migliora ancora, a favore della grafica packed, all’aumentare della profondità di colore. Con grafica a 16 colori, infatti, una word (16 bit) contiene 4 pixel (e nessun bit extra), per cui questa sarà la larghezza minima per un BOB. Con 32 colori ci staranno 3 pixel (e un bit per il quarto pixel) e infine con 64/128/256 colori la word conterrà soltanto 2 pixel (con 4 e 2 bit extra per il terzo pixel, rispettivamente, per i primi due casi).
Giochi con un singolo schermo – Turrican
Gli aridi numeri possono ancora lasciare dubbi sulle implicazioni e relative ricadute di quanto appena descritto, per cui qualche esempio preso da giochi famosi servirà a rimuovere eventuali incertezze rimaste. Partiamo con Turrican (che ovviamente non ha bisogno di presentazioni!) e cominciamo con l’analisi della terza schermata di un sito più completo che abbia trovato su questo giochi (e molti altri):
Se apriamo l’immagine con un programma di grafica, possiamo osservare come la larghezza del “proiettile” (il secondo elemento evidenziato in rosso) risulti di 12 pixel (e 5 di altezza, ma questa dimensione è ininfluente per quest’argomento e non verrà più presa in considerazione).
Poiché il gioco utilizza grafica a 32 colori (profondità di colore di 5 bit), una riga di tale oggetto utilizzerà 12 x 5 = 60 bit = 8 byte (approssimando al più vicino multiplo di 16 bit) con grafica packed. Con grafica planare occuperà, invece, 16 x 5 = 80 bit = 10 byte: il 25% in più (in realtà per questo gioco ne occupa meno, ma ne parlo meglio dopo). Lo spazio occupato dalle rispettive maschere (vedasi l’articolo sul cookie-cut per maggiori dettagli su cos’è una maschera e come viene utilizzata per disegnare BOB), invece, rimane sempre lo stesso (perché, essendo grafica monocromatica = a 2 colori, dovrà sempre essere un multiplo di 16).
I vantaggi penso siano innegabili, ma soprattutto lo sono in termini di banda di memoria utilizzata, in quanto la grafica packed richiede, oltre a meno letture dalla memoria (una word in meno per ogni riga, in questo caso), una sola lettura della maschera a prescindere dal numero di colori dello schermo in cui tale BOB dovrà essere disegnato (come già spiegato nel suddetto articolo sul cookie-cut). A cui si aggiunge il fatto che il Blitter vada programmato una sola volta e non cinque (per questo gioco).
Complessivamente, quindi, la grafica packed consente di risparmiare la lettura di 1
(dalla grafica) + 4
(dalla maschera) = 5
word per ogni singola riga di quest’oggetto che dovrà essere disegnato: notevole! In realtà il risparmio effettivo per questo specifico gioco sarà di 4 word (che è comunque tanto, considerato che avviene per ogni riga), perché ci sono delle particolari considerazioni da fare che non sono immediatamente visibili per un non addetto ai lavori e richiedono un’appropriata spiegazione.
Ho scritto prima che quel proiettile occupa 16 x 5 = 80 bit più altri 16 bit per la maschera, per ogni sua singola riga. Questo sarebbe vero se venissero utilizzati 32 colori, ma in realtà questi oggetti ne usano soltanto 16. Infatti la tavolozza dei colori del gioco è (dovrebbe essere. Non ho controllato il codice del gioco né ho visto come funziona usando WinUAE, ma sto riportando le mie intuizioni di programmatore di giochi per Amiga) suddivisa in due parti: i primi 16 colori (i primi 17, in realtà) sono specifici del fondale, mentre i successivi 16 (gli ultimi 15, per l’esattezza) sono quelli utilizzati sia dal fondale sia da sprite e BOB.
Questo tipo di suddivisione della grafica è tipica dei giochi che consentono di giocare più livelli, quindi più ambientazioni / mappe. Il motivo per cui la tavolozza viene suddivisa in questo modo è dovuto al fatto che non si può (né si vuole!) ridisegnare ogni volta tutta la grafica di sprite e BOB per ogni livello: sarebbe un lavoro immane!
Quindi si lasciano fissi gli ultimi 15 colori (non 16, perché il 17°, che corrisponde al primo degli ultimi 16, viene usato per la trasparenza. Quindi per “forare” la grafica dello sprite o BOB e far visualizzare quella dello sfondo) per tutti i livelli, mentre i primi 17 saranno cambiati ogni volta in modo che i singoli livelli possano godere di un’adeguata variazione cromatica (immaginate un livello a mare e uno in montagna: i colori usati sono molto diversi, mentre soltanto alcuni sono in comune).
Con questo trucchetto la grafica dei BOB richiede meno spazio: 16 x 4 = 64 bit = 8 byte. Cioè quanto l’equivalente packed. Bisogna stare attenti, però, quando si devono disegnare, perché lo schermo rimane pur sempre a 32 colori, quindi ogni volta devono essere aggiornati 5 bitplane. In questo caso i primi 4 bitplane vengono “incastrati” nello sfondo (col classico cookie-cut) come sempre (facendo uso della maschera per scegliere se visualizzare la grafica del BOB o “forarla” e mostrare, invece, quello dello sfondo).
Mentre per l’ultimo bitplane (il quinto), e non essendoci dati a disposizione, si utilizzerà direttamente la maschera che, nel caso in cui indicherà di dover visualizzare la grafica del BOB, imposterà contemporaneamente a 1
il valore del bitplane, quindi selezionando la seconda tavolozza di 16 colori (assumendo idealmente di aver diviso i 32 colori in due tavolozze da 16 colori). Anche qui si usa il cookie-cut, ma riutilizzando i dati della maschera anche per i dati di tale bitplane si ottiene il risparmio di una lettura di dati dalla memoria (è il motivo per cui il vantaggio in termini di banda di memoria della grafica packed è passato da 5 a 4 word lette in meno).
Ricapitolando, usando esclusivamente gli ultimi 16 (15, in realtà) colori per i BOB, si ottiene un risparmio di spazio del 25% e una word in meno da leggere dalla memoria per ogni riga da disegnare. Per contro, la grafica non è a 32 colori, ma soltanto a 16. Rimane un buon compromesso, ma abbiamo visto che la grafica packed è in grado di fare molto meglio almeno in termini di banda (quanto a spazio, invece, si equivalgono).
Passando alla “bolla energetica” alla sinistra (sempre evidenziata in rosso), la situazione è simile, ma questa volta la larghezza è di 9 pixel. Quindi con grafica packed una riga richiederà 9 x 5 = 45 bit = 6 byte. Mentre con grafica packed occuperà sempre 16 x 4 (sempre per il trucchetto spiegato prima, che fa risparmiare la memorizzazione di un bitplane) = 64 bit = 8 byte, quindi il 33% di spazio in più.
Inoltre in termini di banda di memoria la grafica packed richiederà 3 (il numero di word da disegnare) x 4 (il numero di canali usati per il cookie-cut) = 12 accessi alla memoria per ogni riga. Mentre quella planare richiederà 1 (numero di word da disegnare per riga) x 4 (numero di bitplane) x 4 (canali per il cookie-cut) + 1 (word da disegnare) x 1 (bitplane) x 3 (canali) = 16 + 9 = 25 accessi alla memoria: più del doppio rispetto all’equivalente packed! Praticamente con la grafica packed si potrebbero disegnare due “bolle” anziché una, sprecando pure meno banda, e senza nemmeno tenere conto del fatto che la grafica planare richiede di programmare il Blitter 5 volte, mentre quella packed soltanto una.
La “fiamma” (a destra, evidenziata sempre in rosso) che fuoriesce dall’arma del personaggio è larga 12 pixel, quindi valgono le stesse considerazione fatte per il “proiettile”.
Prendendo adesso la dodicesima schermata:
si possono fare considerazioni simili. La “bolla” più in alto (evidenziata in verde) è larga 8 pixel, quindi richiede 8 x 5 = 40 bit = 6 byte (si approssima sempre al multiplo di 16 bit più vicino) per ogni riga. Quindi valgono le stesse considerazioni di cui sopra.
Quella più in basso, invece, è larga 6 pixel, quindi richiede 6 x 5 = 30 bit = 4 byte. Qui il risparmio di spazio e di banda di memoria è ancora più rilevante.
Infine uno dei nemici è evidenziato (sempre in verde), e in questo gioco molti di questi sono larghi 24 pixel (potete controllare anche nelle altre schermate). Per cui lo spazio richiesto per ogni riga è pari a 24 x 5 = 120 bit = 16 byte. L’equivalente planare richiede 32 x 4 = 128 bit = 16 byte, quindi la memoria usata coincide. Mentre per la banda di memoria abbiamo rispettivamente 8 x 4 = 32 accessi richiesti per la grafica packed e 2 x 4 x 4 + 2 x 1 x 3 = 32 + 6 = 38 accessi.
Per correttezza, è bene evidenziare che se la dimensione dei BOB risultasse maggiore di 25 pixel, allora la grafica packed richiederebbe più spazio rispetto a quella planare (d’altra parte visualizza sempre 32 colori e non 16. Quindi la grafica packed richiede al massimo il 25% di spazio in più), e di conseguenza salirebbe anche il consumo di banda di memoria fino a superare leggermente l’equivalente della grafica planare (40 accessi per oggetti larghi 32 pixel).
Questo fino a 32 pixel di larghezza: da 33 in poi quella packed ricomincerebbe a guadagnare (di parecchio) fino nuovamente a scendere nuovamente arrivati a 48 pixel di larghezza. Ciò per fare capire i rapporti fra le larghezze degli oggetti e i relativi andamenti per grafica packed e planare.
Un ultimo doveroso appunto riguarda la maggior possibilità, per la grafica packed, di suddividere gli oggetti grafici in parti più piccole, a motivo dei vantaggi in termini di spazio e utilizzo della banda che da ciò ne derivano, oltre al fatto che il Blitter vada programmato una sola volta.
Se prendiamo, ad esempio, il “grappolo” di “bolle” di cui quella più in alto fa parte, si può osservare come sarebbe possibile ipotizzarne la suddivisione per ogni singolo elemento. A mio avviso ciò non dovrebbe esser stato fatto in questo gioco perché verrebbe fuori un enorme spreco di banda di memoria che comprometterebbe le prestazioni del gioco.
Giochi con due schermi (Dual Playfield) – Shadow of the Beast
Un altro gioco che non bisogno di presentazioni e rappresenta, ancora oggi, il vanto della nostra splendida nonché amatissima piattaforma, è certamente Shadow of the Beast:
Ho citato anche questo gioco, perché si differenzia nettamente da Turrican, in quanto utilizza la modalità Dual Playfield che consente di visualizzare due schermi indipendenti di 8 colori ciascuno (7 per quello che sta in fronte, poiché il primo colore, di indice zero, viene utilizzato per “forare” la sua grafica e far visualizzare quella dello schermo che sta sotto).
In questo caso trucchetti come quello di utilizzare un bitplane in meno per risparmiare spazio è molto difficile che possano essercene: la grafica è già di per sé molto povera quanto a tavolozza dei colori (7 colori per i BOB sono veramente pochi!) e si cerca di sfruttarla al meglio per tutta la varietà degli oggetti che serviranno. Cosa che si può già notare nella schermata qui sopra, dove il “mostro-pipistrello” è caratterizzato facendo ricorso ad appena tre colori.
Il che non sembrerebbe possibile andando a guardare tutti gli sprite e BOB che sono stati estratti dal gioco, poiché si vede l’utilizzo di parecchi colori in generale. In realtà ciò è frutto del sapiente lavoro dei programmatori, i quali cambiano opportunamente la tavolazza dei colori dello schermo frontale (quello in cui vengono disegnati i personaggi) a ogni blocco di nuovi nemici con cui “la bestia” deve avere a che fare, dando l’illusione di una maggior varietà cromatica.
Com’è possibile vedere, alcuni BOB hanno una larghezza non multiplo di 16 pixel, per cui con la grafica packed si riuscirebbe a risparmiare un po’ di memoria e, in particolare, di banda di memoria, considerato che i giochi che usano il Dual Playfield utilizzano ben 6 bitplane, lasciando pochi Slot / ColorClock liberi per Blitter, Copper e CPU.
A parte questo, bisogna anche evidenziare come il fatto di essere costretti (per la grafica planare) ad avere oggetti con larghezza che sia un multiplo di 16 comporti anche restrizioni agli artisti, che sono costretti a disegnare grafica meno realistica e a volte addirittura goffa. Esempi se ne possono vedere nel suddetto elenco, di cui ne riporto alcuni qui di seguito:
Alcuni di questi nemici è come se risultassero tagliati, mentre altri sono slanciati verticalmente, e spesso con le braccia sempre alzate proprio per occupare meno spazio orizzontalmente. Limiti che è possibile, invece, ridurre drasticamente con l’utilizzo di grafica packed, che ha requisiti di granularità / allineamento molto meno marcati, specialmente aumentando il numero dei colori.
Infine, e non meno importante, nei giochi in cui ci sia da colpire qualche oggetto (quindi anche in Turrican) siamo abituati a vederlo diventare interamente bianco per una frazione, “segnalando” il momento in cui sia avvenuto l’impatto. Ciò si ottiene grazie a un abile trucchetto dei programmatori, i quali utilizzano la maschera di un oggetto anche per i dati di tutti bitplane. In questo caso non c’è, quindi, alcuna grafica memorizzata e da usare: è presente esclusivamente la maschera che viene impiegata per impostare a 1
anche il bit relativo alla grafica vera e propria.
In soldoni, ciò vuol dire che in un gioco che usi 8 colori per i BOB (come in questo caso), quando la maschera sarà 1
allora l’indice del colore risultante sarà 7
(che in binario è %111
, cioè tutti i bit dei bitplane sono impostati). In giochi con 32 colori l’indice che viene fuori sarà, invece, 31
(%11111
in binario). Avendo cura di mappare il bianco come indice 7
o 31
, si otterrà l’effetto desiderato.
Oltre all’enorme vantaggio quanto a spazio occupato, ovviamente risultano benefici anche in termini di risparmio di banda di memoria. Infatti in questo caso per disegnare un oggetto largo 16 pixel (ad esempio) su uno schermo a 8 colori saranno necessari soltanto 3 (lettura maschera, lettura fondale, scrittura fondale) x 3 (bitplane) = 9 accessi in memoria per ogni singola riga dell’oggetto, contro i 4 x 3 = 12 richiesti normalmente (uno in più per leggere i dati della grafica vera e propria). Per grafica a 32 colori gli accessi saranno, invece, 3 x 5 = 15 anziché 4 x 5 = 20.
La grafica packed, però, consente di fare molto meglio. Infatti la maschera viene letta una sola volta, qualunque sia la profondità di colore della grafica. Questo significa che per disegnare una riga dello stesso oggetto “colpito” largo 16 pixel serviranno soltanto 1 + 2 x 3 = 7 accessi per grafica a 8 colori, mentre ne serviranno 1 + 2 x 5 = 11 per quella a 32 colori. Direi che i numeri parlino da soli e non serva aggiungere altro.
Ulteriori vantaggi della grafica packed
Un altro fattore che non è immediatamente visibile da quanto scritto finora viene fuori andando a controllare cosa succede quando un certo oggetto debba essere disegnato a una determinata posizione orizzontalmente.
Con la grafica planare sappiamo che tali oggetti debbano essere obbligatoriamente larghi un multiplo di 16. Inoltre è sufficiente che la posizione orizzontale non sia anch’essa un multiplo di 16 per richiedere il cambiamento di due word (16 bit) dello sfondo che si stia andando a modificare, anziché una.
Quindi e per fare un esempio, se la coordinata x vale 0
(oppure 16, 32, 48, 64, ecc.) e il nostro oggetto fosse largo 16 pixel, non succederebbe nulla di strano: servirà leggere, modificare (disegnandoci sopra la grafica dell’oggetto) e infine scrivere una sola word per ogni riga da visualizzare. Ma appena si andasse fuori da questi valori, il costo raddoppierebbe. Infatti per x che vale 1
(2, 3, 4, …, 15, 17, 18, …) servirebbe leggere due word dal fondale, disegnarci sopra i 16 pixel dell’oggetto (che risultano distribuiti nelle due word del fondale: 15 nella prima e 1 nella seconda), e infine scrivere le due word modificate. Il tutto ripetuto per ogni bitplane da processare.
Tutto ciò risulta molto attenuato con la grafica packed, perché gli indici colore dei pixel si trovano tutti impacchettati assieme, uno di seguito all’altro, e in base alla profondità di colore ci saranno più o meno problemi di allineamento della grafica, ma in ogni caso facendo mediamente sempre meglio di quella planare.
Infatti e prendendo gli stessi esempi, per x = 0
e sempre tenendo conto che l’oggetto abbia una larghezza di 16 pixel, con grafica di 8 colori sarà necessario leggere e scrivere in totale 16 x 3 = 48 bit = 3 word del fondale e scriverne altrettante. Non cambia nulla rispetto alla grafica planare, a eccezione del fatto che si risolve tutto con una sola programmazione del Blitter e leggendo una sola volta la maschera (16 bit). Similmente, con grafica a 32 colori servirà leggere e scrivere 16 x 5 = 80 bit = 5 word del fondale.
Ma già con x = 1
la situazione cambierebbe radicalmente. Infatti in questo caso, e con grafica a 8 colori, avremmo la necessità di leggere e scrivere una word in più dal fondale, perché la grafica dell’oggetto sarà distribuita su 4 word anziché 3 (13 bit nella prima word, 16 + 16 nelle due successive, e infine 3 bit nell’ultima). Per contro, con la grafica planare sarebbe necessario leggere 2 x 3 = 6 word del fondale e scriverne altrettante: un aumento di ben il 50%.
Con grafica a 32 colori la situazione è molto simile. Infatti per disegnare i 16 pixel dell’oggetto per x = 1 servirà leggere e scrivere 5 + 1 = 6 word del fondale, perché i 16 x 5 = 80 bit sono distribuiti in 6 word anziché 5 (11 bit nella prima word, 16 + 16 + 16 + 16 nelle successive quattro, e 5 nell’ultima). Mentre con la grafica planare sarà necessario leggere 2 x 5 = 10 word del fondale e scriverne altrettante, con un aumento del 67% in termini di banda di memoria utilizzata.
Situazioni simili capitano quando è necessario salvare la grafica del fondale (nei giochi che non fanno uso del doppio o triplo buffer) e poi ripristinarla: i requisiti di allineamento dei trasferimenti alla dimensione del bus dati (16 bit) comportano sempre maggiori oneri (al più, ma soltanto nei casi più rari, lo stesso onere) nel caso della grafica planare rispetto a quella packed.
In generale, si può affermare che la grafica packed necessiti sempre di al più una word in più in tutte le operazioni grafiche, a prescindere dalla profondità di colore, mentre quella planare avrà bisogno sempre di al più una word in più, ma per ogni bitplane da elaborare.
Giochi con trasparenze (EHB: Extra-Half-Brite) – Fightin’ Spirit
Dopo i vantaggi passiamo brevemente agli svantaggi della grafica packed con giochi che utilizzavano la poco conosciuta modalità Extra-Half-Brite (EHB, in breve), la quale consentiva di visualizzare ben 64 colori a schermo (quindi offrendo un’ottima ricchezza cromatica rispetto ai giochi a 32 colori), ma con la limitazione che gli ultimi 32 colori sono ottenuti sempre partendo dai primi 32, però con luminosità dimezzata (da cui il nome, per l’appunto). Non una grossa limitazione, comunque, perché molto spesso la tavolozza dei colori necessita dei cosiddetti “mezzi toni”.
Il gioco a cui ho avuto l’onore e il piacere di lavorare (come programmatore addizionale), Fightin’ Spirit, ha fatto ampio uso di questa modalità non soltanto per la grafica dei fondali e dei personaggi, ma anche per il disegno delle ombre riflesse sul pavimento, che proprio grazie all’EHB vengono fuori in maniera naturale e molto accurata (per la grafica dei tempi!), come potete vedere da una schermata:
Mentre in giochi senza l’uso di EHB tali ombre venivano disegnate come oggetti opachi, generalmente a tinta unica, come possiamo vedere ad esempio nei vari port del picchiaduro più famoso al mondo (la schermata è presa dalla versione Amiga):
La peculiarità delle ombre di Fightin’ Spirit è che sono state realizzate a costi irrisori in termini sia di spazio sia di banda di memoria consumata. Infatti veniva usato un solo bitplane (anziché i 5 + 1 = 6 necessari per la grafica dei personaggi) per memorizzare la grafica e sempre un solo bitplane (l’ultimo, il sesto: quello che decideva se impiegare il colore normale oppure quello a luminosità dimezzata, a seconda se i suoi bit fossero stati impostati a 0
oppure a 1
) veniva modificato nella grafica del fondale (sempre con la grafica del cookie-cut) per sovraimporla.
Questo trucchetto non è ovviamente utilizzabile con la grafica packed, perché non ci sono bitplane e ogni volta che si disegna qualcosa si modificano tutti i bit degli indici colore dei pixel interessati dall’operazione.
L’unico modo per emulare lo stesso effetto è di conservare in memoria l’ombra (perché in Fightin’ Spirit ha una sola forma, ovale, per tutti i personaggi) come un qualunque altro oggetto grafico, quindi usando 6 bit per pixel e avendo l’accortezza di impostare a 1
soltanto il sesto bit (quello che, appunto, consente di scegliere fra il colore normale e quello a luminosità dimezzata). In questo modo quando si disegnerà l’ombra sarà sufficiente eseguire l’or
binario della grafica dell’ombra con quella del fondale, che riprodurrà esattamente lo stesso comportamento della grafica planare.
Il prezzo da pagare è, però, quello di utilizzare sei volte lo spazio richiesto rispetto alla grafica planare e ovviamente sei volte la banda di memoria necessaria ogni volta che si doveva disegnare un’ombra (quindi due volte). Il costo è notevole, ma fortunatamente l’ombra è abbastanza piccola (come si può vedere), per cui non avrebbe un grosso impatto sulle prestazioni del gioco (che viaggia a 25 FPS circa), anche perché verrebbe ammortizzato da quando scritto sopra riguardo ai vantaggi della grafica packed.
Sintesi finale
Ricapitolando e per chiudere, il concetto che emerge è che l’unico vantaggio concreto della grafica planare nella realizzazione di videogiochi 2D consiste nel poter risparmiare al massimo il 25% di spazio nei soli casi in cui sia possibile sfruttare trucchetti come quelli citati in Turrican, cioè usando meno colori (16) rispetto a quelli del fondale (32).
l caso delle ombre usate in Fightin’ Spirit è anch’esso un netto vantaggio, ma rimane limitato ai soli giochi a 64 colori (sono stati pochi per Amiga), e comunque il suo impatto non è determinante (si fa poco uso di trasparenze di quel tipo in un gioco, e questo a livello generale).
Vantaggio che, a mio avviso, risulta ampiamente compensato con la grafica packed grazie alla possibilità di avere oggetti che non abbiano dimensioni orizzontali multiple di 16 (a motivo della minor granularità), consentendo, dunque, di risparmiare spazio quando si verifica ciò. Oltre al fatto che è possibile suddividere ulteriormente grossi oggetti in pezzi più piccoli, ottenendo benefici sia in termini di spazio occupato che di banda di memoria utilizzata.
Sarebbe possibile risparmiare memoria usando grafica con meno colori esattamente allo stesso modo anche con quella packed, con un’altra modifica al funzionamento del Blitter (e in maniera similare all’algoritmo di generazione delle maschere che ho esposto in precedenza), ma richiederebbe l’utilizzo di un bel po’ di transistor (per buffer, barrel shifter, e mux aggiuntivi allo scopo) e non ne varrebbe la pena, tenuto di quanto sopra esposto: si riesce ugualmente a compensare anche in questi casi.
Infine e tenendo conto della sola banda di memoria, invece, mi pare piuttosto innegabile che non ci sia storia per la grafica planare: quella packed risulta di gran lunga più efficiente, e ciò consente di poter muovere più oggetti grafici a schermo, oppure di ottenere più fluidità (ad esempio potendo passare da 30/25 a 60/50 FPS).
Il prossimo articolo parlerà dei videogiochi 3D.