Sommaire:
1 – Introduction
2 – Un peu de code OpenGL
3 – Démo et Références
1 – Introduction
Dans cet article, nous avons fait connaissance avec les Uniform Buffer Objects (ou UBO pour les intimes). Pour résumer, les UBOs sont des zones mémoire GPU accessible en lecture seule au niveau d’un shader GLSL. La taille d’un UBO est relativement limitée: 64ko pour les GPUs NVIDIA et AMD et 16ko pour les GPUs Intel.
Petite taille, lecture seule, humm… pas terrible tout ça. Avec les tonnes de gigaoctets de memoire disponibles sur les cartes graphiques modernes, on doit pouvoir faire mieux que d’allouer 64ko!
Les Shader Storage Buffer Objects (ou SSBO) peuvent être vu comme la version no-limit des UBOs: ils sont accessible en lecture ET ecriture dans un shader et leur taille semble n’être limitée que par la quantité de mémoire accessible au GPU. Sur une GeForce GTX 660, il est possible d’allouer un SSBO de 2Go. La grande classe quoi!
Le seul inconvéniant des SSBO c’est… Mac OS X. La dernière mouture d’OS X, Mavericks, ne supporte qu’OpenGL 4.1. Donc pas de SSBO pour l’OS à la pomme avant au moins une décénnie 😉 Dommage pour les apps cross-plateformes comme GLSL Hacker.
Sinon pas de soucis pour Windows ou Linux (sous Linux avec les pilotes propriétaires bien sur).
La bible des SSBO se trouve ici: GL_ARB_shader_storage_buffer_object.
2 – Un peu de code OpenGL
La gestion des SSBOs ressemble beaucoup à celle des UBOs à quelques petits détails près.
Reprenons notre petite structure de données, déjà utilisée dans l’article sur les UBOs:
struct shader_data_t { float camera_position[4]; float light_position[4]; float light_diffuse[4]; } shader_data;
Création et initialisation d’un SSBO:
GLuint ssbo = 0; glGenBuffers(1, &ssbo); glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(shader_data), &shader_data, GL_DYNAMIC_COPY); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
Mise à jour: on récupère le pointeur GPU et on copie nos données.
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); GLvoid* p = glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_WRITE_ONLY); memcpy(p, &shader_data, sizeof(shader_data)) glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
Tout comme pour les UBOs, il existe un équivalent aux uniform blocks dans les shaders GLSL. Pour les SSBOs, nous avons un storage block dans un shader. Le storage block décrit la structure de données accessible au shader:
#version 430 ... layout (std430, binding=2) buffer shader_data { vec4 camera_position; vec4 light_position; vec4 light_diffuse; }; ... void main() { ... }
Le mot clé uniform est remplacé par buffer qui montre en même temps le caractère général du buffer: zone de mémoire en read/write.
Et comme avec les UBOs, OpenGL maintient une table de points de laison (binding points) par contexte de rendu. Pour une GTX 660, cette table possède 96 entrées:
Chaque type de shader (vertex, fragment, geometry, tessellation et compute) peut avoir jusqu’à 16 storage blocks pour une GTX 660.
Pour accéder à un SSBO depuis un shader il faut faire un minimum de préparation:
1 – trouver l’index du storage block du shader:
GLunit block_index = 0; block_index = glGetProgramResourceIndex(program, GL_SHADER_STORAGE_BLOCK, "shader_data");
2 – connecter le storage block du shader au SSBO: on dit au shader sur quel binding point se trouve le SSBO. Dans notre cas, on binde le SSBO sur le point 2:
GLuint ssbo_binding_point_index = 2; glShaderStorageBlockBinding(program, block_index, ssbo_binding_point_index);
Cette denière opération n’est pas necéssaire. En effet, le binding point peut être spécifié directement dans le shader au niveau du layout du buffer:
layout (std430, binding=2) buffer shader_data { ... }
glShaderStorageBlockBinding() permet de redéfinir dynamiquement l’endroit où se trouve le shader storage buffer à utiliser au niveau du shader. Par exemple, pour indiquer au shader d’utiliser le storage buffer bindé sur le point 80:
glShaderStorageBlockBinding(program, block_index, 80);
Et comme avec les UBOs, le binding d’un SSBO sur un point particulier se fait avec:
GLuint binding_point_index = 80; glBindBufferBase(GL_SHADER_STORAGE_BUFFER, binding_point_index, ssbo);
Pour terminier, voilà quelques valeurs limites que l’on peut récupérer avec glGetIntegerv():
NVIDIA GeForce GTX 660 limits (R337.50): GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = 96 GL_MAX_SHADER_STORAGE_BLOCK_SIZE = 2147483647 GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = 16 GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS = 16 GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS = 16 GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS = 16 GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS = 16 GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS = 16 GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS = 96
AMD Radeon HD 7970 limits (Catalyst 14.4): GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = 16 GL_MAX_SHADER_STORAGE_BLOCK_SIZE = 16777216 GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = 16 GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS = 16 GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS = 16 GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS = 16 GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS = 16 GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS = 16 GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS = 16
Je me demande si les valeurs sont correctes pour AMD (GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS ou GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS…).
3 – Demo et références
J’ai préparé deux demos qui exploitent les SSBOs. La première, toute simple, utilise un SSBO pour transmettre les matrices de la caméra au shader. Cette demo est disponible dans le répertoire host_api/gl-430-arb-shader-storage-buffer-object/ du demopack de GLSL Hacker.
La seconde demo montre une utilisation en lecture/ecriture des SSBOs. Il s’agit de controller la position d’un grand nombre de particules en utilisant un compute shader. La position des particules est stockée dans un SSBO. Le compute shader fait des accès en lecture et écriture dans le SSBO. Cette demo est disponible dans le répertoire host_api/gl-430-arb-compute-shader_Particles_SSBO/ demopack.