web-dev-qa-db-fra.com

Passer une liste de valeurs pour fragmenter le shader

Je veux envoyer une liste de valeurs dans un fragment shader. Il s'agit peut-être d'une longue liste (de plusieurs milliers d'éléments) de flotteurs à simple précision. Le fragment shader a besoin d'un accès aléatoire à cette liste et je veux rafraîchir les valeurs du CPU sur chaque trame.

J'examine mes options sur la façon dont cela pourrait être fait:

  1. Comme variable uniforme de type tableau ("float uniforme x [10];"). Mais il semble y avoir des limites ici, l'envoi de plus de quelques centaines de valeurs par mon GPU est très lent et je devrais également coder en dur la limite supérieure dans le shader quand je préfère changer cela en runtime.

  2. En tant que texture avec la hauteur 1 et la largeur de ma liste, puis actualisez les données à l'aide de glCopyTexSubImage2D.

  3. Autres méthodes? Je n'ai pas suivi tous les changements dans la spécification GL récemment, peut-être y a-t-il une autre méthode spécialement conçue à cet effet?

67
Ville Krumlinde

Il existe actuellement 4 façons de le faire: textures 1D standard, textures de tampon, tampons uniformes et tampons de stockage de shader.

Textures 1D

Avec cette méthode, vous utilisez glTex(Sub)Image1D pour remplir une texture 1D avec vos données. Puisque vos données ne sont qu'un tableau de flottants, votre format d'image doit être GL_R32F. Vous y accédez ensuite dans le shader avec un simple appel texelFetch. texelFetch prend les coordonnées Texel (d'où le nom), et ferme tout filtrage. Vous obtenez donc exactement un Texel.

Remarque: texelFetch est 3.0+. Si vous souhaitez utiliser les versions antérieures GL, vous devrez passer la taille au shader et normaliser manuellement les coordonnées de la texture.

Les principaux avantages ici sont la compatibilité et la compacité. Cela fonctionnera sur GL 2.1 (en utilisant la notation). Et vous n'avez pas oui pour utiliser GL_R32F formats; vous pourriez utiliser GL_R16F demi-flotteurs. Ou GL_R8 si vos données sont raisonnables pour un octet normalisé. La taille peut signifier beaucoup pour les performances globales.

Le principal inconvénient est la limitation de taille. Vous êtes limité à avoir une texture 1D de la taille de texture maximale. Sur GL matériel de classe 3.x, ce sera autour de 8192, mais il est garanti qu'il ne sera pas inférieur à 4096.

Objets tampons uniformes

La façon dont cela fonctionne est que vous déclarez un bloc uniforme dans votre shader:

layout(std140) uniform MyBlock
{
  float myDataArray[size];
};

Vous accédez ensuite à ces données dans le shader comme un tableau.

De retour dans le code C/C++/etc, vous créez un objet tampon et le remplissez de données à virgule flottante. Ensuite, vous pouvez associer cet objet tampon au bloc uniforme MyBlock. Plus de détails peuvent être trouvés ici.

Les principaux avantages de cette technique sont la vitesse et la sémantique. La vitesse est due à la façon dont les implémentations traitent les tampons uniformes par rapport aux textures. Les récupérations de texture sont des accès à la mémoire globale. Les accès uniformes à la mémoire tampon ne le sont généralement pas; les données de tampon uniformes sont généralement chargées dans le shader lorsque le shader est initialisé lors de son utilisation dans le rendu. De là, c'est un accès local, ce qui est beaucoup plus rapide.

Sémantiquement, c'est mieux car ce n'est pas seulement un tableau plat. Pour vos besoins spécifiques, si tout ce dont vous avez besoin est un float[], ça n'a pas d'importance. Mais si vous avez une structure de données plus complexe, la sémantique peut être importante. Par exemple, considérons un ensemble de lumières. Les lumières ont une position et une couleur. Si vous utilisez une texture, votre code pour obtenir la position et la couleur d'une lumière particulière ressemble à ceci:

vec4 position = texelFetch(myDataArray, 2*index);
vec4 color = texelFetch(myDataArray, 2*index + 1);

Avec des tampons uniformes, il ressemble à tout autre accès uniforme. Vous avez nommé des membres qui peuvent être appelés position et color. Donc, toutes les informations sémantiques sont là; il est plus facile de comprendre ce qui se passe.

Il existe également des limitations de taille. OpenGL requiert que les implémentations fournissent au moins 16 384 octets pour la taille maximale des blocs uniformes. Ce qui signifie que pour les tableaux flottants, vous n'obtenez que 4 096 éléments. Notez à nouveau qu'il s'agit du minimum requis des implémentations; certains matériels peuvent offrir des tampons beaucoup plus volumineux. AMD fournit 65 536 sur son matériel de classe DX10, par exemple.

Textures tampons

Ce sont en quelque sorte une "texture super 1D". Ils vous permettent effectivement de accéder à un objet tampon à partir d'une unité de texture . Bien qu'ils soient unidimensionnels, ce ne sont pas des textures 1D.

Vous ne pouvez les utiliser que depuis GL 3.0 ou supérieur. Et vous ne pouvez y accéder que via la fonction texelFetch.

Le principal avantage ici est la taille. Les textures de tampon peuvent généralement être assez gigantesques. Bien que la spécification soit généralement conservatrice, exigeant au moins 65 536 octets pour les textures de tampon, la plupart des implémentations GL leur permettent de varier dans le mégaoctets de taille. En effet, la taille maximale est généralement limitée par la mémoire GPU disponible et non par les limites matérielles.

De plus, les textures tampons sont stockées dans des objets tampons, pas les objets de texture plus opaques comme les textures 1D. Cela signifie que vous pouvez utiliser certaines techniques de streaming d'objet tampon pour les mettre à jour.

Le principal inconvénient ici est la performance, tout comme avec les textures 1D. Les textures tampons ne seront probablement pas plus lentes que les textures 1D, mais elles ne seront pas aussi rapides que les UBO. Si vous n'en tirez qu'un flotteur, cela ne devrait pas vous inquiéter. Mais si vous en tirez beaucoup de données, pensez à utiliser un UBO à la place.

Objets de tampon de stockage de shader

OpenGL 4.3 fournit une autre façon de gérer cela: tampons de stockage de shader . Ils ressemblent beaucoup à des tampons uniformes; vous les spécifiez en utilisant une syntaxe presque identique à celle des blocs uniformes. La principale différence est que vous pouvez leur écrire. Évidemment, ce n'est pas utile pour vos besoins, mais il y a d'autres différences.

Les tampons de stockage de shader sont, conceptuellement parlant, une autre forme de texture de tampon. Ainsi, les limites de taille pour les tampons de stockage de shader sont a beaucoup plus grandes que pour les tampons uniformes. Le minimum OpenGL pour la taille maximale UBO est de 16 Ko. Le minimum OpenGL pour la taille max SSBO est 16 Mo . Donc, si vous avez le matériel, c'est une alternative intéressante aux UBO.

Assurez-vous simplement de les déclarer comme readonly, puisque vous ne leur écrivez pas.

L'inconvénient potentiel ici est à nouveau la performance, par rapport aux UBO. Les SSBO fonctionnent comme une opération de chargement/stockage d'image à travers des textures de tampon. Fondamentalement, c'est du sucre syntaxique (très agréable) autour d'un type d'image imageBuffer. En tant que tel, les lectures à partir de celles-ci s'exécuteront probablement à la vitesse des lectures à partir d'un readonly imageBuffer.

Si la lecture via le chargement/stockage d'images via des images tampons est plus rapide ou plus lente que les textures de tampon, il n'est pas clair à ce stade.

Un autre problème potentiel est que vous devez respecter les règles pour accès à la mémoire non synchrone . Ceux-ci sont complexes et peuvent très facilement vous faire trébucher.

127
Nicol Bolas

Cela ressemble à un cas d'utilisation de Nice pour objets de tampon de texture . Celles-ci n'ont pas grand-chose à voir avec les textures régulières et vous permettent essentiellement d'accéder à la mémoire d'un objet tampon dans un shader comme un simple tableau linéaire. Ils sont similaires aux textures 1D, mais ne sont pas filtrés et ne sont accessibles que par un index entier, ce qui ressemble à ce que vous devez faire lorsque vous l'appelez une liste de valeurs. Et ils prennent également en charge des tailles beaucoup plus grandes que les textures 1D. Pour le mettre à jour, vous pouvez ensuite utiliser les méthodes d'objet tampon standard (glBufferData, glMapBuffer, ...).

Mais d'un autre côté, ils nécessitent du matériel GL3/DX10 pour être utilisés et ont même été intégrés dans OpenGL 3.1, je pense. Si votre matériel/pilote ne le prend pas en charge, votre deuxième solution serait la méthode de choix, mais utilisez plutôt une texture 1D plutôt qu'une texture largeur x 1 2D). Dans ce cas, vous pouvez également utiliser une texture 2D non plate et un peu de magie d'index pour prendre en charge des listes plus grandes que la taille de texture maximale.

Mais les tampons de texture correspondent parfaitement à votre problème, je pense. Pour un aperçu plus précis, vous pouvez également consulter le spécification d'extension correspondant.

EDIT: En réponse au commentaire de Nicol sur objets tampons uniformes , vous pouvez également rechercher ici pour une petite comparaison des deux. J'ai toujours tendance à utiliser les TBO, mais je ne peux pas vraiment expliquer pourquoi, uniquement parce que je le vois mieux sur le plan conceptuel. Mais peut-être que Nicol pourra répondre à la question avec plus de détails.

7
Christian Rau

Une façon serait d'utiliser des tableaux uniformes comme vous le mentionnez. Une autre façon de le faire est d'utiliser une "texture" 1D. Recherchez GL_TEXTURE_1D et glTexImage1D. Personnellement, je préfère cette façon car vous n'avez pas besoin de coder en dur la taille du tableau dans le code du shader comme vous l'avez dit, et opengl a déjà des fonctions intégrées pour télécharger/accéder aux données 1D sur le GPU.

6
edvaldig

Je dirais probablement pas le numéro 1 .. vous avez un nombre limité de registres pour les uniformes shader, qui varie selon la carte. Vous pouvez interroger GL_MAX_FRAGMENT_UNIFORM_COMPONENTS pour connaître votre limite. Sur les cartes plus récentes, il se compte par milliers, par exemple un Quadro FX 5500 a 2048, apparemment. (http://www.nvnews.net/vbulletin/showthread.php?t=85925). Cela dépend du matériel sur lequel vous souhaitez qu'il s'exécute et des autres uniformes que vous souhaitez également envoyer au shader.

Le numéro 2 pourrait être fait fonctionner selon vos besoins. Désolé pour le flou ici, j'espère que quelqu'un d'autre peut vous donner une réponse plus précise, mais vous devez être explicite dans le nombre d'appels de texture que vous faites dans les anciennes cartes de modèle de shader. Cela dépend également du nombre de lectures de texture que vous voulez faire par fragment, vous ne voudriez probablement pas essayer de lire des milliers d'éléments par fragment, encore une fois, selon votre modèle de shader et vos exigences de performances. Vous pouvez regrouper des valeurs dans RGBAs d'une texture, vous donnant 4 lectures par appel de texture, mais avec un accès aléatoire comme exigence, cela pourrait ne pas vous aider.

Je ne suis pas sûr du numéro 3, mais je suggérerais peut-être de regarder les UAV (vues d'accès non ordonnées) bien que je pense que ce soit DirectX uniquement, sans équivalent openGL décent. Je pense qu'il y a une extension nVidia pour openGL, mais encore une fois, vous vous limitez à une spécification minimale assez stricte.

Il est peu probable que transmettre des milliers d'éléments de données à votre fragment shader soit la meilleure solution à votre problème.

2
Hybrid