gKit2 light
|
cf tuto_storage.cpp, storage.glsl et tuto_storage_buffer.cpp
Les uniforms déclarés dans les shaders permettent de manipuler des tableaux de structures de manière assez directe, mais leur taille est très limitée (32Ko ou 64Ko), ce qui les rend assez peu pratiques.
Mais il est possible d'associer un buffer à un block d'uniforms et d'éliminer la limite de taille. La déclaration dans le shader ressemble à celle d'une structure :
Autre fonctionnalité interressante, les shaders peuvent écrire dans ces buffers. Il est recommandé de préciser l'utilisation du contenu du buffer : lecture seule, écriture seule ou mixte, avec les mots-clés readonly
, writeonly
, et coherent
en général, pour les accès mixtes. Pour que l'application puisse associer un buffer à la déclaration dans le shader, il faut nommer le block, cf vertexData
dans l'exemple précédent. Et comme il est possible d'utiliser plusieurs storage buffers, ils sont numérotés.
L'application peut utiliser le nom du block pour connaître son identifiant, cf glGetProgramResourceIndex()
et lui associer un numéro de block, cf glShaderStorgeBlockBinding()
et enfin, un buffer, cf glBindbufferBase()
:
En pratique, il est plus simple de déclarer le numéro du block dans le shader avec layout(binding= xxx)
, ce qui permet à application d'associer un buffer au block directement avec glBindBufferBase(GL_SHADER_STORAGE_BUFFER, xxx, buffer)
, sans utiliser glShaderStorageBlockBinding( )
:
ce qui permettra, dans l'application, de créer un buffer contenant les données et de le sélectionner sur GL_SHADER_STORAGE_BUFFER
numero 0 :
attention : glBindBufferBase()
sélectionne le buffer à la fois sur GL_SHADER_STORAGE_BUFFER
et sur la version numérotée GL_SHADER_STORAGE_BUFFER 0
.
L'utilisation des storage buffers est assez directe, mais il y a une grosse différence par rapport aux uniforms classiques, il n'y a pas d'interface (cf glUniform())
permettant d'affecter des valeurs aux variables déclarées dans le block. Il faut le faire "à la main" en remplissant correctement le buffer qui sera associé au block. Un cpu et un gpu sont des processeurs conçus différemment, et ils n'accèdent pas à la mémoire de la même manière... Il faut donc placer les données au bon endroit dans le buffer pour que les shaders relisent correctement toutes les valeurs. Ce n'est pas très compliqué, voici quelques exemples, pour mieux comprendre les diférences.
Quelle est l'organisation mémoire d'une structure Triangle
pour un cpu ?
offsetof(type, field)
du c++ renvoie la position en octets d'un champ à l'intérieur d'une structure :
ce qui correspond à :
Les types de base int
et float
du c++ occupent 4 octets et les structures vec3
sont rangées les unes à la suite des autres :
Pour un shader, la structure triangle est organisée différement :
ce qui correspond à cette organisation mémoire :
Les vec3
occupent la place de 4 float
, soit 16 octets. De manière générale, dans un shader :
int
, float
et bool
,float
pour les vec2
, ou 2x type de base pour ivec2, uvec2 et bvec2,float
pour les vec3
, ou 4x type de base pour ivec3, uvec3 et bvec3float
pour les vec4
, ou 4x type de base pour ivec4, uvec4 et bvec4Par défaut les éléments d'un tableau sont alignés sur 16 octets, quelque soit le type de l'élement. un int t[1]
ou int t[2]
occupent 16 octets dans un buffer... Mais il est aussi possible de choisir un alignement un peu plus économique en mémoire, il faut décorer la déclaration avec layout(std430)
, les éléments d'un tableau sont alignés normalement, en fonction de leur type, mais la taille de la structure doit être un multiple de son membre le plus important, pour que toutes les variables du tableau restent alignées correctement pour le gpu.
triangles[0].a
et triangle[1].a
doivent être tous les deux alignés sur un multiple de 16 octets (4xfloat
), donc la taille de la structure doit être aussi un multiple de 16.
Question pratique, comment remplir simplement un buffer avec les variables organisées correctement pour les shaders, et comment vérifier que tout est correct ?
Une solution directe consiste tout simplement à ne pas utiliser de vecteurs à 3 composantes. Dans le cas général, ce n'est pas suffisant, il peut être nécessaire de déclarer une ou plusieurs variables supplémentaires dans la structure pour respecter l'alignement gpu :
autre exemple :
La taille de la structure Data
est de 4x4 + 4x2 = 24 octets. Pour que toutes les cellules du tableau data[]
soient alignées correctement, il faut que la taille de la structure Data soit elle aussi un multiple de 16 (alignement nécessaire pour le champ vec4 a
), il faut donc ajouter 2 float supplémentaires pour que data[0].a et data[1].a restent alignés sur un multiple de 16.
cf tuto_storage_buffer.cpp pour une version plus complète.
Les compilateurs C et C++ permettent de modifier l'alignement des variables en c++11 avec : alignas(n)
avec n
l'alignement d'une variable en octets.
on peut donc déclarer une structure glsl::vec3, compatible avec les shaders :
et l'utiliser directement :
cf tuto_storage_buffer.cpp pour une version plus complète qui aligne le block suivant :
La solution directe consiste à préparer un std::vector
d'une structure utilisant des membres alignés et à allouer le buffer :
En cas de doute sur l'organisation mémoire d'un block, comment connaître la position des variables ? openGL 4.3 a introduit 2 fonctions glGetProgramInterface( ) / glGetProgramResource( )
permettant d'énumérer les blocks associés à des buffers et de récupérer l'organisation mémoire des variables déclarées dans ces blocks. Comme d'habitude, il faut commencer par récupérer le nombre de blocks et le nombre de variables du block afin de pouvoir itérer sur les variables.
cf tuto_storage.cpp pour une version plus complète, qui récupère aussi le nom, le type de chaque variable ainsi que l'organisation des tableaux et des matrices.