Dopo aver preparato il terreno spiegando con dovizia di particolari il funzionamento del tracciamento delle linee implementato nel Blitter per la grafica planare, adesso tocca esaminarne una soluzione hardware (che comunque consiste in pochissime modifiche a quanto già implementato) per la grafica packed.
Il Blitter – Tracciamento linee “packed” – Soluzione hardware
Come già anticipato, la stessa cosa si può fare tranquillamente con la grafica packed, ma con alcuni accorgimenti particolari, poiché si deve necessariamente tenere conto del fatto che l’indice di un pixel non è costituito da un solo bit, ma corrisponde a un numero di bit pari alla profondità di colore.
Al solito e a fini puramente esplicativi assumiamo sempre di operare con grafica a 8 colori, quindi con profondità di colore pari a 3 bit per pixel. Sempre per semplicità e per mostrare come funzioni il nuovo algoritmo con la grafica packed, assumiamo che l’indice del colore dei pixel della linea sia l’ultimo disponibile, il 7
, perché in binario si traduce come %111
, che aiuta a far capire meglio (perché tutti i bit sono a 1
) in che modo tale valore venga utilizzato (e come si “muova”. Maggiori dettagli sotto).
La prima cosa da cambiare è il valore da inserire nel registro BLTADAT
, che passa da $8000
a $E000
. Infatti se prima si poteva soltanto “accendere” un pixel, impostandone a 1
il relativo bit, questa volta dobbiamo, invece, impostare 3 bit, che in questo caso (indice colore pari a 7
) porta a tale risultato (in quanto $E000
in binario è pari a %1110 0000 0000 0000
, dove %111
è il nostro indice colore). Ovviamente indici colore diversi portano a inizializzare tale registro con valori diversi (cambiano i 3 bit più significativi. Sempre per grafica a 8 colori).
Il secondo cambiamento riguarda, manco a dirlo, SHIFTA
(che ricordo essere i 4 bit più significativi di BLTCON0
: #15..#12), questa volta tenendo conto del fatto che la coordinata x1 dev’essere inizialmente moltiplicata per la profondità di colore per posizionare correttamente il Blitter all’inizio del primo bit dell’indice colore del pixel desiderato.
Quindi se x1 = 21, in SHIFTA
deve finirci (21
* 3
) modulo 16
= 15
. Siamo già rientrati nel caso peggiore, perché l’indice colore va copiato in due word diverse (ma attigue) in memoria. Teniamolo per un momento da parte e poniamo x1 = 20
, perché ci consente di trattare il caso più semplice (ma anche il più comune). Quindi in questo caso in SHIFTA
deve andare scritto (20
* 3
) modulo 16
= 12
.
A questo punto per tracciare il primo pixel (in posizione x1) il Blitter shifta il contenuto di BLTADAT
di 12 posizioni a destra, ottenendo il valore $000E
, che è pari a %0000 0000 0000 1110
in binario. Questo è il valore che andrà combinato con quello presente nella grafica (framebuffer) e infine memorizzato nella stessa word in memoria (la word è stata aggiornata, insomma).
Rimane soltanto da passare al prossimo pixel, che chiaramente avrà coordinate x1 + 1. Com’è possibile intuire dal summenzionato caso peggiore (cioè quando x1 = 21
), SHIFTA
dovrà essere incrementato di 3
, ovvero della profondità di colore. Il che è a dir poco lapalissiano, visto che il primo bit dell’indice colore del pixel successivo si trova… dopo tutti i bit del pixel attuale (e siccome i bit utilizzati per gli indici colore sono 3…)!
Dunque il funzionamento del Blitter in questo caso va leggermente cambiato: anziché incrementare sempre di 1
la coordinata x
attuale (che è parzialmente rappresentata da / memorizzata in SHIFTA
), deve farlo utilizzando la profondità di colore (che va da 1 a 6 per il chipset OCS/ECS, mentre da 1 a 8 per quello AGA). La quale, però, dovrà essere memorizzata da qualche parte. E’ sufficiente sfruttare, allo scopo, il registro BLTBPT
, il quale risulta completamente inutilizzato; ci sono, infatti, ben 32 bit a disposizione (visto che è costituito da due registri a 16 bit: BLTBPTH
e BLTBPTL
), ma sarà sufficiente usarne soltanto 4 (ad esempio i 4 bit meno significativi).
A questo punto basterà aggiungere a SHIFTA
il valore contenuto in questi 4 bit ogni volta che il Blitter dovrà passare al pixel successivo, e il gioco è fatto. I problemi arrivano, però, quando il pixel attuale si ritrova coi bit del suo indice colore spalmati fra la word attuale (puntata dai canali C e D) e quella immediatamente successiva. Questo è il caso peggiore che in precedenza era rappresentato da x1 = 21
, e che giocoforza adesso bisogna considerare.
Per x1 = 21
abbiamo visto che in SHIFTA
debba finirci (21
* 3
) modulo 16
= 15
, il che significa che il primo bit (il più significativo) dell’indice colore finirà nell’ultimo bit (il meno significativo) di una word (16 bit), mentre gli altri due andranno fuori. Infatti con BLTADAT
che è stato impostato a $E000
uno shift (a destra) di 15 significa che il valore risultante sarà $0001
, mentre gli altri due bit (#14 e #13, anch’essi a 1
) saranno stati troncati (per il momento. Vedremo poi dove andranno a finire, in realtà).
Col valore $0001
si può procedere come al solito, combinandolo con la word della sorgente C e scrivendolo sulla destinazione (che è sempre la stessa word puntata da C). Ciò rappresenta, però, soltanto metà del lavoro, visto che soltanto il primo bit dell’indice colore è stato inserito nella grafica (framebuffer). Rimangono gli altri due bit che sono stati “scartati” ed è qui che è necessario implementare il cambiamento più importante nel Blitter per poter gestire questo caso speciale (che si verifica esclusivamente per profondità di colore che non siano potenze del due: con potenze del due si rientra, invece, sempre e soltanto nell’unico caso: quello normale e già tranquillamente gestito).
Prima di tutto bisogna capire quando ci troviamo in questo caso speciale e ciò risulta di facile implementazione, fortunatamente. Infatti quando si deve passare al pixel successivo abbiamo visto che il Blitter incrementa di 3
(nel caso di grafica a 8 colori) SHIFTA
. Quando questa somma genera un riporto (capita quando il valore di SHIFTA
è superiore a 12.
Ricordiamo che SHIFTA
è un valore a 4 bit e, quindi, per il suo aggiornamento si utilizza un sommatore a 4 bit, per cui risultati superiori a 15 genereranno sempre un riporto) sappiamo che dobbiamo passare alla word successiva (e, quindi, serve incrementare di due i puntatori alla sorgente C e alla destinazione D), come abbiamo visto. Ciò rappresenta la precondizione (o condizione necessaria) affinché si possa poi verificare se ci si trovi nel caso speciale o in quello normale.
Il caso normale si verifica quando SHIFTA
diventa 0
(ricordo sempre che viene eseguito il modulo 16
per aggiornarne il valore; quindi rimaniamo sempre nell’intervallo 0
..15
per questa variabile) e, quindi, non serve fare altro: si continua esattamente come si è sempre fatto. In tutti gli altri casi (quindi se SHIFTA
non è 0
) ci ritroviamo nel caso speciale. Per cui il controllo a questo punto diventa banale: basta verificare che il nuovo valore di SHIFTA
sia 0
oppure no (che si calcola con una porta or i cui 4 ingressi siano il nuovo SHIFTA
. 0
indicherà il caso normale, mentre 1
quello speciale), oltre ovviamente ad avere riscontrato che l’aggiornamento di SHIFTA
abbia generato un riporto.
Nello specifico e ricordando di avere x1 = 21
sappiamo che, in questo caso, SHIFTA
sarà pari a 15
e che, per passare al pixel successivo, il nuovo SHIFTA
dovrà essere 15
+ 3
= 18
. Per cui abbiamo un riporto (il valore è maggiore di 15
, che è il massimo che SHIFTA
possa contenere) mentre in SHIFTA
ci finirà 18
modulo 16
= 2
.
Ora bisogna capire come fare a gestire gli altri due bit che dovrebbero finire nella grafica (framebuffer) per completare la scrittura dell’indice del colore. Come possiamo vedere dal diagramma di funzionamento del Blitter, il valore attuale presente in BLTADAT
viene combinato con quello precedente (che è sempre zero. Dopo si capirà meglio), e il tutto viene shiftato a destra di SHIFTA
posizioni dal barrel shifter a 32 bit che è collegato alla sorgente A. I 16 bit più significativi risultanti sono quelli della prima word ($0001
) e vengono già prelevati e utilizzati. Adesso basta prendere gli altri 16 bit di tale risultato, cioè quelli meno significativi (pari a $C000
, che in binario è %1100 0000 0000 0000
. Si vede subito che i due bit più significativi di tale valore siano proprio i due bit mancanti dell’indice colore), i quali normalmente andrebbero a finire nel registro interno (old A
nel diagramma), e utilizzarli, infine, per aggiornare la word successiva. Problema risolto!
Per far funzionare il tutto serve aggiungere un nuovo stato al Blitter: quello relativo al caso speciale. Dunque dopo aver aggiornato la word attuale e aver capito di trovarci nel caso speciale, il Blitter entrerà in una nuova modalità. A questo punto preleverà la word successiva dal canale C (il cui puntatore è stato già aggiornato, come abbiamo visto in precedenza, incrementandolo di 2
= due byte), lo combinerà col contenuto di old A
(come ha fatto in precedenza col nuovo contenuto di A), e infine lo scriverà nella destinazione (il cui puntatore era stato fatto avanzare anch’esso). Infine, pulirà old A
, perché il suo contenuto è già stato utilizzato (e non deve assolutamente esserlo successivamente!).
Sembra più complicato di quel che potrebbe sembrare, ma se si ci ferma a rifletterci sopra si capisce che il caso speciale è veramente semplice da implementare e con poche risorse (c’è poca logica necessaria per orchestrare il tutto). Anche qui, senza un’implementazione concreta non si può vedere quanti transistor sarebbero necessari, ma la personalissima idea che mi sono fatto è che ne servirebbero pochissimi.
Il Blitter – Tracciamento linee “packed” – Prestazioni
In chiusura è d’obbligo dare un’occhiata ai numeri, per comprendere il motivo per cui è preferibile spendere su qualche transistor in più pur di implementare in hardware questa primitiva anche per la grafica packed.
Abbiamo visto come il limite teorico per la grafica planare sia rappresentato da 895mila (NTSC) e 886mila (PAL) pixel al secondo disegnati per schermi (framebuffer) monocromatici (2 soli colori). Che, però, diminuiscono proporzionalmente all’aumentare della profondità di colore: 149/148mila al secondo per OCS/ECS con schermi a 64 colori = 6 bit per pixel e 112/111mila per AGA con 256 colori = 8 bit per pixel.
Ebbene, il suddetto limite teorico rimane lo stesso con la grafica packed per qualunque profondità di colore che sia una potenza del due (1, 2, 4, e 8 bit = 2, 4, 16 e 256 colori): 895/886mila pixel al secondo. Ciò perché, in questi casi, non si verifica mai il caso speciale, ma esclusivamente quello normale e, dunque, per ogni pixel tracciato viene sempre e soltanto letta e scritta una word dalla/alla grafica (framebuffer). Il confronto con la grafica planare diventa a dir poco impietoso…
Ovviamente per profondità di colore che non siano potenze del due si deve tenere conto del caso speciale, che si verifica quando l’indice colore è suddiviso fra una word e quella successiva.
Il caso migliore è rappresentato dal tracciamento di linee orizzontali, dove il caso speciale si verificherà più raramente. Sappiamo che con la grafica a 8 colori, ad esempio, una word (16 bit) contiene ben 5 pixel più un bit per l’indice colore di un sesto pixel. Questo significa che, rozzamente (approssimo, ma soltanto per semplificare il discorso, che rimane comunque in piedi a livello di validità), ogni 6 pixel ce ne saranno 5 che saranno disegnati nel caso normale e soltanto uno ricadrà in quello speciale. Quindi nell’83% casi il Blitter lavorerà al massimo delle prestazioni (quello teorico) mentre nel rimanente 17% dei casi sarà costretto a eseguire il doppio passaggio del caso speciale.
Il caso peggiore è, invece, rappresentato dal tracciamento di linee verticali i cui pixel abbiano gli indici colore sempre suddivisi in due word: qui il Blitter opererà sempre il doppio passaggio per ogni singolo pixel disegnato.
Se volessimo tenere in considerazione esclusivamente il caso peggiore per la grafica packed, il limite teorico nel caso di profondità di colore che non fossero potenze del due si potrebbe considerare dimezzato: 448/443mila pixel al secondo, che comunque sarebbe di gran lunga superiore all’equivalente per la grafica planare.
C’è da dire che in questo modo stiamo assumendo che il Blitter impiegherebbe 16 cicli di clock di sistema (8 per la prima word e altri 8 per quella successiva) per tracciare un pixel nel caso speciale.
In realtà il nuovo stato del Blitter che è stato introdotto per gestire il caso speciale potrebbe essere implementato in maniera molto più efficiente, impiegando soltanto 4 cicli di clock di sistema per la seconda word anziché i classici 8. Infatti alla fine della scrittura della prima word egli ha già effettuato tutti gli aggiornamenti del suo stato (che non deve ripetere nuovamente quando deve elaborare la seconda word) e in più ha già a disposizione il valore da utilizzare per la seconda word. Basta, a questo punto, soltanto leggere la seconda word della sorgente C (operazione che richiede uno slot / 2 cicli di clock di sistema), combinarla con quanto già elaborato, e scrivere il risultato nella destinazione (un altro slot / 2 cicli di clock di sistema).
Ciò porterebbe il caso peggiore a utilizzare 12 cicli di clock di sistema, portando il limite teorico a ben 597/591mila pixel al secondo. Direi che la convenienza dell’implementazione hardware sia innegabile ed estremamente vantaggiosa per la grafica packed.
E con questo è veramente tutto: l’argomento Amiga “packed” è stato sviscerato in ogni suo singolo dettaglio implementativo e trattando tutte le primitive grafiche proponendone delle versioni in hardware.
Soltanto quella di riempimento delle aree ha trovato poco spazio perché ho deciso di non trattare un’eventuale soluzione interamente hardware, in quanto oggettivamente troppo complicata e richiedente troppe risorse per l’epoca. Quella “ibrida” proposta rappresenta, a mio modesto avviso, un buon compromesso.
I prossimi, pochi, pezzi avranno un taglio meno tecnico (chiedo scusa ai lettori, perché mi rendo conto che hanno avuto a che fare con una valanga di contenuti estremamente tecnici e difficili da digerire, ma l’argomento era di per sé complicato) e tratteranno alcuni casi d’uso reali (s.o. & applicazioni dotate di GUI, giochi 2D, giochi 3D), facendo un confronto su cosa avvenga con grafica planare e packed, chiudendo poi il tutto con le mie considerazioni finali.