4 bitplanes de l'Atari ST

Structure de la mémoire vidéo en 320x200 de l'Atari ST

Quand on se (re)plonge dans la programmation de l’Atari ST et notamment de l’affichage des pixels à l’écran, on se rend vite compte qu’on a pris de bien mauvaises habitudes avec nos ordinateurs “modernes”, où la structure en mémoire est très simple en comparaison.

Cet article, illustré de programmes en C, va vous permettre de comprendre ENFIN cet agencement particulièrement étonnant pour ne pas dire étrange.

Retrouvez mon ami Fred, qui a délaissé un peu son Amiga pour venir du côté obscur de l’Atari ST, dans sa quête d’affichage de pixels !

Préambule et mise en situtation

Fred : “Je veux afficher des Sprites animés avec de la musique en même temps, à 60 FPS !”

Minute Papillon ! Retour en 1985, Nom de Zeus ! Tu n’as que 16 couleurs à ta disposition, parmi 4096 si tu es le chanceux propriétaire d’un STE ou seulement 512 pour les ST et STF.

Et en plus tu vas rire, mais pour changer la couleur d’1 pixel à l’écran tu vas devoir manipuler des bits différents sur 4 octets distincts !

Fred : “Mais ! Euh !!! C’est compliqué !”

Oui c’est un peu compliqué, mais c’est un mal nécessaire pour économiser de la RAM ! Et puis tu verras, 64 bits consécutifs te permettront de définir la couleur de 16 pixels et ainsi de suite, tous les 8 octets (64 bits).

Allez, sans plus attendre je t’explique tranquillement, mais tu peux oublier de faire de la musique en même temps pour le moment !

La mémoire vidéo

A l’initialisation de l’Atari ST, une zone de la mémoire est réservée et contient l’information d’affichage des pixels affichés à l’écran. Je ne vais évoquer dans cet article que l’affichage en basse résolution, c’est à dire en 320x200 16 couleurs.

L’adresse de cette zone mémoire peut varier. En effet à l’initialisation, la fin de la RAM est allouée. Cela dépend donc de la taille de la mémoire :

  • avec un ST équipé de 4 Mo de RAM, elle se situe à l’adresse 3F8000 ;
  • alors qu’elle se situe à l’adresse F8000 pour un ST équipé d’un seul méga octet.

De plus, en pratique, l’adresse de la mémoire vidéo est changeable avec certaines contraintes dans le cas du ST (le STE n’aura pas ces contraintes), mais ceci est un autre sujet que je n’aborderai pas ici (double buffering, etc.).

Cette zone mémoire fait 32 000 octets, c’est à dire un peu moins de 32 Ko, puique 1 Ko = 1024 octets.

En C, je dispose d’une function Logbase() qui me retournera le pointer vers cette zone mémoire.

videoAddress = Logbase();

Etant donné qu’un pixel à l’écran fait référence à une couleur parmi 16, seuls 4 bits sont nécessaires par pixel pour préciser la référence de la couleur à afficher. Et oui, 4 bits == 16 valeurs possibles (de 0 à 15).

Petit rappel si jamais cela devait être nécessaire :

Décimal Binaire Hexa
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

La palette et le codage des couleurs

La palette de l’Atari ST est située à l’adresse fixe $FF8240. En C je dispose de fonctions qui appelleront directement des fonctionnalités du TOS pour récupérer (read), affecter (write) des valeurs à la palette, voire même de définir une nouvelle palette complète par copie d’une structure équivalente.

Chaque couleur est exprimée sur 16 bits, c’est-à-dire 2 octets, ou encore 1 word (mot). Il y a donc 32 octets utilisés pour la palette (16 couleurs * 2 octets).

Ces 16 bits permettent de spéficier la valeur “RGB” (Red Green Blue) de la couleur.

En réalité, seuls les 12 bits de poids faibles seront vraiment utilisés sur STE, et seulement 9 bits répartis sur les 12 bits dans le cas d’un ST/STF.

Chaque composante de couleur peut aller de :

  • 0 à 7 (3 bits) dans le cas d’un Atari ST
  • de 0 à F (4 bits) pour le STE, mais avec un agencement spécifique qui ne se suit pas de 0 à F de manière naturelle et classique.

En effet, sur STE, afin d’être compatible avec le ST mais aussi de permettre 4096 nuances par rapport à 512, les valeurs entre 8 et F sont réparties de la sorte, de la plus petite à la plus élevée : 0, 8, 1, 9, 2, A, 3, B, 4, C, 5, D, 6, E, 7, F. Les valeurs supplémentaires pour STE sont ainsi intercallées dans les valeurs possibles du ST.

Nous allons nous concentrer exclusivement sur une palette en mode ST “Normal”, sachant que les couleurs définies ainsi sont compatibles avec l’Atari STE.

On remarque qu’il y a donc 8 valeurs possibles (de 0 à 7) pour chaque composante. Ce qui fait 8 x 8 x 8 = 512 couleurs possibles.

Voici quelques exemples pour représenter certaines couleurs :

` RGB` Couleur
0777 blanc
0000 noir
0700 rouge
0070 vert
0007 bleu
0300 Rouge sombre
0003 Bleu sombre

Fred, regarde, les 4 bits de poids fort sont systématiquement à 0. On code la palette sur 12 bits seulement.

Pour les exemples suivants, nous utiliserons cette petite palette, définie en C de la manière suivante :

__uint16_t palette[16] =
    {
        0x000, // couleur 0
        0x300, // couleur 1 
        0x030, // couleur 2
        0x003, // couleur 3
        0x330, // couleur 4
        0x033, // couleur 5
        0x303, // couleur 6
        0x333, // couleur 7
        0x000, // couleur 8
        0x700, // couleur 9, rouge vif
        0x070, // couleur 10, vert vif
        0x007, // couleur 11, bleu vif
        0x770, // couleur 12
        0x077, // couleur 13
        0x707, // couleur 14
        0x777  // couleur 15
    };

Notez ici que les 4 bits de poids fort sont omis. __uint16_t est un type de 16 bits non signé.

La structure des blocs de 64 bits (4 x 16 bits) consécutifs

Voici venu le temps, non pas des rires et des chants, mais celui de s’attaquer à la fameuse structure “4 bitplanes”.

C’est assez particulier, mais quand on “revient” en 1985, cette structure porte tout sens quand il s’agit de gérer de multiples résolutions avec des couleurs faisant référence à une palette, le tout dans un espace mémoire contraint, dans notre cas, je le rappelle : environ 32 Ko ce qui est conséquent sur un ordinateur qui ne possède que 520 Ko !

Ces 64 bits, répartis donc sur 4 mots de 16 bits, serviront à représenter 16 pixels consécutifs.

Fred : “Mais alors, dis moi, par exemple, comment est représenté le premier pixel en haut à gauche, si je le veux en rouge !”

Alors déjà, le “rouge”, dans notre palette, c’est la couleur à l’index 9 du tableau. En effet, à cet index on y trouve la définition du rouge en RGB, c’est à dire 0x700.

Il faut donc affecter “9” quelque part en mémoire pour dire “affiche moi du rouge” !

Si jamais cela était nécessaire, 9, en binaire est représenté 1001. Le bit le plus à droite est le bit de poids faible. Le bit de plus à gauche est le bit de poids fort.

Fred : “Mais je le place où ce 1001 pour que mon premier pixel en haut à gauche soit rouge ?”

C’est là que ça se complique, il va falloir que tu répartisses chacun des ces bits sur 4 mots (word, 16 bits) différents :

  • 1er mot : sur le bit le plus à gauche (donc le bit de poids fort), il faut placer le bit de poids faible qui représente 9, c’est à dire 1
  • 2ème mot : sur le bit le plus à gauche, il faut placer le 2nd bit qui représente 9, c’est-à-dire 0
  • 3ème mot : sur le bit le plus à gauche toujours, il faut placer le 3ème bit qui représente 9, c’est-à-dire 0
  • 4ème mot : encore sur le bit le plus à gauche, il faut placer le 4ème et dernier bit qui représente 9, c’est-à-dire 1

Ce qui donne pour ces 4 premiers mots le valeurs suivantes, en binaire.

__uint16_t pixels[4] =
    {
        0b1000000000000000,         
        0b0000000000000000,          
        0b0000000000000000,         
        0b1000000000000000 
    };

J’ai disposé volontairement les mots les uns en dessous des autres. Regarde la première colonne (après 0b) ! On y voit le nombre 1001 en vertical, sachant que le bit de poids faible de ce nombre est placé sur le premier mot, et le bit de poids fort sur le second mot.

C’est un peu comme si tu distribuais des cartes à différents joueurs :

  • les cartes, ce sont les “bits” de l’index de la couleur dans palette,
  • les joueurs, ce sont les 4 mots consécutifs.

Fred : “Ouah mais c’est la galère ! Et comment je fais pour avoir du vert à côté du rouge maintenant ?”

Et bien le vert dans notre palette, c’est la couleur 10, qui se note 1010 en binaire. On refait donc la même manipulation sur chaque mot, mais cette fois-ci sur chacun des bits situés à 1 décallage à droite du bit de poids fort, ceux qui nous a permis d’indiquer la couleur rouge. Je place aisni successivement les bits 1010 répartis sur la 2ème colonne de chacun des mots.

__uint16_t pixels[4] =
    {
        0b1000000000000000,         
        0b0100000000000000,          
        0b0000000000000000,         
        0b1100000000000000 
    };

Tu vois, en 2ème colonne, le nombre “1010” est apparu, quand tu lis du haut vers le bas.

Maintenant, si tu veux du bleu sur le 16ème pixel de l’écran …

Fred : “Je sais ! Je place 1011, car c’est la référence du bleu, sur le 16ème bit de mes 4 mots.

__uint16_t pixels[4] =
    {
        0b1000000000000001,         
        0b0100000000000001,          
        0b0000000000000000,         
        0b1100000000000001 
    };

Pour résumer, nous avons fait ceci (j’ai supprimé de ce schéma les bits et les couleurs que nous n’avons pas utilisés dans cet exemple, même si le noir, en 0, est bien une couleur !)

Schema

Pour t’aider de manière interactive, je te propose ce petit simulateur.

Fred : “En fait, c’est simple mais pour changer 1 pixel, il faut manipuler 4 octets répartis sur 4 mots !”

Oui c’est un peu compliqué, il faut bien l’avouer, c’est l’une des raisons pour laquelle les “sprites” sur Atari ST ont très souvent une largeur de 16 pixels, car ils se manipulent ainsi d’un seul bloc.

Fred : “Et si je veux refaire le même motif pour les 16 pixels suivants ?”

Facile : tu copies les 4 mots à une adresse située à 4 mots (64 bits) de décalalage, c’est-à-dire 8 octets plus loin, et ainsi de suite.

Sur une ligne à l’écran, tu as donc 20 blocs de 16 pixels. Chaque bloc prend 8 octets (64 bits, 4 mots). Une ligne est donc composée de 160 octets.

Et quand tu sais qu’il y a 200 lignes, cela fait bien 32000 octets pour représenter l’écran. (160 octets x 200 lignes).

Fred : “Ouahhh, j’ai compris !”

Mise en pratique en C

Pour cette mise en pratique, j’ai donc installé le compilateur GCC “Cross-Compiler” pour Motorola 68000 sur mon Linux. Le détail de l’installation est dans cet article.

En complément j’ai installé libcmini qui me permet d’obtenir un exécutable .TOS de faible taille sur l’Atari ST.

Voici le code source d’un programme qui illustre tout ce que nous venons de voir.

#include <sys/types.h>
#include <stdio.h>

__uint16_t* videoAddress;

const short LOW_RES = 0;
const short MEDIUM_RES = 1;

__uint16_t savedResolution;
__uint16_t savedPalette[16];

__uint16_t palette[16] =
{
    0x000, 
    0x300,
    0x030,
    0x003,
    0x330,
    0x033,
    0x303,
    0x333,
    0x000,
    0x700,
    0x070,
    0x007,
    0x770,
    0x077,
    0x707,
    0x777
};

// gets color value in the current palette
__uint16_t GetColor(short colorIndex)
{
    return Setcolor(colorIndex, -1);
}

// saves the current palette into a buffer
void SavePalette(__uint16_t* paletteBuffer)
{
    for(int i=0; i < 16; i++)
    {
        paletteBuffer[i] = GetColor(i);
    }
}

// saves the current resolution and its palette
void SaveResolutionAndPalette()
{
    savedResolution = Getrez(); // Get current resolution
    SavePalette(savedPalette); // Save the palette
}

// restores the saved resolution and its palette
void RestoreResolutionAndPalette()
{
    Setscreen(-1,-1, savedResolution);
    Setpalette(savedPalette);
}

// changes the current resolution
void SetResolution(short resolution)
{
    Setscreen(-1,-1, resolution);
}

void DisplayInfo()
{
    SetResolution(MEDIUM_RES);
    Setcolor(0, 0x000);
    Setcolor(1, 0x777);
    Setcolor(2, 0x700);
    Setcolor(3, 0x070);
    printf("Starting ...\r\n");
    printf("Video Address : %x\r\n", videoAddress);
    printf("Press [ENTER]\r\n");
    getchar();
}

void DisplayScreen()
{
    // pattern : 16 pixels into 4 words
    __uint16_t pixels[4] =
    {
        0b1000000000000001,
        0b0100000000000001,
        0b0000000000000000,
        0b1100000000000001
    };

    // not optimized, but better for understanding
    // 20 blocks * 200 lines = 4000 iterations
    for(int i = 0 ; i < 4000 ; i ++)
    {
        short offset = 4 * i;
        videoAddress[offset]     = pixels[0];
        videoAddress[offset + 1] = pixels[1];
        videoAddress[offset + 2] = pixels[2];
        videoAddress[offset + 3] = pixels[3];
    }
}

// demo runs here
int main(int argc, char *argv[])
{
    // inits
    videoAddress = Logbase(); // get the logical pointer of the video RAM
    SaveResolutionAndPalette();
    DisplayInfo();
 
    // demo
    SetResolution(LOW_RES);
    Setpalette(palette);
    DisplayScreen();    
    getchar();

    // restore initial state   
    RestoreResolutionAndPalette();
    return 0;
}

Nota : l’usage de libcmini m’impose de mettre \r\n pour avoir un retour à la ligne correct dans la fonction DisplayInfo , mais ce n’est pas le comportement normal de printf.

Pour le compiler, ce Makefile :

CC=m68k-atari-mint-gcc
LIBCMINI=/usr/m68k-atari-mint/libcmini
CFLAGS=-std=gnu99 -I/usr/m68k-atari-mint/include-libcmini -nostdlib $(LIBCMINI)/crt0.o 
LINKFLAGS=-s -L$(LIBCMINI) -lcmini -lgcc
all: clean main

main: 
	$(CC) $(CFLAGS) main.c -o main.tos $(LINKFLAGS)

clean:
	rm -rf *.tos

et pour l’exécuter :

$ make all && hatari main.tos

Et on obtient le premier écran d’info (pas très bavard, mais c’est déjà ça) :

DisplayInfo

Puis après avoir appuyé sur la touche ENTREE :

DisplayScreen

Avec un petit zoom, sur les 3 premier blocs de 16 pixels (rouge, vert, sur les deux premiers pixels, puis bleu sur le 16ème pixel):

DisplayScreen

Puis après avoir appuyé sur la touche ENTREE retour au Desktop avec la bonne résolution et la bonne palette.

Retour GEM

Conclusion

Tu vois, ce n’était pas si compliqué.

Fred : “Mais c’est aussi parce que tu expliques trop bien…”

Tu vas me faire rougir ! Et tu sais quoi, tout ce que tu viens de comprendre est aussi valable sur ton Amiga, qui, il me faut l’avouer même si cela m’est difficile, est un peu plus puissant que mon Atari ST sur ce point. En effet, sur ton Amiga, tu peux avoir une palette de 64 couleurs en 320x256 qui utilise 6 bitplanes où 1 pixel est réparti sur 6 mots !

Mais je terminerai cet article par le mot “Atari ST”, pour ne pas conclure sur l’Amiga, ce serait un comble. Mince, encore …

Atari ST !!!

Voilà, c’est bien comme cela.

Remerciements

Je tiens à remercier Lyloo et Vincent Rivière pour leur relecture attentive de cet article et pour leurs conseils bienveillants.

Commentaires

Mes autres articles
Simulateur de Palette et 4 bitplanes
Assembleur 6809 pour Thomson MO5 sous Linux
Assembleur 68000 pour Atari ST sous Linux
Programmation C pour Atari ST sous Linux
REST API avec Quarkus, JPA, PostGreSQL et GraalVM
Functional Switch/Case en Java 8 et +
Repenser son style de codage
Le singleton est mort, vive le singleton !
Découverte de Deltaspike Data Module
Préconditions des méthodes d'une API
Serveur minimal HTTP avec JShell en Java_10
Apache Archiva sur Raspberry PI
Contribuer à l'Eclipse Foundation : DONE
Connaissez-vous bien l'instanciation ?
Xenon Reborn, développer en s'amusant et réciproquement
Où définir au mieux une NamedQuery JPA ?
Lombok, Oui ! Mais...