M2 Image
TP4 - compute shaders
Partie 1 : préparation - compute shaders, storage buffers
pour démarrer en douceur, on va se concentrer sur les nouveautés
techniques : les "shader storage buffers"... ce sont des buffers
comme les autres (ils sont crées avec glBufferData()) mais un shader
peut écrire une valeur dans ces buffers (d'ou leur nom...)
relisez la doc,
si ce n'est pas déjà fait.
exercice 1 : écrivez un compute shader débile qui ajoute 10
à chaque case d'un tableau.
void ajoute( std::vector<int>& entree )
{
for(unsigned i= 0; i <
entree.size(); i++)
entree[i]= entree[i] + 10;
}
à partir de quelle taille de tableau un compute shader
peut s'exécuter efficacement ?
quelle taille de groupe de threads utiliser ?
rappel : n'oubliez pas de déclarer les buffers avec la
décoration std430
... ce sera plus simple pour écrire
l'application
exercice 2 : que doit faire l'application pour exécuter le
shader précédent ?
indication : comme toutes les fonctions, un shader doit lire
ses entrées et écrire ses résultats quelquepart...
indications : votre programme de test va ressembler à :
init() :
charger le compute shader et créer le
program, cf program= read_program( ... ) avec #ifdef COMPUTE_SHADER /
#endif
creer un buffer pour les donnees, cf glBufferData(GL_SHADER_STORAGE_BUFFER, ...);
render( ) :
glUseProgram(program);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffer);
glDispatch
Compute
( ... );
// attendre les resultats
glMemoryBarrier( ... );
// relire et verifier...
...
exercice 3 : comment vérifier que ca marche ?
on peut relire le contenu d'un buffer avec glGetBufferSubData(); et
écrire une fonction de vérification dans l'application.
attention : il faut attendre que l'exécution du shader soit
terminée avant de relire le contenu du buffer dans l'application,
relisez la section "attendre les résultats" dans la doc
sur les compute shaders.
exercice 4 : on va faire un truc très compliqué,
modifiez le shader pour que 10 soit un paramètre fixé par
l'application...
rappel : paramètres uniform des shaders...
exercice 5 : modifiez le shader et l'application pour que les
résultats soient écrits dans un tableau / buffer résultat, différent
du tableau / buffer en entrée.
void ajoute( const int v, const
std::vector<int>& entree, std::vector<int>&
sortie )
{
sortie.resize(entree.size());
for(unsigned i= 0; i <
entree.size(); i++)
sortie[i]= entree[i] + v;
}
rappel : utilisez les décorations readonly, writeonly
pour déclarer les buffers en lecture seule, ou en écriture seule...
indication : oui, il faudra créer un buffer pour stocker les résultats du shader.
exercice 6 : écrivez un autre shader qui filtre ses entrées,
ie il n'écrit que les nombres plus grands qu'une valeur v
(par exemple) dans un tableau résultat.
qu'est ce qui change ? le shader ne connait pas à l'avance la place
du résultat dans le tableau / buffer de sortie.
void filtre( const int v, const
std::vector<int>& entree,
std::vector<int>&
sortie
)
{
sortie.clear();
for(unsigned i= 0; i <
entree.size(); i++)
if(entree[i] > v)
sortie.push_back(entree[i]);
}
rappel : operations atomiques... cf la doc
de glsl sur les buffers, et celle sur les compute
shaders
pour les rapides :
exercice 7 : limiter le nombre d'opérations atomiques
simultanées sur le même compteur...
la solution de l'exercice précédent fonctionne mais : tous les threads vont exécuter atomicAdd(n, 1)
simultanement sur la variable n... et ca va être assez lent.
indication : on peut aussi utiliser des compteurs en mémoire
partagée, (ie declarés avec la décoration shared
),
mais la mémoire partagée n'est visible / partagée que par les
threads d'un même groupe, pas par tous les groupes... par contre, si
on pouvait ne faire qu'un seul atomicAdd(n, ...) par groupe, on
accelererait pas mal le shader...