gKit2 light
openGL 4.3 : multi draw indirect

cf tuto_mdi.cpp + indirect.glsl

Les fonctions d'affichage classiques, cf glDraw( ) et la famille, sont relativement efficaces pour dessiner quelques objets, glDrawInstanced( ) permet d'afficher de nombreuses copies du même objet, mais openGL 3.3 ne fournit pas de méthode pour afficher un grand nombre d'objets différents.

En effet, la préparation de chaque draw nécessite pas mal de travail au driver openGL, et c'est finalement le cpu qui limite le nombre d'objets affichables, alors que la carte graphique est sous exploitée. Le tuto mesure du temps cpu et gpu présente une méthode pour mesurer le temps cpu utilisé pour préparer N commandes d'affichage pour la carte graphique, en comparant les solutions précédentes : N commandes ou 1 commande instanciée avec N copies.

openGL 4.3 a introduit de nouvelles variantes de draw : glMultiDrawArraysIndirect( ) et glMultiDrawElementsIndirect() qui permettent de décrire les paramètres d'affichage de plusieurs objets. Le gain d'efficacité se trouve du coté du driver qui ne réalise qu'une seule préparation et soumet toutes les commandes d'affichage à la carte graphique en une seule fois.

Le principe est relativement simple, les paramètres de chaque commande d'affichage sont décrits par une structure, et un tableau de structures est passé en paramètre à MultiDraw().

// parametres pour MultiDrawArraysIndirect
struct ArraysParam
{
unsigned vertex_count;
unsigned instance_count;
unsigned vertex_base;
unsigned instance_base;
};

cette structure permet de décrire les paramétres de glDrawArrays(), glDrawArraysInstanced|BaseInstance(). En gros, voila ce que fait MultiDrawArraysIndirect( ) :

MultiDrawArraysIndirect( GLenum primitives, std::vector<ArraysParam>& params )
{
for(int i= 0; i < params.size(); i++)
if(params[i].instance_count > 0)
// draw instancie
glDrawArraysInstancedBaseInstance(primitives, params[i].vertex_base, params[i].vertex_count,
params[i].instance_count, params[i].instance_base);
else
// draw normal
glDrawArray(primitives, params[i].vertex_base, params[i].vertex_count);
}

Pour glDrawElements(), glDrawElementsInstanced(), y compris les variantes permettant de préciser les indices de la première instance et du premier sommet, cf glDraw( ) et la famille glDrawElementsBaseVertex() et glDrawElementsInstancedBaseVertexBaseInstance() (!!) la structure est :

// parametres pour MultiDrawElementsIndirect
struct ElementsParam
{
unsigned index_count;
unsigned instance_count;
unsigned first_index; // cf parametre offset de glDrawElements()
unsigned vertex_base;
unsigned instance_base;
};

Dernier détail, le tableau de structures doit être dans un buffer sélectionné sur GL_DRAW_INDIRECT_BUFFER.

Un code d'exemple direct est disponible dans tuto_mdi_elements.cpp + indirect_elements.glsl. il permet de dessiner tous les groupes d'un objet indexé en utilisant glMultiDrawElementsIndirect() en remplissant un tableau de paramètres ElementsParam.

Un code d'exemple plus complet tuto_mdi.cpp + indirect.glsl transfere un tableau de ~1000 objets associés à une transformation model et permet de comparer le temps d'exécution des 2 solutions : classique, 1 draw par objet et 1 multi draw pour l'ensemble d'objet. Voici les temps mesurés :

Comme prévu, l'utilisation du gpu ne change pas trop, la différence se trouve du cote du driver et de l'application...

L'utilisation la plus interressante de MultiDraw() est de remplir le buffer directement sur la carte graphique avec un shader. Le (compute) shader peut, par exemple, faire quelques tests de visibilité et déterminer s'il est nécessaire de dessiner chaque objet. Le nombre d'objets à dessiner se trouvera aussi dans un buffer. Les tests de visibilité seront plus rapides que si l'application avait fait tous les tests, et le buffer est déjà sur la carte graphique, pret à etre utilisé par MultiDraw(). Alors que si l'application fait les tests, il faut aussi transférer les résultats dans un buffer à chaque affichage.

Mais, dans ce cas, un problème se pose : il faut connaitre, dans l'application, le nombre de commandes / d'objets à dessiner pour utiliser MultiDraw( ), lorsque c'est un shader qui a construit le buffer, cette information n'est pas directement disponible pour l'application. bien sur, on peut relire le compteur dans l'application avec un glGetBufferData(), mais cette solution à plusieurs inconvénients : 1/ relire un buffer est assez lent et 2/ plus subtil, on force la carte graphique à attendre que l'application relise le compteur avant d'appeller MultiDraw().

La solution est fournie par une variante de MultiDraw() : glMultiDrawArraysIndirectCountARB( ). Le nombre de commandes / d'objets à dessiner se trouve dans (une variable d) un buffer sélectionné sur GL_PARAMETER_BUFFER_ARB. Cette fonction n'est pas encore standard, mais elle est disponible sur toutes les cartes graphiques. C'est l'extension GL_ARB_indirect_parameters qui permet de l'utiliser.

cf tuto_mdi_elements_count.cpp pour un exemple direct qui ne dessine pas tous les groupes d'un objet. c'est une petite modification de l'exemple précédent tuto_mdi_elements.cpp.

cf tuto_mdi_count.cpp + indirect_cull.glsl + indirect_remap.glsl pour un exemple complet, avec un compute shader qui décide de dessiner un objet, ou pas, et qui comptabilise les objets à dessiner par glMultiDrawArraysIndirectCountARB( ).

remarque : vérifier que l'extension est bien disponible avant de l'utiliser, sinon c'est segfault, la librairie GLEW utilisée par gKit permet d'écrire directement le test

#include "glcore.h" // deja inclut par window.h, app.h, etc.
init( ) :
// verifier la presence de l'extension
if(GLEW_ARB_indirect_parameters == 0)
// erreur, extension non disponible
return -1;
int init(std::vector< const char * > &options)
Definition: shader_kit.cpp:92

organisation des données

Le seul problème de la famille MultiDraw() est la nécessité d'organiser les données : par exemple, tous les objets que l'on souhaite dessiner avec un appel de MultiDraw() doivent être décrits par les mêmes buffers et le même VAO... toutes les textures nécessaires doivent être sélectionnées, les transformations, les uniforms, etc.

La variable gl_DrawIDARB de GLSL numérote les commandes, ce qui permet de retrouver toutes les informations de l'objet, mais il faut les organiser sous cette forme. En gros, les paramètres uniform deviennent des tableaux, indexés par gl_DrawIDARB. En pratique on utilisera des uniform buffers ou des storage buffers pour ne pas être bloqué par la limite de taille des uniforms.

De même, il faut concaténer les attributs des objets dans un (gros) buffer et configurer le vao correspondant. L'objectif n'est pas de dessiner toute la scène avec un seul MultiDraw(), mais de limiter leur nombre à une valeur raisonnable. Par exemple, les objets animés ne seront pas transformés par le même vertex shader que les objets statiques, les objets opaques ne seront pas dessinés avec le même fragment shader que les objets transparents... et comme il n'est pas possible de changer de shader program objet par objet avec MultiDraw(), il faudra nécessairement plusieurs appels pour afficher ces différents types d'objets.

Voici le vertex shader utilisé par l'exemple, cf indirect.glsl :

#extension GL_ARB_shader_draw_parameters : require
// necessaire pour utiliser gl_DrawIDARB
layout(location= 0) in vec3 position;
uniform mat4 vpMatrix; // projection * view
uniform mat4 model[1000]; // model
void main( )
{
// recupere la transformation model de l'objet et transforme le sommet...
gl_Position= vpMatrix * model[gl_DrawIDARB] * vec4(position, 1);
}
vecteur generique, utilitaire.
Definition: vec.h:146
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition: vec.h:168

remarque : la variable gl_DrawID n'est pas encore inclue dans une version standard d'openGL, il faut donc utiliser une extension GLSL. Il suffit d'ajouter, dans le source du shader, une ligne donnant le nom de chaque extension nécessaire :

#extension GL_ARB_shader_draw_parameters : require

Pour etre complet, il faut aussi vérifier la présence de l'extension openGL dans l'init de l'application :

#include "glcore.h" // deja inclut par window.h, app.h, etc.
init( ) :
// verifier la presence de l'extension
if(GLEW_ARB_shader_draw_parameters == 0)
// erreur, extension non disponible
return -1;