Il Blitter
Dopo l’articolo sugli sprite rimane adesso l’ultimo elemento da trattare nonché l’altro componente (assieme al controllore video) estremamente importante nel chipset dell’Amiga: il Blitter.
Una cosa da precisare immediatamente è che il Blitter non nasce per operare con (ed esclusivamente) coi bitplane (il che può sembrare strano), in quanto si tratta di un componente che, in realtà, opera sempre a livello di word (16-bit): legge delle word (usando fino a 3 sorgenti diverse, chiamate A, B e C), effettua delle operazioni su di esse (magari combinando in qualche modo i valori letti), e infine scrive la word risultante (sull’unica destinazione, chiamata D). Il funzionamento risulta ben descritto sempre nell’onnipresente Hardware Manual (in particolare nell’introduzione), per chi volesse approfondire l’argomento.
Ovviamente e per com’è concepito si sposa benissimo col concetto di (singolo) bitplane e, poiché l’Amiga usa diversi bitplane dov’è memorizzata la grafica degli schermi, è sufficiente (nonché necessario!) utilizzarlo più volte per ottemperare allo scopo di aggiornare / manipolare tutta la grafica. Quindi il Blitter sarà programmato una volta per ogni singolo bitplane da elaborare.
Se, ad esempio, uno schermo ha 32 colori (cioè la profondità di colore è 5 bit), allora per manipolarne la grafica sarà necessario ripetere la stessa operazione 5 volte e il Blitter dovrà essere programmato lo stesso numero di volte, dandogli in pasto ogni volta i bitplane giusti da elaborare. Questo è un discorso generale che, per ragioni di semplicità, non tiene conto di casi particolari (vedi le ombre di Fightin’ Spirit, di cui ho già parlato nell’articolo teorico).
Al di là di questi che sono meri dettagli (seppur interessanti per far capire come funzioni questo gioiellino), ciò che è realmente importante è comprendere se e come questo coprocessore fosse in grado di aiutare il sistema (quindi scaricando la CPU da questa responsabilità) nell’elaborazione delle primitive grafiche che sono necessarie per i vari compiti (alla GUI per disegnare i controlli grafici a schermo, muovere finestre, etc.; ai programmi di grafica per l’elaborazione delle immagini; ai giochi per spostare grafica e disegnare i BOB = sprite realizzati col Blitter).
Le primitive più importanti sono state già riportate nonché analizzate nel summenzionato articolo (che invito a rileggere, per eventuali dubbi), il quale, però, si è soffermato più sull’aspetto teorico e sull’efficienza che su concrete implementazioni (d’altra parte non era nemmeno quello il suo scopo, sebbene anche in merito abbia fornito alcune indicazioni).
Sembrerà assurdo, ma l’impianto base del Blitter funziona bene anche con la grafica packed, proprio perché è un’unità che è stata concepita per operare su intere word (16-bit), come già detto nonché ampiamente chiarito nel manuale di Commodore. Pertanto alcune primitive grafiche per la versione packed funzioneranno senza modifiche, mentre per altre cose saranno necessari alcuni cambiamenti o non funzioneranno (se non con pesanti aggiunte e complicazioni) o non avranno alcun senso, ma in ogni caso l’implementazione usata nell’Amiga può essere già presa così com’è quale base su cui apportare le necessarie modifiche per la grafica packed.
Per essere ancora più preciso, quest’articolo non si prefigge di fornire un’implementazione concreta di quanto riportato in quello teorico (ma questa volta in “salsa Amiga”), quanto di far vedere come funzionerebbe un Amiga in cui l’unico elemento cambiato fosse il formato grafico con quello packed al posto di quello planare. Dunque tutti gli altri elementi dell’Amiga sono esattamente gli stessi (registri e bit inclusi), mentre per quelli coinvolti è necessario mostrare i cambianti necessari, come funzionerebbero, e i relativi costi. Controllore video e sprite sono già stati analizzati, ma rimane il Blitter, in cui di seguito verranno mostrati alcuni esempi concreti che introducono il suo funzionamento interno e cosa succede passando alla versione packed.
Il Blitter – Copiare una porzione di memoria
Quella di copiare una porzione di memoria è la primitiva più semplice da realizzare nonché la più comune e utilizzata, per cui non suscita stupore sapere che sia anche quella che risulta implementata praticamente in ogni Blitter che sia stato creato (quindi anche per altri sistemi), perché serve a copiare e poi ripristinare zone di memoria che sono soggette a sovrascrittura (pensate a uno sfondo in cui viene disegnato qualcosa che poi dovrà essere ripristinato).
Senza scendere troppo nei dettagli, al Blitter dell’Amiga è sufficiente che sia indicata la sorgente (un puntatore ai dati) dalla quale leggere le word, la destinazione (altro puntatore) in cui scriverle, quante word vadano trasferite per ogni riga (il Blitter opera sempre per aree rettangolari, quindi bisogna definire “larghezza” e “altezza” per esse), quante righe devono essere trasferite, e infine per sorgente e destinazione bisogna che sia impostato anche il modulo (valore da aggiungere a tutti i canali utilizzati alla fine dell’elaborazione di ogni riga per posizionarli correttamente alla riga successiva. Esattamente come funziona il controllore video, insomma). A questo punto il Blitter sarà pronto e potrà procedere all’elaborazione (la scrittura dell’ampiezza dell’area rettangolare in un apposito suo registro fa scattare immediatamente l’attività).
Tutto molto semplice e anche estremamente efficiente. Talmente semplice che non serve alcuna modifica al Blitter per supportare spostamenti di aree rettangoli packed: è sufficiente impostarlo esattamente allo stesso modo e il gioco è fatto.
La differenza, però, è che con grafica packed non serve ripetere l’operazione tante volte quanti siano i bitplane usati per la grafica (tranne nei casi in cui la grafica dei bitplane è interleaved, come spiegato nel pezzo teorico), e quindi si risparmia di dover impostare i registri del Blitter tutte le volte. Questo vale in generale per qualunque operazione col Blitter con grafica packed: serve programmarlo una sola volta, a prescindere dalla profondità di colore.
Altra cosa e come già accennato in precedenza, il Blitter consente di effettuare operazioni logiche (dettagli più precisi in questa parte del citato manuale) sui bit dei dati letti dai canali sorgenti. In questo caso c’è un solo canale sorgente per cui l’unica cosa sensata e fattibile sarebbe eventualmente quella di negare tali bit e, quindi, trasferire nella destinazione le word lette dal canale sorgente, ma tutte coi bit negati. Anche con grafica packed ciò può essere utile (si otterrebbe una sorta di “complemento” dei colori).
C’è, però, un importante chiarimento da fare: tutto ciò funziona soltanto per porzioni di schermo che siano allineate a 16-bit e l’area rettangolare coinvolgerà grafica (dati, in generale) che siano multipli di 16-bit. D’altra parte abbiamo detto che stiamo spostando word, e ciò risulta ovvio. Ma non lo è quando parliamo di grafica e pixel, perché in questo caso i rettangoli da spostare potrebbero essere arbitrari (anche di un solo pixel).
Qui salta fuori un’altra sostanziale (questa volta) differenza con l’Amiga: spostare zone di memoria rettangolari con grafica packed non obbliga ogni volta a postare 16 pixel alla volta (che è ciò che avviene con l’Amiga, per l’appunto), ma tutto dipende dalla profondità di colore utilizzata.
Infatti abbiamo visto col controllore video che una word (16-bit) può contenere 5 pixel (più una frazione del sesto) in uno schermo con 8 colori (3 bit per pixel) e, dunque, sarà rozzamente questo il limite: sarà obbligatorio trasferire aree rettangoli con ampiezza di 5 pixel e poco più (una frazione: un terzo di pixel). Che è un gran guadagno, se ci si pensa bene (molto meglio che essere obbligati a trasferire 16 pixel alla volta), e questo si potrà vedere meglio con applicazioni reali (i giochi, in particolare).
In generale, più aumenta la profondità di colore e meno pixel sarà obbligatorio trasferire a ogni copia di word, quindi più efficienti saranno i trasferimenti. Anche questo sembra paradossale, perché nell’immaginario collettivo è sempre meglio poter spostare più pixel possibili per “andare più veloci” (essere più efficienti), ma con la grafica planare per assurdo diviene meno efficienti all’aumentare del numero di bitplane da processare. Come già detto, si vedrà meglio con alcuni esempi reali (in un prossimo articolo).
Il Blitter – Copiare una porzione rettangolare (in una posizione diversa)
Quella di estrarre una porzione rettangolare e copiarla in un’altra regione, ma in un’altra posizione, è un’altra primitiva molto semplice, ben descritta sempre nel manuale (in questa parte).
In questo caso se vogliamo spostare l’inizio del rettangolo sorgente a un pixel di posizione (orizzontale) diversa dobbiamo far uso di uno dei due barrel shifter (a 16 bit, e in grado di eseguire spostamenti a destra da 0 a 15 posizioni di tutti i bit) di cui il Blitter è dotato per i canali sorgente A e B, e grazie ai quali è possibile definire in maniera precisa da dove iniziare a copiare i pixel (bit, nel caso della grafica planare) della sorgente.
Oltre a ciò serve anche mascherare i bit non utilizzati in modo che non finiscano copiati nella destinazione. Per far questo il Blitter mette a disposizione due registri “maschera” per la prima e l’ultima word lette per ogni riga da elaborare, ma queste maschere sono disponibili esclusivamente per il canale A.
Ancora una volta basta un solo canale sorgente (A, perché servono le due maschere) allo scopo, che punta al rettangolo da copiare, ed è necessario impostare la dimensione di tale rettangolo nell’apposito registro, oltre ovviamente a inizializzare opportunamente le due maschere a seconda della dimensione del rettangolo e della posizione in cui dovranno finire i pixel, e infine impostando anche i moduli dei due canali (in modo da posizionarsi correttamente alla prossima riga, ogni volta che finisca l’operazione per una certa riga).
Dunque, quando l’operazione si avvia il Blitter procede a leggere la prima word di ogni riga, applica lo shift impostato per questo canale (per spostare il primo pixel della sorgente nella posizione orizzontale richiesta per la destinazione), e conserva il contenuto nella destinazione. Poi prosegue con eventuali altre word da copiare, fino alla penultima. Da notare che, a ogni passaggio, utilizza i bit shiftati via a destra dalla word precedente, inserendoli all’inizio della word successiva (e a cui ha applicato lo stesso shift, ovviamente); in questo modo tratta tutti i bit delle word come una catena continua e ininterrotta di bit (anche questo si vede dal diagramma interno del Blitter).
Il tutto fino all’ultima word processata e per la quale applica l’apposita maschera per togliere di mezzo eventuali bit non desiderati. Ad esempio e se il rettangolo sorgente è costituito da 30 pixel (per cui vengono lette 2 word che contengono 32 bit = 32 pixel), allora la prima maschera sarà impostata sempre a $FFFF
perché tutti i bit devono essere copiati. L’ultima, invece, dovrà essere $FFFC
, perché gli ultimi due bit devono essere scartati.
E così via per ogni riga (avendo cura di aggiungere il modulo ai puntatori dei canali A e D alla fine di ogni riga elaborata, in modo da posizionarli alla prossima).
Per la versione packed tutto funziona esattamente così com’è, ma bisogna tenere conto che i pixel possono utilizzare più bit, a seconda della profondità di colore. Questo, però, non interessa al Blitter, in quanto non ne è nemmeno a conoscenza (lavora sempre con word = 16 bit): che i singoli bit delle word rappresentino ognuno i singoli pixel di un bitplane o che a 3 a 3 siano, invece, i bit usati per l’indice dei colori di uno schermo a 8 colori, non ha nessunissima importanza!
Per prendere l’esempio di cui sopra, se il rettangolo da copiare è costituito da 30 pixel, ma per uno schermo a 8 colori, per il Blitter vuol dire che per ogni riga da elaborare dovrà leggere 30 * 3 = 90 bit = 6 word a 16-bit. Inoltre se il primo pixel di tale rettangolo dovrà essere copiato a partire dal quarto pixel orizzontalmente, allora lo shift da impostare per il canale A (la sorgente) dovrà essere pari a 4 * 3 = 12. Infine la maschera per l’ultima word di tale canale dovrà essere impostata a $FFC0
(la prima, come detto prima, dev’essere sempre $FFFF
perché c’interessano tutti i bit), in quanto gli ultimi 6 bit letti da tale canale dovranno essere scartati.
L’Hardware Manual riporta, subito a seguire, una nota in cui mostra che lo stesso meccanismo può essere usato per estrarre la grafica di un singolo carattere di un font, facendo attenzione questa volta a usare la maschera per la prima word, in modo da scartare i pixel appartenenti al carattere precedente a quello che si voglia copiare. Inutile dire che la stessa cosa vale esattamente allo stesso modo per la versione packed, ma sempre tenendo conto della profondità di colore, come già visto nell’esempio qui sopra.
Il Blitter – Spostare una porzione rettangolare
Un caso più generale del precedente è quello di voler spostare una qualunque porzione rettangolare di pixel (anche soltanto un singolo pixel, come caso limite) all’interno di un’altra area rettangolare (che, però, sia in grado di contenerla, altrimenti si genererebbero artefatti grafici), preservando, in quest’ultima, tutta la grafica all’infuori dell’area interessata.
Pensate, ad esempio, a quando si sposta una finestra all’interno di uno schermo: il rettangolo rappresentato dalla finestra si deve incastrare nello schermo alla posizione desiderata, ma tutta la grafica dello schermo attorno alla finestra deve rimanere così com’è. Questo è l’esempio che inquadra perfettamente questa primitiva grafica ed è anche l’ultimo che il manuale riporta con dovizia di particolari (nel link che ho fornito in precedenza).
Per poter combinare opportunatamente la grafica della finestra con quella dello schermo sarà necessario utilizzare un altro canale (C, nell’esempio) per leggere il contenuto dello schermo dall’area interessata.
Per i dati della sorgente sarà necessario utilizzare il canale B, impostandone lo shift opportunamente a seconda della posizione (orizzontale) in cui si voglia inserire il suo primo pixel nello schermo. Nell’esempio del manuale la posizione è 5, e quindi lo shift assumerà questo valore.
Infine, il canale A non userà alcuno shift (non serve qui), ma dovrà avere preimpostato il suo registro dati (quello che viene caricato coi dati prelevati dal DMA, mano mano che le word vengono lette, quando è abilitato per questo canale) con tutti i bit a 1 ($FFFF
), mentre le maschere per la word iniziale e finale vanno impostate opportunamente tenendo conto di eventuali pixel da scartare all’inizio del rettangolo (della finestra) e alla sua fine; specificamente, si dovrà tenere conto dello shift impostato nel canale B (anche se per il canale A non è usato) per la maschera iniziale, e dello stesso shift più i pixel da scartare per quella finale. Infine, sarà necessario tenere spento il DMA per questo canale (avendone impostato manualmente i dati).
Sembra tutto complicato e macchinoso, ma andando a guardare l’esempio nel manuale e seguendo passo passo le operazioni effettuate dal Blitter, si può verificare come tutto fili liscio come l’olio.
Anche in questo caso la versione packed è sostanzialmente identica a quella planare, ma bisogna tener conto della profondità di colore. Per cui, sempre seguendo l’esempio fornito nel manuale, lo shift da considerare per B dovrà essere di 5 * 3 = 15 (considerando sempre la grafica a 8 colori = 3 bitplane). Mentre la maschera di A per la prima word dovrà essere impostata a $0001
(si saltano i primi 5 pixel e si lascia in piedi soltanto l’unico bit rimasto del sesto).
Un po’ più complicato è invece, il calcolo della maschera necessaria per l’ultima word, che dovrà essere invece impostata a $F000
. Ciò perché si deve tenere conto dello shift di 15 e del fatto che il rettangolo di 23 x 3 pixel (3 sono le righe) richiede il trasferimento di 23 * 3 (3 è la profondità di colore) = 69 bit e, quindi, di 5 word a 16 bit per ogni singola riga processata. Questo vuol dire che l’ultimo bit da trasferire dalla finestra sarà il 15 + 69 = 84°. Poiché devono essere processate 6 word, pari a 6 * 16 = 96 bit, vuol dire che gli ultimi 96 – 84 = 12 bit non dovranno essere copiati. Da cui il valore $F000
(si copiano i primi 4 bit e si tralasciano gli ultimi 12).
Una cosa molto importante da evidenziare è che, per questo tipo di operazione, è stato necessario far ricorso alle funzioni logiche definibili per i canali sorgenti, come messo a disposizione dal Blitter, chiamati minterm, che consentono di specificare in maniera precisa in che modo usare i bit dei tre canali sorgente per ottenere il bit finale che andrà a finire nella destinazione.
Questo tipo di logica va benissimo quando si manipolano singoli bit e, quindi, il contenuto dei bitplane (perché sono “piani bit”, per l’appunto), ma non ha molto senso parlando di grafica packed, in quanto in questo caso i bit hanno un significato diverso, essendo raggruppati per indicare l’indice del colore associato a un pixel.
Soltanto per questa primitiva grafica tale significato è stato completamento ignorato, in quanto non serviva allo scopo. Infatti l’obiettivo era e rimane quello di inserire la grafica di una finestra a un’arbitraria posizione dello schermo, cosa che riesce benissimo anche se la grafica è packed, considerandola semplicemente un’area rettangolare di bit, a patto di seguire gli opportuni accorgimenti sopra mostrati.
Alla fine, come già detto, al Blitter non importa il significato dei dati che gli vengono passati: esso si occupa di leggere word dai canali sorgente, combinarle come gli viene comandato, e scrivere le word risultanti nel canale destinazione. Non gli interessa altro, e va benissimo così (per ora).
Il prossimo articolo si occuperà di una delle primitive grafiche per cui il Blitter è famoso: quella cosiddetta di cookie-cut.