Buffers GPU: Les Uniform Buffers Objects d’OpenGL 3.1

Sommaire:
1 – Introduction
2 – Un peu de code OpenGL
3 – Demo et Références

1 – Introduction

Les uniform buffer objects (ou UBO en abrégé) sont apparus avec OpenGL 3.1. La bible des uniform buffers se trouve ici: GL_ARB_uniform_buffer_object.

Les uniform buffers sont des zones mémoires allouées dans de la mémoire accessible au GPU (des GPU buffers) et permettent de transmettre des données de l’application hôte au programme GLSL. La mémoire accessible au GPU peut être la mémoire sur la carte graphique, ou de la mémoire système CPU (cas des GPU intégrés).

Le gros avantage d’utiliser des uniform buffers est qu’ils peuvent être partagés entre plusieurs programmes GLSL. Ainsi, un seul UBO est suffisant pour tous les shaders qui utilisent les mêmes données.

Du point de vue d’un shader GLSL, un uniform buffer est une zone mémoire en lecture seule (read only).

Prenons un exemple simple: imaginons que nous devions passer à un shader la position de la camera, de la lumière (une omni) et la composante diffuse de la lumière. Avec les variables uniforms classiques, nous aurions en entrée du shader:

uniform vec4 camera_position;
uniform vec4 light_position;
uniform vec4 light_diffuse;

Pour chaque shader ayant besoin de ces 3 variables, il faut mettre à jour les variables uniforms (avec des appels à glUniform4f() en OpenGL ou gh_gpu_program.uniform4f() avec GLSL Hacker. S’il n’y a qu’un shader, ç’est acceptable, pour 10 voire 100 shaders, c’est un peu plus génant. Surtout lorsque les variables changent souvent comme c’est le cas pour la position de la camera ou de la lumière.

Le buffer uniform est une solution à la fois élégante et efficace à ce problème. Au niveau application hôte (en C/C++), on va créer la structure de données suivante:

struct shader_data_t
{
  float camera_position[4];
  float light_position[4];
  float light_diffuse[4];
} shader_data;

On va ensuite créer un UBO à partir de cette structure de données. Une fois l’UBO créé, initialisé et bindé (comme pour n’importe quel buffer object d’OpenGL), on va pouvoir accéder à ce GPU buffer depuis un shader avec la déclaration suivante dans le code du shader:

#version 150
...
layout (std140) uniform shader_data
{ 
  vec4 camera_position;
  vec4 light_position;
  vec4 light_diffuse;
};
...
void main()
{
  ...
}

Dans ce bout de code, shader_data est ce que l’on appelle un uniform block (ou plus généralement un interface block comme on le verra dans un autre article).

Il suffit de mettre à jour une seule fois le GPU buffer et tous les shaders qui utilisent ce GPU buffer auront les données actualisées.

Chaque contexte de rendu OpenGL ne peut avoir qu’un nombre limité d’uniform buffers bindés en même temps. Par exemple avec une GeForce GTX 660, il est possible de binder 84 UBOs au maximum. Chaque GLSL shader (vertex, pixel, compute, tessellation ou geometry) peut avoir jusqu’à 14 uniform blocks pour une GTX 660.

Chaque UBO a également une taille limite. Avec la GTX 660, un UBO ne peut pas exéder 64ko ou 65536 octets.

Ces valeurs sont des limites liées à l’implémentation OpenGL et à la carte graphique en même temps. On peut interoger OpenGL pour les connaître avec la fonction glGetIntegerv():

GeForce GTX 660 limits:
- GL_MAX_UNIFORM_BUFFER_BINDINGS -> 84 
- GL_MAX_UNIFORM_BLOCK_SIZE -> 65536
- GL_MAX_VERTEX_UNIFORM_BLOCKS -> 14
- GL_MAX_FRAGMENT_UNIFORM_BLOCKS -> 14
- GL_MAX_GEOMETRY_UNIFORM_BLOCKS -> 14

Pour le GPU Intel HD Graphics 4600, nous avons:

Intel HD Graphics 4600 limits:
- GL_MAX_UNIFORM_BUFFER_BINDINGS -> 84 
- GL_MAX_UNIFORM_BLOCK_SIZE -> 16384
- GL_MAX_VERTEX_UNIFORM_BLOCKS -> 14
- GL_MAX_FRAGMENT_UNIFORM_BLOCKS -> 14
- GL_MAX_GEOMETRY_UNIFORM_BLOCKS -> 14

Et pour une Radeon HD 7970 d’AMD:

AMD Radeon HD 7970 limits:
- GL_MAX_UNIFORM_BUFFER_BINDINGS -> 75 
- GL_MAX_UNIFORM_BLOCK_SIZE -> 65536
- GL_MAX_VERTEX_UNIFORM_BLOCKS -> 15
- GL_MAX_FRAGMENT_UNIFORM_BLOCKS -> 15
- GL_MAX_GEOMETRY_UNIFORM_BLOCKS -> 15

2 – Un peu de code OpenGL

Nous allons rapidement voir comment créer et mettre à jour un uniform buffer.

Création et première initialisation:

GLuint gbo = 0;
glGenBuffers(1, &gbo);
glBindBuffer(GL_UNIFORM_BUFFER, gbo);
glBufferData(GL_UNIFORM_BUFFER, sizeof(shader_data), &shader_data, GL_DYNAMIC_COPY);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

Mise à jour: on récupère le pointeur GPU et on copie nos données.

glBindBuffer(GL_UNIFORM_BUFFER, gbo);
GLvoid* p = glMapBuffer(GL_UNIFORM_BUFFER, GL_WRITE_ONLY);
memcpy(p, &shader_data, sizeof(shader_data))
glUnmapBuffer(GL_UNIFORM_BUFFER);

Maintenant nous allons voir comment faire les connections entre un UBO et un programme GLSL. La première étape est de trouver l’index de l’uniform block représentant notre uniform buffer dans le shader:

unsigned int block_index = glGetUniformBlockIndex(program, "shader_data");

La deuxième étape consiste à connecter l’uniform block à l’uniform buffer. Cela se fait en spécifiant l’index du point de binding (ou point de liaison) de l’uniform buffer. Par défaut, quand on binde (ou active) un uniform buffer, ce dernier est attaché au binding point 0. On peut spécifier un autre point de binding et c’est même une opération obligatoire si l’on veut binder plusieurs buffers et y accéder depuis un shader.

La spécification d’un point de binding (ici l’index 2) se fait avec:

GLuint binding_point_index = 2;
glBindBufferBase(GL_UNIFORM_BUFFER, binding_point_index, gbo);

Nous aurions pu binder cet uniform buffer sur le point 23 ou meme 80. Le nombre maximal de points de binding se trouve via GL_MAX_UNIFORM_BUFFER_BINDINGS.
Pour la GeForce GTX 660, il y 84 points de binding possibles dans un context OpenGL. OpenGL maintient une sorte de tableau de pointeurs sur les différents uniform buffers. Chaque entrée de ce tableau est un point de binding. Pour la GTX 660, ce tableau a 84 éléments. Comme toujours, un petit dessin vaut mieux que 100 mots:

Maintenant que le buffer est bindé sur un certain binding point, on peut faire la connection entre le shader et cet uniform buffer:

GLuint binding_point_index = 2;
glUniformBlockBinding(program, block_index, binding_point_index);

Et voilà, la mécanique opengélistique (?) de base est en place: l’uniform buffer est crée et contient des données et l’on y accéder en lecture depuis le shader.

3 – Demo et références

J’ai préparé une petite demo avec GLSL Hacker. Cette demo affiche un quad texturé via un shader. Rien de particulier. Cela devient plus intéressant si l’on regarde comment les matrices de la camera sont transmises au shader: en utilisant un uniform buffer. Cette demo est disponible dans le répertoire host_api/gl-310-arb-uniform-buffer/ du demopack.

Dans GLSL Hacker 0.7.0+, j’ai ajouté une nouvelle librairie Lua / Python appellée gh_gpu_buffer. Cette lib de bas niveau permet de gérer tous les types de GPU buffers y compris, bien sur, les uniform buffers.

Références:
Shared Uniforms
GLSL Core Tutorial – Uniform Blocks