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);

    glDispatchCompute( ... );

    // 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...