Dopo aver introdotto l’argomento Amiga “versione packed“, illustrando anche il funzionamento del controllore video dell’originale, riprendiamo parlando finalmente dell’innovazione, ossia del medesimo componente ma questa volta che funzioni esclusivamente con grafica packed. Al solito, il testo è stato suddiviso in sezioni per facilitarne la fruizione.
Il controllore video – packed – Introduzione e arbitraggio degli slot
Adesso sarebbe interessante tornare al tema dell’articolo e vedere come avrebbe funzionato un Amiga dove al posto dei bitplane fosse stata utilizzata la grafica packed, ma è già intuibile che sarebbe stato molto diverso, visto che lo sono anche i due formati grafici (combaciano soltanto per schermi con due soli colori, ossia un solo bitplane).
Supponiamo, per pura semplicità, di dover visualizzare uno schermo con 8 colori (quindi 3 bit per ogni pixel), ma il discorso vale per qualunque profondità colore (da 1 a 8 bit, quindi da 2 a 256 colori). Ho scelto 8 colori / 3 bit e non 4 / 2 bit per riallacciarmi a tutte le analisi già effettuate nel precedente articolo, per cui valgono e possono esserne riutilizzati anche gli esempi. 4 colori / 2 bit avrebbe ulteriormente semplificato la trattazione, ma essendo una potenza del due e non soggetta a problematiche di disallineamento per gli accessi alla memoria (ma soltanto per i casi più semplici: per quelli più complicati presenta gli stessi, identici, inconvenienti!) avrebbe lasciato la porta a facili critiche da parte di chi non abbia ancora capito che questi casi particolari non hanno, in realtà, assolutamente nulla di realmente speciale / diverso dagli altri.
Il controllore video – packed (OCS/ECS) – Prelievo dati e calcolo indice colore
Abbiamo visto che, qualunque sia il numero di bitplane utilizzati, per visualizzare uno schermo a bassa risoluzione (320 pixel orizzontali) il controllore video dell’Amiga normalmente richiede di iniziare il caricamento dei dati dai bitplane a partire dallo slot $38, completa quest’operazione allo slot $40, usa i successivi 5 slot per elaborazioni interne, e infine a partire dallo slot $45 inizierà effettivamente a visualizzare i pixel.
Si potrebbe ricalcare lo stesso schema (e simile complessità) nel caso in cui si volesse mantenere la stessa flessibilità nel cercare di concedere quanti più slot liberi alla CPU (fino a 4, come già detto nel precedente pezzo), e soltanto da questo punto di vista non cambierebbe nulla. Ma ho già spiegato che ciò non è realmente utile né conveniente: rimane una complicazione (anche per l’implementazione del chipset) che ha soltanto il “merito” di castrare le prestazioni di CPU più moderne del 68000. Per tale motivo preferisco mostrare il funzionamento di un sistema packed libero da questi vincoli, anche perché ha delle positive ricadute su altre cose, oltre alla CPU (ne parlerò quando affronterò gli argomenti sprite e AGA).
Un sistema packed è libero da questi vincoli, perché non deve necessariamente partire da slot che siano multipli di 8 ($38, $30, $28, …, oppure $40, $48, …), ma è sufficiente che il controllore video carichi abbastanza dati dalla memoria per poter visualizzare i pixel in maniera continuativa (fino alla fine della riga da visualizzare), andando a pescare nuovi dati dalla memoria soltanto quando servono (ossia quando il buffer interno è vuoto, o comunque non ha abbastanza dati per garantire la visualizzazione continuativa dei successivi pixel). L’unico vincolo, in tal senso, è rappresentato dal fatto che il controllore video deve visualizzare due pixel consecutivi per ogni slot / color clock (perché un singolo color clock è costituito da due clock di sistema, e in bassa risoluzione viene visualizzato un pixel a ogni clock di sistema), per cui a un preciso slot deve sempre avere i dati a disposizione per visualizzare almeno due pixel. Un esempio pratico aiuterà a chiarire il concetto.
Intanto c’è da mettere immediatamente in risalto un fatto molto importante: c’è un solo puntatore in memoria per i dati dello schermo (BPL1PT
), come pure un solo registro a 16-bit (BPL1DAT
) per i dati letti e soltanto un altro registro interno (il buffer usato per le elaborazioni). Dunque già questo evidenzia un notevole risparmio sui costi di implementazione (contro i 6/8 bitplane & accessori necessari per la grafica planare).
Lasciando per il momento in piedi la necessità di 5 slot / color clock per l’elaborazione, l’inizio del caricamento dei dati dal singolo bitplane (ometterò la parola “singolo” da ora in poi, sempre per semplicità. Quindi, in questo contesto, bitplane = puntatore ai dati dello schermo packed) può partire allo slot $3F (dunque quello immediatamente prima del $40, il quale segna l’inizio dei 5 slot di elaborazione) anziché allo slot $38. Ciò perché, leggendo un solo dato dalla memoria ed essendo questo costituito da 16-bit, conterrà i dati per ben 5 pixel (ogni pixel a 8 colori richiede 3 bit) più un bit che appartiene al sesto pixel da visualizzare (il suo bit più significato, per l’esattezza).
A questo punto il controllore video ha già l’indice del colore del primo pixel che si trova nei 3 bit più significativi del buffer che è stato caricato: è sufficiente estrarli e utilizzarli per andare a leggere le componenti cromatiche del pixel dall’apposita voce nella tavolozza dei colori. La differenza rispetto alla grafica planare è che l’indice risulta già costruito (si tratta dei 3 bit più significativi nel buffer, per l’appunto), mentre nell’altro caso bisognava copiare tutti i bit più significativi dei buffer di ogni bitplane e assemblarli (shiftarli opportunamente). In questo caso bisogna estrarre i 3 bit dell’indice già costruito tenendo presente di riempire con zeri i bit non utilizzati (nel caso di uno schermo con 8 colori non vengono usati il quarto e il quinto bit per selezionare la voce nella tavolozza dei colori. Esattamente come con la grafica planare, dove i bit del quarto e quinto bitplane non erano utilizzati e andavano forzati a zero). Il costo di queste operazioni dovrebbe essere similare. In realtà sarebbe stato molto più semplice se il sistema fosse stato little-endian, perché sarebbe stata sufficiente una banale operazione di mascheramento (cioè un and binario col valore %111) per ottenere immediatamente l’indice del colore senza alcuna estrazione e spostamento di bit, in quanto i 3 bit sarebbero già posizionati nei 3 bit meno significativi del buffer.
Il controllore video – packed (OCS/ECS) – Pixel successivo e prossimi (max) 16 pixel
Con la grafica planare il passo per passare al pixel successivo era di shiftare a sinistra il bit più significativo di ogni bitplane, in modo da scartarlo e far subentrare quello che lo precedeva. Per far ciò si usano 6 barrel shifter a 16 bit da un solo shift. Su Stack Overflow è riportato lo schema di un barrel shifter a 8 bit con shift da 0 a 7, e da questo si possono immaginare gli shifter usati dal controllore video dell’Amiga, prendendo il primo a sinistra (verticalmente) ed estendendolo a 16 bit (16 multiplexer utilizzati).
Con la grafica packed, invece, per passare al pixel successivo si deve shiftare ogni volta a sinistra di 3 posizioni anziché una, quindi si deve usare il barrel shifter al completo, ma esteso a 16 bit. E fin qui il costo dell’implementazione packed risulta dimezzato rispetto alla grafica planare, perché al posto di 6 barrel shifter a 16 bit da un solo shift ne servono soltanto 3, ma combinati in modo da poter gestire shift da 0 a 7 bit (cioè esattamente il barrel shifter visto su Stack Overflow, ma esteso a 16-bit).
I problemi sorgono quando finiscono i dati per i pixel da visualizzare e se ne devono prelevare altri dati, concatenandoli con quelli parziali che erano rimasti. Avevo già mostrato che con la lettura dei primi 16-bit dal bitplane c’erano i dati per i primi 5 pixel (che usano 3 bit ciascuno) più il primo bit del sesto pixel. Quindi la problematica si presenta precisamente quando è stato visualizzato il quinto pixel e si deve passare al sesto, cioè esattamente allo slot / color clock $47 + 0,5 (perché i primi 2 pixel sono stati visualizzati durante lo slot $45, gli altri due con lo slot $46, il quinto nella prima metà dello slot $47 e il sesto dev’essere visualizzato nella seconda metà dello slot $47. Ricordo che uno slot / color clock è costituito da due cicli di clock di sistema e il controllore video visualizza un pixel per ogni ciclo di sistema che passa).
A questo punto nel buffer è presente soltanto il bit più significativo del sesto pixel e nient’altro: mancano i due bit meno significativi per completare il terzetto e poter visualizzare il pixel. Questo significa che il controllore video avrebbe già dovuto leggere i nuovi 16-bit dal bitplane, di modo che tali dati mancanti sarebbero stati, invece, disponibili. Ciò sarebbe dovuto avvenire sfruttando al massimo lo slot precedente ai 5 necessari per l’elaborazione, dunque usando il $42 (o il $41 o il $40: l’importante è che non sia il $43 o superiore) per leggerli e memorizzarli nel registro (non interno, ma quello accessibile da CPU e Copper).
Problema risolto, ma rimane la sfida più importante: mettere assieme i dati rimasti con quelli appena letti, in modo da poter continuare a visualizzare i pixel rimanenti senza discontinuità. Tecnicamente la soluzione che mi è venuta in mente (non so se in passato altri abbiano proposto soluzioni simili: intanto è importante che ci sia arrivato io per risolvere questo rompicapo, visto che non si tratta banalmente di mettere assieme due dati e darli in pasto a un unico barrel shifter. Infatti quella seguente non è la soluzione finale, che verrà presentata più avanti, con un esempio concreto) consta dell’esecuzione delle seguenti operazioni:
- ruotare i nuovi dati a destra del numero di bit rimasti. In questo modo il bit più significativo dei nuovi dati risulta posizionato esattamente sotto l’ultimo bit rimasto nel buffer;
- estrarre tutti i bit nuovi a partire da tale bit fino al primo (bit zero). Operazione che si traduce in un semplice and binario dei 16-bit rappresentati dai nuovi dati con una maschera costituita da tutti i bit a 1 a partire dal bit interessato fino al primo bit, e con tutti gli altri bit (superiori) a zero;
- eseguire l’or binario di questi bit estratti con quelli del buffer, conservando il risultato nel medesimo. In questo modo il buffer risulta nuovamente pieno, avendovi copiato i nuovi bit subito dopo (a scendere) quelli che erano rimasti;
- negare i bit della maschera generata al punto 2 ed eseguirne l’and binario coi nuovi dati, conservandovi il risultato. In questo modo vengono tolti di mezzo i bit copiati nel buffer.
L’operazione è pesante, come già detto, perché al punto 1 necessita di un barrel shifter a 16 bit che sia in grado di ruotare il contenuto per un massimo di 7 posizioni (i pixel sono al massimo di 64 colori, cioè 6 bitplane, per cui al massimo possono rimanere 5 bit nel buffer, e quindi i nuovi dati dovranno essere ruotati al massimo di 5 posizioni a destra). Questo significa che l’implementazione packed va in pari con quella planare per quanto riguardo l’uso dei barrel shifter, con in più un po’ di altra logica (elementare, ma comunque necessaria) per implementare le rimanenti operazioni. A ciò si aggiunge la necessità di tenere traccia, ogni volta, di quanti bit siano rimasti nel buffer e quanti nei nuovi dati, in modo da effettuare nuovamente il travaso dei secondi al primo alla prima occorrenza (quando serve rimpinguare il buffer per i successivi pixel da visualizzare).
Il controllore video – packed (OCS/ECS) – Implementazione più efficiente per buffer e dati nuovi
E’ possibile, però, ridurre notevolmente l’uso dei barrel shifter e, al contempo, gestire le sfide future rappresentate dall’uso di bus dati di ampiezza più elevata (32 o 64 bit con gli AGA) implementando buffer e registri interni per i nuovi dati sotto forma di coda FIFO di singoli byte (quindi 8 bit alla volta) e non di word (16 bit per l’Amiga). Il tutto semplificando il meccanismo di travaso dei bit e di rifornimento dei nuovi dati.
Il funzionamento della lettura dei dati è affidato al gestore della coda FIFO, che all’inizio di uno slot / color clock effettua i seguenti controlli:
- se c’era un’operazione di lettura al precedente slot, prende i dati letti (16 bit, quindi 2 byte), li inserisce alla fine della coda, e la sposta di due posizioni (due byte letti);
- se la coda ha ancora spazio libero e non sono finiti i dati della riga di raster, fa partire un’operazione di lettura;
- se il byte interno non ha dati e la coda non è vuota, copia il primo byte della coda nel byte interno, fa avanzare la coda di un posto, e imposta a 8 il numero di bit presenti nel byte interno.
Il meccanismo è molto semplice (anche da implementare) e garantisce che ci saranno sempre dati nella coda e nel byte interno. Tutto ciò posto, ovviamente, che lo slot di inizio della lettura dei dati sia adeguato, ma soddisfare questo criterio è molto semplice (è sufficiente che sia impostato allo slot precedente all’inizio dell’elaborazione dei dati da parte del controllore video).
Il controllore video risulta anch’esso semplificato, perché a ogni clock di sistema (perché i pixel vengono visualizzati usando questo clock) effettua le seguenti operazioni:
- estrae l’indice colore dai 3 bit più significativi presenti nel buffer. E’ sicuro che i dati siano presenti, quindi può farlo immediatamente, senza indugiare;
- shifta a sinistra di 3 posizioni, in modo da togliere di mezzo l’indice già estratto (e i 3 bit più in basso saranno a zero);
- sposta almeno (il perché dell’uso di questo termine sarà chiarito più avanti) 3 bit dal byte interno al buffer, utilizzando la procedura già descritta in precedenza (rotazione del byte contente i nuovi dati, ecc., ma con una piccola modifica che sarà illustrata nel prossimo articolo, nell’esempio fornito). Ovviamente tutto dipende da quanti bit siano ancora contenuti nel byte interno: se saranno meno di quelli richiesti, nel buffer saranno copiati soltanto quelli a disposizione;
- aggiorna il numero di bit a disposizione nel buffer. 3 sono stati tolti al secondo punto e alcuni copiati al terzo: la differenza fra questi due valori viene aggiunta al numero di bit a disposizione;
- aggiorna il numero di bit a disposizione nel byte interno. Se non ce ne sono, copia nel byte interno il prossimo byte disponibile nella coda FIFO, imposta il numero di bit del byte interno a 8, e aggiorna il numero di byte disponibili nella coda (questo è esattamente ciò che avviene nel punto 3 illustrato in precedenza, dal gestore della coda FIFO).
A prima vista può sembrare complicato, ma in realtà anche questo meccanismo è abbastanza semplice. Soprattutto, è molto efficiente, perché richiede l’uso di due soli barrel shifter a 8 bit in grado di spostare i bit fino a 7 posizioni, e ciò a prescindere dalla dimensione del bus dati. Infatti questa soluzione, assieme a quella della coda FIFO per leggere i dati, ha il pregio di aver totalmente disaccoppiato il controllore video dalla dimensione del bus dati, richiedendo sempre le stesse risorse (molto limitate: niente registri interni a 32, 64, o più bit. All’aumentare della dimensione del bus dati cambia soltanto la lunghezza della coda FIFO, come verrà illustrato nel prossimo pezzo).
Con questo è tutto, per il momento. Il prossimo articolo mostrerà un dettagliato esempio chiarificatore di quanto spiegato sopra, poi la valutazione dei costi del nuovo controllore video, e infine come sarebbe la versione AGA (packed).