Il Blitter è stato il coprocessore delle meraviglie per chi ha avuto il piacere di programmarlo, grazie alla notevole flessibilità & programmabilità che offrì all’epoca (ricordo che parliamo dell’85!), il quale non si limitava soltanto alle primitive grafiche, seppur potenti (specialmente quella di cookie-cut!, di cui abbiamo completato l’analisi nel precedente pezzo), che abbiamo visto nei precedenti articoli. Infatti consentiva anche di disegnare linee e riempire aree… sempre in hardware!
Il Blitter – Tracciamento linee & riempimento “packed” – Soluzioni a costo zero (in hardware)
Ovviamente il problema di come implementare il tutto in versione packed rimane il costo, perché tenere conto della profondità del colore dei pixel ha un peso non indifferente, come abbiamo visto con la versione packed dell’utilissima primitiva cookie-cut. Questi sono casi in cui la semplicità di dover operare su un solo bitplane, come sui nostri amati Amiga, paga a livello di complessità e/o risorse impiegate per l’implementazione dello specifico algoritmo.
Poiché tali primitive grafiche risultano già implementate per il caso planare / singolo bitplane, la prima idea che m’è venuta in mente è di riutilizzarle in qualche modo per la grafica packed. Per far ciò risulta, purtroppo, necessario l’uso di un buffer temporaneo, costituito da un singolo bitplane, da utilizzare per tracciare le linee o per per eseguire l’operazione di riempimento di aree, così da avere lì il risultato che sarà poi necessario “traslare” / “rimappare” nella grafica packed, dove i singoli bit che vengono generati rappresentano dei “segnaposto” (ossia marcano quali pixel dovranno essere poi elaborati).
Dunque se la linea o l’area di riempimento interessata risiede nell’area rettangolare di coordinate (x1, y1), (x2, y2), si dovrà scansionare quest’area in cerca dei bit che sono stati impostati (i segnaposto di cui sopra) per andare poi a sostituire l’indice colore nella grafica packed per i pixel che si trovino in quella posizione (la stessa dei segnaposto). In pseudocodice:
for y in y1..y2:
for x in x1..x2:
if buffer[x, y]:
framebuffer[x, y] = color
Il problema rimane come implementare tutto ciò in maniera efficiente, cioè utilizzando il Blitter che, come sappiamo, è molto veloce nell’eseguire operazioni su aree rettangolari, ma… su singoli bitplane!
In soccorso arriva la già citata primitiva grafica cookie-cut, che è stata dettagliatamente analizzata e spiegata nel precedente articolo. Infatti è sufficiente impostare il canale A del Blitter per puntare al suddetto buffer temporaneo (quindi i bit / segnaposto diventano a tutti gli effetti i bit della maschera da applicare), i canali C e D che puntano alla grafica (framebuffer) da cambiare, e il canale B… a un altro buffer addizionale costituito da una sola riga packed (di lunghezza pari alla larghezza dell’area rettangolare in cui lavoriamo) in cui tutti i pixel sono impostati al colore da utilizzare per rimpiazzare quello indicato dai segnaposto.
Per dirla in un altro modo e cercare di chiarire ancora meglio il concetto, è come se il buffer addizionale fosse una sorta di texture monodimensionale (è costituito da una sola riga di pixel) nonché monocolore (perché tutti i suoi pixel hanno lo stesso colore) che viene selettivamente applicata alla grafica packed (il framebuffer), usando i bit del buffer elaborato in precedenza (per tracciare la linea o riempire l’area) come maschere per decidere quando applicare il colore della texture (ossia il bit del buffer = maschera è impostato a 1
) oppure (quando tale bit è a 0
) lasciare intatto il colore della grafica (framebuffer).
Ovviamente nulla impedisce di utilizzare una texture bidimensionale e con diversi colori (che ha molto più senso per la primitiva di riempimento delle aree), ma l’esempio di cui sopra serviva soltanto per illustrare in che modo fosse possibile realizzare il riempimento di aree solide per grafica packed.
Ricapitolando, se non vogliamo implementare totalmente in hardware le operazioni di tracciamento delle linee e/o riempimento di aree per la grafica packed, si possono benissimo riciclare tout court le medesime primitive che sono già implementate nel Blitter (ma per la grafica planare = per un singolo bitplane), dovendo, però, pagare il costo per:
- un buffer (singolo bitplane) separato in cui eseguire la determinata primitiva grafica per ottenere la maschera da utilizzare successivamente per applicare il cambio colore;
- un buffer (packed) per la texture da utilizzare per il colore (o i colori) da applicare;
- l’elaborazione (tracciamento linea o riempimento dell’area) del buffer / maschera;
- l’elaborazione (cookie-cut) della grafica packed (framebuffer).
Se tutto fosse implementato in hardware il costo effettivo sarebbe esclusivamente quello dell’ultimo punto (in quanto è lì che avverrebbe il tutto, in una singola passata), ma in questo caso bisogna tenere conto dei costi aggiuntivi relativi ai primi tre punti (i primi due in termini di memoria occupata, mentre il terzo in termini di pura elaborazione del Blitter). Ovviamente quanto tali costi possano incidere dipende strettamente dagli specifici scenari.
Il Blitter – Tracciamento linee “packed” – Soluzione software
Il discorso sembra filare liscio, ma magari a qualcuno avrà suonato un campanello d’allarme perché sente puzza di bruciato in tutto questo bel ragionamento. E in effetti il problema non attiene tanto alla primitiva grafica di riempimento delle aree (perché in questo caso si opera sempre con regioni rettangolari, che è esattamente quello che è stato illustrato con l’algoritmo diciamo “ibrido” planare / packed), quanto a quella di tracciamento delle linee.
Infatti per tracciare linee non v’è certo bisogno di andare a processare tutta l’area rettangolare interessata (perché ricordo che l’algoritmo di cui sopra prevede, sì, il tracciamento della linea nel buffer, ma poi si deve processare tutto il buffer andando a caccia dei bit impostati a 1), in quanto si procede soltanto… per i singoli pixel interessati!
Per fare un esempio pratico, se ho da tracciare la diagonale di uno schermo da 320 x 200 pixel, non è che vado a processare tutti e 320 x 200 = 64000 pixel, ma basta elaborare soltanto quelli indicati dal buon Pitagora: sqrt(3202 x 2002) = 377 pixel. Più di due ordini di grandezza di differenza!
Quindi è chiaro che non si possa assolutamente pensare di utilizzare quell’algoritmo ibrido per il tracciamento di linee con la grafica packed: molto meglio, a questo punto, sarebbe sfruttare direttamente il processore, che con la grafica packed si trova certamente molto più a suo agio rispetto a quella planare.
Il Blitter – Tracciamento linee – Limiti della grafica planare
Fortunatamente un’implementazione hardware è possibile anche con la grafica packed, utilizzando, tra l’altro, soltanto pochissime risorse in più. Il che sarebbe ampiamente desiderabile soprattutto se si guardasse al rapporto costi / benefici, in quanto a fronte di risibili risorse aggiuntive si otterrebbero prestazioni di gran lunga superiori.
C’è da dire, infatti, che l’implementazione hardware del Blitter funziona benissimo ed è molto efficiente quando si ha a che fare con grafica monocromatica (2 colori = singolo bitplane), ma risulta via via molto, molto più inefficiente quando la profondità di colore aumenti.
Infatti e come possiamo leggere dall’onnipresente (e onnisciente) Hardware Manual, il Blitter necessita sempre di 4 slot / color clock, pari a 8 cicli di clock di sistema per ogni singolo pixel che disegna (“When in line mode , each pixel takes eight ticks“). Il che si traduce in 895mila e 886mila pixel al secondo, rispettivamente per gli Amiga NTSC e PAL (che hanno clock di sistema pari a 7.16 e 7.09Mhz). Ciò supponendo che il Blitter possa idealmente (ma mai, realisticamente) avere a disposizione tutti gli accessi alla memoria (quando gli servono).
Tutto ciò vale, comunque, soltanto per la grafica monocromatica, poiché all’aumentare del numero di colori utilizzati questi valori dovranno necessariamente essere divisi per la profondità di colore. Quindi con grafica a 64 colori (profondità di colore pari a 6 bit, che è il massimo per Amiga OCS/ECS) quei due valori diventano, rispettivamente, 149mila e 148mila pixel al secondo. Che con AGA e grafica a 256 colori diventano, a loro volta, 112mila e 111mila.
Valori di tutto rispetto, considerato che è pur sempre il Blitter a prendersene carico, il quale funziona certamente molto più velocemente della CPU (ricordiamo che i primi Amiga OCS/ECS erano dotati soltanto di un processore 68000 a 7.16 o 7.09Mhz, che non brilla per prestazioni né, soprattutto, in efficienza per gli accessi in memoria), ma di gran lunga inferiori a quanto si potrebbe ottenere con grafica packed (ed è il motivo per cui varrebbe la pena esplorarne un’implementazione hardware).
Il Blitter – Tracciamento linee – Funzionamento della versione planare
Bisogna, però, comprendere prima come sia stato implementato nel Blitter il tracciamento delle linee, prima di passare alla descrizione dei cambiamenti necessari per ottenerne una versione packed. Fortunatamente dalla descrizione fornita dal suddetto manuale (oltre che dalla descrizione / riepilogo su come devono essere impostati i suoi registri) è possibile capire che non si tratti di nient’altro che del classico algoritmo di Bresenham, con qualche piccola modifica (alcune costanti sono moltiplicate per due, ad esempio, e la gestione degli ottetti è interna a questo coprocessore).
Sinteticamente e supponendo che i registri del Blitter siano stati impostati come da manuale, il funzionamento è relativamente semplice:
- si parte dalle coordinate iniziali (x1,y1), assumendo quindi che inizialmente
x
= x1 ey
= y1, disegnando il pixel a quella posizione; - si aggiorna la coordinata
x
incrementandola di1
(quest’operazione è implicita nel ciclo for, ovviamente); - se necessario (controllo sulla variabile
D
, utilizzata per “capire in che direzione muoversi”, se mi fate passare la semplificazione), incrementa di1
la coordinatay
e aggiorna opportunamente la variabileD
; - si aggiorna opportunamente la variabile
D
.
Tralasciando l’aggiornamento delle variabili y
e D
dei relativi registri del Blitter (perché questa parte è esattamente identica, sia per la grafica planare sia per quella packed), ciò che è interessante sarebbe capire in che modo questo coprocessore disegni effettivamente il pixel e si sposti poi alla coordinata x
successiva.
La chiave di tutto sta nel valore $8000
impostato nel registro BLTADAT
e nei bit 15-12 del registro BLTCON0
(che risultano impostati al valore di x1 modulo 16
. Nel manuale c’è un errore in quanto è riportato modulo 15
; molto probabilmente perché il codice d’esempio riporta x1 con and binario 15
, che produce lo stesso risultato, ma è molto più veloce), che per comodità chiamerò SHIFTA
. Specificamente, $8000
significa che il bit più significativo (cioè il primo bit nella grafica, visto che parliamo di un sistema big-endian) è posto a 1
, mentre tutti gli altri sono a 0
.
Ogni volta che deve disegnare un pixel, il Blitter prende il contenuto di BLTADAT
(che è sempre, fisso, a $8000
) e lo shifta a destra di SHIFTA
, in modo da muovere l’unico bit a 1
esattamente dov’è posizionato il bit relativo al pixel di coordinate x1 (inizialmente. Poi ovviamente cambierà, incrementando SHIFTA
di 1
ogni volta, in modo posizionarsi alla coordinata x
del pixel successivo).
Quindi, e per fare un esempio, se x1 vale 21
, SHIFTA
sarà impostato a 21
modulo 16
= 5
. Di conseguenza per il primo pixel verrà calcolato $8000
shiftato a destra di 5
= $0400
, che in binario fa %0000 0100 0000 0000
. Il Blitter prenderà poi questo valore e lo combinerà (in genere con un or binario) coi 16 bit che nel frattempo ha letto dallo schermo (framebuffer) alla posizione (x1, y1), e infine conserverà tale risultato nella medesima posizione dello schermo.
Per passare al pixel successivo (x1 + 1, x1 + 2, e così via) gli basterà semplicemente incrementare SHIFTA
di 1. Se, però, SHIFTA
era 15
allora sorge un problema, in quanto il Blitter lavora sempre in termini di word = 16 bit alla volta, per cui sarà necessario passare alla word successiva. Quindi in questo caso speciale i puntatori dei canali C e D verranno incrementati di due (due byte = una word a 16 bit), mentre SHIFTA
verrà impostato a 0
(ossia il primo pixel della nuova word: quella successiva).
Penso sia tutto abbastanza semplice da capire. In caso di dubbi la descrizione dell’algoritmo di Bresenham può fornire dei chiarimenti. Comunque il punto chiave è che il Blitter si sposta sempre di un pixel alla volta orizzontalmente, incrementando il valore di SHIFTA
, ed eventualmente passando alla word successiva. Il tutto in maniera esemplificativa, ovviamente (perché ci sarebbe da tenere in considerazione gli ottanti, ma non è rilevante ai fini della discussione generale e delle problematiche trattate).
Nel prossimo articolo proporrò un’implementazione hardware per la versione packed, mostrandone il funzionamento (comunque di poco differente dall’attuale per la grafica planare) e gli enormi vantaggi in termini prestazionali.