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 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 ?std430... ce sera plus simple pour écrire
l'applicationinit() :
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...
...
glUseProgram(program);
{ ... }
glDispatchCompute(n, 1, 1);
//
attendre que les resultats soient disponibles
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
// relire
le resultat
std::vector<int> tmp(n);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer_sortie);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(int) *
tmp.size(), tmp.data());
// afficher le
resultat
for(unsigned i=
0; i < tmp.size(); i++)
printf("%d ", tmp[i]);
printf("\n");
rappel : utilisez les décorationsvoid 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;
}
readonly, writeonly
pour déclarer les buffers en lecture seule, ou en écriture seule...v
(par exemple) dans un tableau résultat. rappel : operations atomiques... cf la doc de glsl sur les buffers, et celle sur les compute shadersvoid 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]);
}
atomicAdd(n, 1) simultanement
sur la variable n... et ca va être assez lent. 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...
exercice 8 :
reprennez le tp du cours précédent qui teste la visibilité d'un
bloc / d'une région avant de la dessiner.
c'est dommage de faire calculer tout ça sur un seul thread du
processeur, non ?
écrivez le test de visibilité d'une boite englobante alignée sur
les axes dans un compute shader.
bien sur le shader testera en parallèle toutes les régions de la
scène. ie il faudra un tableau / buffer en entrée et un tableau
(storage) buffer en sortie.
le résultat 0 ou 1, visible ou pas, sera écrit dans un buffer /
tableau d'entiers.
récupérez ce (storage) buffer dans l'application (cf
glGetBufferSubData()) et vérifiez que vous obtenez bien les mêmes
résultats que le test du tp précédent.
on peut bien sur récupérer le résultat du test de visibilité et
dessiner ensuite les blocs / régions visibles, mais c'est quand
même dommage de créer une synchronisation entre l'application et
le gpu. de plus, le gpu doit maintenant attendre (longtemps) de
recevoir les draws émis par l'application.
il serait quand même beaucoup plus efficace de pouvoir dessiner
les régions directement, sans re-passer par l'application...
(re-)lisez la doc de multidraw indirect, cette fonctionnalité va permettre de faire exactement ça ! (après un peu de préparation bien sur...)
l'idée est de construire un buffer contenant les paramètres du
draw de chaque région à dessiner et d'utiliser glMultiDrawIndirect()
pour tout dessiner d'un coup. un compute shader prend en
entrée les paramètres des draws des régions ainsi que leurs
englobants, teste la visibilite de l'englobant et copie dans un
autre buffer les paramètres de draw des régions visibles.
exercice 9 :
pour démarrer en douceur, on va tricher un peu : au lieu de ne
copier que les paramètres des régions visibles, on peut simplement
mettre à zero le nombre d'instances à dessiner lors de la copie.
exercice 10 :
maintenant que la première solution fonctionne, modifiez le
compute shader pour ne copier que les paramètres des régions
visibles.
c'est malin, comment on dessine maintenant ? l'application ne
connait pas le nombre de draws à réaliser ! utilisez glMultiDrawIndirectCountARB()
qui est faite exactement pour ça !
exercice 11 :
il serait sans doute plus rapide d'éviter de faire autant
d'opérations atomiques... décomposez l'addition en utilisant un
compteur en mémoire partagée.
bravo ! vous venez d'écrire votre premier "gpu driven
pipeline" ! ie c'est un shader qui décide quoi dessiner sans
intervention de l'application...
la version moderne de ce test s'écrit avec un mesh shader, ce qui
évite d'écrire les paramètres des draws dans un buffer et qui va
encore plus vite (plus besoin de synchronisation...). vous pouvez
regarder les perfs avec ou sans dans Alan Wake 2 : ici
chez TechPowerUp, par exemple.
malheureusement, l'extension
openGL permettant d'écrire des mesh shaders n'est pas encore
disponible dans les drivers standards. ce sera le cas l'année
prochaine normalement (enfin !!). l'extension propriétaire nvidia
pour les geforce est disponible par contre.