Navigation

Tutoriel précédent : rasterization   Sommaire   Tutoriel suivant : Render Output Target

II. Introduction

Comme je l'ai dit précédemment, nos textures sont du papier peint. Ce sont des images numériques comme les autres, que l'on va plaquer sur la surface d'un objet. Cela peut permettre de simuler un matériau : on peut plaquer une texture de marbre sur un mur, par exemple. Ces textures servent aussi à d'autres choses, comme simuler du relief via du bump-mapping, etc.

Comme je l'ai dit, une texture est, en première approximation, une image qui stocke du papier peint. Cette image est donc composée de pixels. Pour bien faire la différence entre les pixels d'une texture et les pixels de l'écran, les pixels d'une texture sont couramment appelés des texels.

Les cartes graphiques supportent divers formats de textures, qui servent à indiquer comment les texels sont stockés en mémoire. Par exemple, les cartes graphiques peuvent supporter des textures au format RGB, RGBA, en niveaux de gris, etc.

Chacun de ces texels sera attribué à un sommet. Attribuer un texel à un sommet consiste simplement à plaquer la texture sur notre objet géométrique. Cette attribution est faite à la fabrication du modèle de l'objet : lorsque les créateurs de jeux vidéo conçoivent le modèle de l'objet. Chaque sommet contient donc des coordonnées de texture, en plus de sa position, de ses couleurs et de sa normale. Ces coordonnées de textures identifient quel texel appliquer sur le sommet. Elles précisent également la position du texel dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à la ligne 5, et la colonne 27 dans ma texture.

Application d'une texture

À partir de ces coordonnées de texture, un circuit placé dans la carte graphique se chargera d'aller lire le contenu de cette texture dans la mémoire vidéo. Évidemment, cela ne marche que pour les pixels qui sont tout pile sur un sommet. Pour les autres, ce sont les coordonnées barycentriques qui sont utilisées pour déterminer quel texel utiliser.

Sur certaines architectures assez anciennes, les textures disposaient de leur propre mémoire, séparée de la mémoire vidéo. Mais c'est du passé : maintenant, les textures sont stockées dans la mémoire vidéo principale.

III. Filtrage

On pourrait croire que simplement plaquer nos textures sur nos sommets sans aucune forme de procès suffit à rendre les graphismes de notre application d'une qualité époustouflante. Seulement, il y a un léger problème avec cette technique : les texels de notre texture ne vont pas tomber tout pile sur un pixel de l'écran. Nos texels sont appliqués sur une surface dans un espace 3D. Leur coordonnées sont donc des flottants. Une fois mappés sur la surface de l'écran, nos texels sont placés à une position bien précise. Mais celle-ci n'est pas forcément exactement sur le pixel. Le sommet peut être un petit peu trop en haut, ou trop à gauche, etc.

Pour résoudre ce problème, on peut appliquer une solution simple : ne rien faire. Chaque pixel sera colorié avec le texel correspondant au sommet le plus proche. Autant être franc, le résultat est assez dégoûtant.

Pour améliorer la qualité de l'image, la carte graphique va effectuer un filtrage de la texture. Ce filtrage consiste à choisir le texel à appliquer sur un pixel du mieux possible. La carte graphique va alors calculer les coordonnées du texel par un calcul mathématique assez simple. Suivant la qualité d'image voulue, on peut utiliser divers traitements.

Pour donner un exemple, voici ce que peut donner un filtrage. À gauche, pas de filtrage. Au milieu, un filtrage dit bilinéaire, utilisé dans pas mal de cartes graphiques. À droite, un filtrage bicubique, pas encore utilisé dans les cartes graphiques actuelles. La différence est flagrante.

Exemple d'échantillonage

Ce filtrage est réalisé par un circuit spécialisé : l'échantillonneur de texture. L'ensemble des circuits chargés de gérer les textures est donc composé de deux grands circuits :

  • un circuit qui calcule les adresses mémoire des texels à lire et les envoie à la mémoire ;
  • un circuit qui va filtrer les textures.
Schéma d'une unité de texture

III-A. Filtrage bilinéaire

Le plus simple de ces filtrages est le filtrage bilinéaire. Dans le cas le plus simple, le pixel est situé à égale distance des quatre texels. Dans ce cas, les couleurs RGB de ces texels sont simplement moyennées pour obtenir la couleur RGB du pixel final. Tout change quand le pixel n'est pas situé au centre du carré formé par les quatre texels environnants.

Schéma de filtrage de texture

Dans ce cas, le filtrage va effectuer une opération mathématique très simple qu'on appelle une interpolation linéaire. Pour comprendre l'idée, nous allons supposer que notre pixel est aligné avec deux autres texels.

Schéma de filtrage linéaire

Dans ces conditions, on peut obtenir une approximation de sa couleur. Pour cela, nous allons faire une première supposition : la couleur varie entre les deux texels en suivant une fonction affine.

Schéma interpolation linéaire

En supposant cela, on peut calculer la couleur de notre pixel par un petit calcul mathématique. Il suffit de calculer la pente de la courbe, en prenant un des deux texels comme point de départ. Ensuite, il suffit de multiplier cette pente par la distance entre le texel choisi et le pixel. Et enfin, il faut ajouter la couleur de base du texel choisi.

Seul problème, cela marche pour deux pixels, pas quatre. Avec quatre pixels, nous allons devoir calculer la couleur de points intermédiaires :

  • celui qui se situe à l'intersection entre la droite formée par les deux texels de gauche, et la droite parallèle à l'abscisse qui passe par le pixel. ;Première phase d'un filtrage bilinéaire
  • celui qui se situe à l'intersection entre la droite formée par les deux texels de droite, et la droite parallèle à abscisse qui passe par le pixel.Seconde étape du filtrage bilinéaire

Le calcul de la couleur de ces deux points s'effectue par interpolation linéaire, comme indiqué plus haut. Ces deux points sont alignés avec le pixel, et il suffit d'utiliser une troisième interpolation linéaire pour obtenir le résultat.

Le circuit qui permet de faire ce genre de calcul est particulièrement simple. Celui-ci est composé de circuits, chacun étant chargé d'effectuer une interpolation linéaire, reliés comme suit :

Schéma unité d'échantillonage

On trouve un circuit pour chaque composante de couleur de chaque texel : un pour le rouge, un pour le vert, un pour le bleu et un pour la transparence.

III-B. Mip-mapping

Utiliser des textures brutes a un léger désavantage. Si jamais cette texture est plaquée sur un objet lointain, une bonne partie des détails de la texture sera invisible pour l'utilisateur. Par exemple, un objet assez lointain peut très bien ne prendre que quelques dizaines de pixels à l'écran. Plaquer une texture de 512 pixels de côté serait vraiment du gâchis en termes de performances : il faudrait charger tous les pixels de la texture, les traiter, et n'en garder que quelques-uns. De plus, procéder comme cela pourrait créer des artefacts visuels : les textures affichées ont tendance à pixeliser. Cela se voit à l'écran et donne un résultat assez perturbant.

Pour limiter la casse, les concepteurs de jeux vidéo utilisent souvent la technique du mip-mapping. Cette technique consiste simplement à utiliser plusieurs exemplaires d'une même texture, chaque exemplaire étant adapté à une certaine distance. Ce qui différenciera ces exemplaires, ce sera leur résolution. Par exemple, une texture sera stockée dans un exemplaire de 512 * 512 pixels, un autre de 256 * 256, un autre encore de 128 * 128 et ainsi de suite jusqu'à un dernier exemplaire de 32 * 32. Chacun de ces exemplaires correspondra à un niveau de détail, aussi appelé Level Of Detail en anglais (abrévié en LOD).

Le bon exemplaire sera choisi lors de l'application de la texture. Ainsi, les objets proches seront rendus avec la texture la plus grande (512 par 512 dans notre exemple). Au-delà d'une certaine distance, les textures 256 par 256 seront utilisées. Encore plus loin, les textures 128 par 128 seront utilisées, etc. Évidemment, cette technique consomme de la mémoire RAM : chaque texture est dupliquée en plusieurs exemplaires. On peut remarquer une chose : si je prends une texture à un niveau de détail donné, la texture de niveau de détail immédiatement inférieur sera quatre fois plus petite : deux fois moins de pixels en largeur et deux fois moins en hauteur. Donc, si je pars d'une texture de base contenant X pixels, la totalité des mip-maps, texture de base comprise, prendra X + (X/4) + (X/44) + (X/44*4) + … Cela donne 4/3 * X. La technique du mip-mapping prendra donc au maximum 33 % de mémoire en plus (sans compression).

Pour faire en sorte que la bonne mip-map soit choisie, les circuits chargés de calculer l'adresse de la texture doivent recevoir des informations supplémentaires pour choisir la mip-map à appliquer. De plus, ils doivent être adaptés pour calculer l'adresse du texel correctement : celui qui doit être chargé depuis la bonne mip-map.

Schéma fonctionnement mipmapping

Pour faciliter ces calculs, les mip-maps d'une texture sont stockées les unes après les autres en mémoire. Pas besoin de se souvenir de la position en mémoire de chacune des mip-maps : l'adresse de la plus grande et quelques astuces arithmétiques suffisent.

III-C. Filtrage trilinéaire

Avec le mip-mapping, les textures sont un peu plus belles. Mais cette technique a un léger défaut : des discontinuités apparaissent lorsqu'une texture est appliquée répétitivement sur une surface. Par exemple, pensez à une texture de sol : celle-ci est appliquée plusieurs fois sur toute la surface du sol. Au-delà d'une certaine distance, le LOD utilisé change brutalement et passe par exemple de 512 * 512 à 256 * 256. La transition est malheureusement visible pour un joueur attentif. Le filtrage trilinéaire permet d'adoucir ces transitions. Son principe est simple : il consiste à faire « une moyenne » entre les textures des niveaux de détails adjacents.

Le filtrage trilinéaire fonctionne comme suit. Il consiste à effectuer deux filtrages bilinéaires : un sur la texture du niveau de détail adapté et un autre sur la texture de niveau de détail inférieur. Les résultats obtenus lors des deux filtrages sont ensuite multipliés par des coefficients, avant de subir une interpolation linéaire. Le circuit qui s'occupe de calculer un filtrage trilinéaire est une amélioration du circuit utilisé pour le filtrage bilinéaire. Il est constitué d'un circuit effectuant un filtrage bilinéaire, de deux registres, d'un interpolateur linéaire et de quelques circuits de gestion, non-représentés.

Son fonctionnement est simple : ce circuit charge quatre texels d'une mip-map, les filtres, et stocke le tout dans un registre. Il recommence l'opération avec les quatre texels de la mip-map de niveau de détail inférieur, et stocke le résultat dans un autre registre. Enfin, le tout passe par un circuit qui interpole les couleurs finales en tenant compte des coefficients, ces coefficients étant précisés dans des registres. Bien sûr, il s'agit d'un schéma de principe : les circuits utilisés dans nos cartes graphiques sont certainement un peu plus optimisés que ça.

Schéma filtrage bilinéaire

Il est possible de créer un circuit qui effectue les deux filtrages en parallèle.

Schéma filtrage bilinéaire parallèle

Seul problème : ce genre de circuit nécessite de charger huit pixels simultanément. Qui plus est, ces huit pixels ne sont pas consécutifs en mémoire. Utiliser ce genre de circuit nécessiterait d'adapter la mémoire et le cache, ce qui ne vaut généralement pas la peine.

Modifier le circuit de filtrage ne suffit pas. Comme je l'ai dit plus haut, la dernière étape d'interpolation linéaire utilise des coefficients, qui lui sont fournis par des registres. Seul problème : entre le moment où ceux-ci sont calculés par l'unité de mip-mapping, et le moment où les texels sont chargés depuis la mémoire, il se passe beaucoup de temps. Si ceux-ci sont lus depuis la mémoire, cela peut aller jusqu'à 400 à 800 cycles d'horloge. Durant tout ce temps, ces coefficients doivent être mémorisés.

Le problème, c'est que les unités de texture sont souvent pipelinées : elles peuvent démarrer une lecture de texture sans attendre que les précédentes soient terminées. À chaque cycle d'horloge, une nouvelle lecture de texels peut commencer. La mémoire vidéo est conçue pour supporter ce genre de chose. Cela a une conséquence : durant les 400 à 800 cycles d'attente entre le calcul des coefficients et la disponibilité des texels, entre 400 et 800 coefficients sont produits : un par cycle. Autant vous dire que mémoriser 400 à 800 ensembles de coefficient prend beaucoup de registres.

III-D. Filtrage anisotropique

Le filtrage trilinéaire permet de gommer les imperfections dues au mip-mapping. Mais d'autres artefacts peuvent survenir lors de l'application d'une texture. La perspective a tendance à déformer les textures, et peut entraîner l'apparition de flou dans certains cas. Pour gommer ce flou de perspective, les chercheurs ont inventé le filtrage anisotropique.

Dans tous les cas, le filtrage anisotropique va charger un grand nombre de texels et effectuer des suites de filtrages bilinéaires sur ces texels chargés. Les texels chargés seront convenablement choisis, d'une manière qui change selon l'algorithme utilisé. De plus, ces texels se verront attribuer des coefficients afin de prendre en compte certains texels en priorité.

L'utilisation de filtrage anisotropique ne change rien au niveau des circuits de filtrage. Cela peut paraître bizarre, mais en réalité, le filtrage anisotropique ne fait que mieux choisir les texels sur lesquels utiliser le filtrage, et leur attribuer des coefficients. Tout se passe donc lors du choix des texels, à savoir : l'étape de calcul d'adresse.

Schéma filtrage anisotropique

Quand je parle de filtrage anisotropique, je mens un tout petit peu. En fait, je devrais plutôt dire : LES filtrages anisotropiques. Il en existe plusieurs. Certains sont des algorithmes qui ne sont pas utilisés dans les cartes graphiques actuelles. Ceux-ci prennent beaucoup trop de circuits et sont trop gourmands en accès mémoires et en calculs pour être efficaces. Il semblerait que les cartes graphiques actuelles utilisent des variantes de l'algorithme TEXRAM, comme l'algorithme Fast Footprint Assembly. On pourrait aussi citer l'algorithme Talisman de Microsoft, qui serait implémenté depuis Direct X 6.0.

IV. Compression

Nos textures sont donc des images. Soit. Cela dit, ce sont souvent des images assez grosses, du style 512 pixels de large pour 512 pixels de haut en moyenne. Certaines textures un peu spéciales peuvent aller jusqu'à des résolutions de 2048 par 2048. Chaque pixel faisant 4 octets, on arrive à des textures pouvant aller jusqu'au mébioctet. Quand on sait qu'une scène 3D normale peut dépasser la cinquantaine de textures, on est heureux de ne pas être une mémoire vidéo. Et c'est sans compter le filtrage, qui impose de lire plusieurs texels pour colorier un seul pixel. Un filtrage bilinéaire impose de lire quatre texels en mémoire par pixel. Le filtrage trilinéaire en demande huit. Et le filtrage anisotropique en demande encore plus (bien que cela dépende de l'algorithme).

D'après les mesures récentes, la majorité des accès à la mémoire vidéo sont des accès aux textures. Pour limiter la casse, de nombreux formats de compression de texture ont été inventés. Nos textures sont donc compressées une fois pour toutes. Elles prennent ainsi moins de place en mémoire vidéo. Seul problème : ces textures doivent être décompressées pour être utilisables. Cette décompression a lieu lors de l'accès à la texture. Notre carte graphique contient un circuit, capable de décompresser un ou plusieurs texels chargés depuis la mémoire vidéo. Fait important : toute la texture n'est pas décompressée : seuls les texels lus depuis la mémoire le sont.

Nos cartes graphiques supportent un grand nombre de formats de compression de texture. Nous allons en voir quelques-uns. Tous ces formats sont des formats de compression dits avec perte. Cela signifie qu'il y a une légère perte de qualité lors de la compression. Toutefois, cette perte peut être compensée en utilisant des textures à résolution plus grande.

IV-A. Couleurs indicées

La première technique de compression est particulièrement rudimentaire. Elle était utilisée autrefois dans d'anciennes cartes graphiques, et est encore utilisée dans certains systèmes embarqués.

Avec cette technique, chaque texture est fournie avec une table des couleurs. Cette table des couleurs est un vulgaire tableau, contenant toutes les couleurs utilisées dans la texture. La texture ne contient aucune couleur par elle-même. À la place de chaque couleur, la texture stockera un indice, un numéro qui pointe vers une couleur dans la table des couleurs.

IV-B. Vector quantization

La technique de vector quantization peut être vue comme une amélioration de la technique précédente. Sauf que cette fois-ci, elle ne travaille pas sur des pixels, mais sur des blocs de pixels.

Avec la technique de vector quantization, l'image est découpée en blocs de 2 * 2, 3 * 3 ou 4 * 4 texels suivant la carte. À l'intérieur de la carte graphique, on trouve une table qui stocke tous les blocs possibles de 2 * 2, 3 * 3 ou 4 * 4 texels suivant la carte. Chacun de ces blocs possibles se voit attribuer un numéro. La texture sera composée d'une suite de ces numéros, qui encoderont chacun un bloc de 2 * 2, 3 * 3 ou 4 * 4 texels suivant la carte.

Quelques anciennes cartes graphiques ATI, ainsi que quelques cartes utilisées dans l'embarqué, utilisent encore ce genre de techniques de compression.

IV-C. S3TC / DXTC

Le format de compression de texture le plus utilisé de nos jours est le DXT. Il s'agit du format de compression de texture utilisé de base par Direct X. Le DXTC découpe la texture à compresser en carrés de quatre pixels de côté. Chaque bloc est compressé et décompressé indépendamment des autres. Ce bloc de 16 pixel (4 * 4), sera encodé dans un bloc de mémoire de 64 bits. Le bloc de mémoire contiendra :

  • une couleur codée sur 16 bits ;
  • une seconde couleur placée immédiatement à la suite ;
  • et des bits de contrôle qui indiquent comment colorier les 16 texels du bloc.

On trouve deux bits de contrôle par texel. Suivant la valeur des deux bits, la carte graphique choisira une des deux couleurs, ou les mélangera. Si jamais la couleur 1 et supérieure à la couleur 2, alors les deux bits sont à interpréter comme suit :

  • 00 = Couleur 1 ;
  • 01 = Couleur 2 ;
  • 10 = (2 * Couleur 1 + Couleur 2) / 3 ;
  • 11 = (Couleur1 + 2 * Couleur2) / 3.

Si jamais ce n'est pas le cas, alors les deux bits sont à interpréter comme suit :

  • 00 = Couleur 1 ;
  • 01 = Couleur 2 ;
  • 10 = (Couleur 1 + Couleur 2) / 2 ;
  • 11 = Transparent.

Cette première technique de compression de texture est ce qu'on appelle le DXT1. Elle ne code que des couleurs encodées au format RGB et ajoute une gestion minimale de transparence quand couleur1 < couleur2.

Pour combler les limitations du DXT1, le format DXT2 a fait son apparition. Il a rapidement été remplacé par le DXT3. Dans le DXT3, la texture est toujours découpée en blocs de 16 texels. Seule différence : la transparence fait son apparition. Chacun de ces blocs de texels est encodé sur 128 bits. Les 64 premiers bits servent à stocker des informations de transparence : 4 bits par texel. Le tout est suivi d'un bloc de 64 bits identique au bloc du DXT1. Le DXT3 a rapidement été replacé par le DXT4 et par le DXT5.

Dans ces deux formats, l'information de transparence est stockée par :

  • un en-tête contenant deux valeurs de transparence ;
  • le tout suivi d'une matrice qui attribue trois bits à chaque texel.

En fonction de la valeur de ces bits, les deux valeurs de transparence sont combinées pour donner la valeur de transparence finale. Le tout est suivi d'un bloc de 64 bits identique à celui qu'on trouve dans le DXT1. Pour être franc, tous les jeux vidéos actuels encodent une bonne partie de leurs textures en DXT5.

IV-D. PVRTC

Passons maintenant à un format de compression de texture un peu moins connu, mais omniprésent dans notre vie quotidienne : le PVRTC. Ce format de texture est utilisé notamment dans les cartes graphiques de marque PowerVR. Vous ne connaissez peut-être pas cette marque, et c'est normal : elle ne crée pas de cartes graphiques pour PC. Elle travaille surtout dans les cartes graphiques embarquées. Ces cartes se trouvent notamment dans l'iPad, l'iPhone et bien d'autres smartphones actuels.

Avec le PVRTC, les textures sont encore une fois découpées en blocs de 4 texels par 4. Mais la ressemblance avec le DXTC s'arrête là. Chacun de ces blocs est stocké en mémoire dans un bloc qui contient :

  • une couleur codée sur 16 bits ;
  • une couleur codée sur 15 bits ;
  • 32 bits qui servent à indiquer comment mélanger les deux couleurs ;
  • et un bit de modulation, qui permet de configurer l'interprétation des bits de mélange.

Les 32 bits qui indiquent comment mélanger les couleurs sont une collection de 2 paquets de 2 bits. Chacun de ces deux bits permet de préciser comment calculer la couleur d'un texel du bloc de 4 * 4.

V. Cache de texture

Les accès aux textures se font donc en mémoire vidéo. Seul problème : notre mémoire vidéo est lente. Pour faciliter l'accès aux textures, les cartes 3D utilisent souvent une ou plusieurs mémoires caches ultrarapides, spécialisées dans le traitement des textures. Le but de cette mémoire cache n'est pas tellement de rendre les accès à la mémoire plus rapides. Elle sert surtout à diminuer les accès à la mémoire. Cela permet de libérer la mémoire vidéo pour charger des sommets, enregistrer une image en mémoire, etc.

Schéma cache de texture

Lorsqu'un texel est lu depuis la mémoire, celui-ci est placé dans ce cache de textures. Ainsi, lors des utilisations ultérieures, la carte graphique aura juste à lire le texel depuis ce cache, au lieu de devoir se taper un accès à la mémoire vidéo. Cette mémoire cache étant plus rapide que la mémoire vidéo, les performances augmentent.

Notre mémoire cache est composée de blocs de mémoire, qui servent pour échanger des données entre la mémoire vidéo et le cache de textures. Lorsque des données doivent copier de la mémoire dans le cache de texture, un bloc de données est rapatrié de la mémoire vers le cache. Ce bloc a une taille fixe, et sert d'unité de base pour les échanges entre mémoire et cache.

Les accès en mémoire vidéo sont particulièrement optimisés pour les accès à des adresses consécutives. De base, les textures sont souvent stockées linéairement : les pixels d'une texture sont stockés les uns à la suite des autres, ligne par ligne. On pourrait croire que cette solution fonctionne bien pour échanger des données entre le cache de textures et la mémoire vidéo, mais en réalité, elle entre en conflit avec le filtrage de texture.

Comme on l'a vu précédemment, le filtrage de texture utilise souvent des carrés de texels. Pour faciliter l'utilisation du cache de textures, le stockage des textures en mémoire peut tenir compte de cette organisation. Les textures peuvent être découpées en carrés de N texels de côté, placés les uns à côté des autres en mémoire. En faisant cela, on accède plus facilement à des zones proches de la mémoire lors des opérations de filtrage. Les performances sont les meilleures possibles quand chaque carré de texel permet de remplir exactement une ligne de cache.

D'ordinaire, les textures sont toujours compressées une fois chargées dans le cache de texture. La décompression s'effectue une fois la donnée lue dans le cache. Il est possible de décompresser les textures avant de les placer dans le cache, lors de leur transfert entre la mémoire vidéo et le cache. Mais en faisant cela, la quantité de données utiles stockées dans le cache diminue : une texture compressée prenant moins de place, on peut en mettre plus dans un cache de taille fixe.

V-A. Multilevel texture cache

Cela dit, nos cartes graphiques actuelles n'ont pas qu'un seul cache de textures. Toutes les cartes graphiques actuelles disposent de deux caches de textures : un petit et un gros.

Schéma de cache à plusieurs niveaux

Les deux caches sont fortement différents. L'un est un gros cache, qui fait dans les 4 kibioctets, et l'autre est un petit cache, faisant souvent moins d'1 kibioctet.

V-B. Cohérence des caches

Dans la majorité des cas, le cache de textures est accessible uniquement en lecture et pas en écriture. Il est conçu ainsi afin de diminuer la quantité et le coût de ses circuits. Dans certains cas, cela peut poser problème.

Les jeux vidéos 3D récents utilisent des techniques dites de rendu vers texture (render-to-texture). Ces techniques consistent simplement à calculer certaines données et à les écrire en mémoire pour une utilisation ultérieure. Ces données peuvent être des données servant dans les algorithmes d'éclairage et d'illumination, peu importe.

La présence d'une mémoire cache en lecture seule peut alors poser des problèmes. Imaginons que la carte graphique lise une donnée dans une texture. Celle-ci est alors placée dans le cache de texture. Peu de temps après, une écriture au même endroit dans la texture a lieu. Le cache étant en lecture seule, son contenu n'est alors pas mis à jour. Lors des accès ultérieurs à ce cache de texture, c'est l'ancienne donnée qui sera récupérée dans le cache, pas la nouvelle version qui vient d'être écrite.

Le contenu du cache et celui de la mémoire ne sont donc pas cohérents. Leur contenu peut diverger suivant les accès effectués à la mémoire. Trouver une solution à ce problème est une nécessité.

Une solution simple consiste à garder notre cache en lecture seule et à invalider les données mises à jour en mémoire lors d'une écriture. Si la carte graphique écrit dans la mémoire, le cache vérifie s'il y a conflit : si l'adresse écrite a été chargée dans le cache, celle-ci est invalidée et ne peut plus être accédée depuis le cache. Pour cela, notre cache contient un bit pour chaque ligne, qui indique si la donnée est invalide. Ce bit est mis à jour lors des écritures.

Cette technique peut être adaptée dans le cas où plusieurs mémoires de textures séparées existent sur une même carte graphique. Dans ce cas, les écritures doivent invalider toutes les copies dans tous les caches de texture. Cela nécessite d'ajouter des circuits qui propagent l'invalidation dans tous les autres caches. Ces circuits qui permettent de rendre les caches de textures cohérents sont toutefois très coûteux.

Autre solution : rendre le cache de texture accessible en écriture. Si un seul cache de texture est présent dans la carte graphique, il n'y a pas besoin de modifications supplémentaires. Mais s'il y en a plusieurs, le problème mentionné plus haut revient. Si une unité de texture modifie un texel dans le cache, les copies des autres caches doivent être invalidées. De plus, la mémoire cache qui a la bonne donnée doit fournir la bonne version de la donnée, quand les autres caches voudront la mettre à jour.

VI. Prefetching

Avec l'organisation telle qu'on l'a vue, l'accès aux textures est lent : plusieurs centaines de cycles d'horloges si on lit depuis la mémoire vidéo, et environ 20 cycles si on lit dans le cache. Pour améliorer la rapidité des accès, il est possible d'utiliser un prefetch de texture. Avec ce prefetch, la carte graphique peut préparer certaines lectures de texture à l'avance.

Un accès à une texture est composé d'un grand nombre de sous-étapes. Par exemple, cette série d'étapes pourrait être :

  • déterminer le niveau de mip-map ;
  • effectuer des calculs pour le filtrage anisotropique ;
  • calculer l'adresse effective en mémoire vidéo des texels à lire ;
  • accéder à la mémoire ;
  • filtrer les texels et les décompresser.

Ceci dit, il faut préciser qu'il s'agit là d'une description grossière : certaines étapes ont été omises. De plus, chacune de ces étapes peut prendre beaucoup de temps.

Le but du prefetching est d'effectuer à l'avance les étapes avant l'accès mémoire pour certaines requêtes de texture. Ainsi, pas besoin d'attendre qu'une lecture termine pour commencer à déterminer les mip-maps ou calculer l'adresse de la prochaine lecture.

Pendant qu'une lecture est en cours, il est possible de commencer à calculer les prochaines adresses à lire. Nos adresses de textures sont ainsi pré-calculées. Dès que la mémoire est libre, on peut directement envoyer les adresses pré-calculées à la mémoire et effectuer directement la lecture.

Les adresses pré-calculées, qui attendent la mémoire, sont stockées temporairement dans une petite mémoire tampon, en attendant que la mémoire vidéo soit libre. Cette mémoire tampon est ce qu'on appelle une mémoire FIFO. Cette mémoire est une mémoire dans laquelle les données sont stockées dans leur ordre d'arrivée. On peut écrire dedans et y lire. Lors d'une lecture, la donnée arrivée en dernier est renvoyée.

Ce prefetch peut s'implémenter de deux façons, suivant que la carte graphique utilise ou non un cache de texture.

Voici ce que cela donne sans cache :

Schéma de prefetching de texture

Si on rajoute des mémoires caches, la situation se complique. Avec un cache, une fois l'adresse calculée, il faut envoyer l'adresse à celui-ci. Si jamais la donnée à lire n'est pas dans le cache, la lecture est redirigée vers la mémoire d'attente (la FIFO).

Schéma prefetching avec caches

Navigation

Tutoriel précédent : rasterization   Sommaire   Tutoriel suivant : Render Output Target