Liaison de ressources dans HSL

Cette rubrique décrit certaines fonctionnalités spécifiques de l’utilisation du nuanceur HLSL (High Level Shader Language) Modèle 5.1 avec Direct3D 12. Tout le matériel Direct3D 12 prend en charge le modèle de nuanceur 5.1. La prise en charge de ce modèle ne dépend donc pas du niveau de fonctionnalité matérielle.

Types de ressources et tableaux

La syntaxe de ressource du modèle de nuanceur 5 (SM5.0) utilise la register mot clé pour relayer des informations importantes sur la ressource au compilateur HLSL. Par exemple, l’instruction suivante déclare un tableau de quatre textures liées aux emplacements t3, t4, t5 et t6. t3 est le seul emplacement de registre apparaissant dans l’instruction, étant simplement le premier dans le tableau de quatre.

Texture2D<float4> tex1[4] : register(t3)

La syntaxe de ressource du modèle de nuanceur 5.1 (SM5.1) dans HLSL est basée sur la syntaxe de ressource de registre existante, pour faciliter le portage. Les ressources Direct3D 12 dans HLSL sont liées à des registres virtuels dans des espaces de registre logiques :

  • t – pour les vues de ressources du nuanceur (SRV)
  • s : pour les échantillonneurs
  • u : pour les vues d’accès non ordonnées (UAV)
  • b – pour les vues de mémoire tampon constante (CBV)

La signature racine référençant le nuanceur doit être compatible avec les emplacements de registre déclarés. Par exemple, la partie suivante d’une signature racine serait compatible avec l’utilisation d’emplacements de texture t3 à t6, car elle décrit une table de descripteur avec des emplacements t0 à t98.

DescriptorTable( CBV(b1), SRV(t0,numDescriptors=99), CBV(b2) )

Une déclaration de ressource peut être un tableau scalaire, un tableau 1D ou un tableau multidimensionnel :

Texture2D<float4> tex1 : register(t3,  space0)
Texture2D<float4> tex2[4] : register(t10)
Texture2D<float4> tex3[7][5][3] : register(t20, space1)

SM5.1 utilise les mêmes types de ressources et les mêmes types d’éléments que SM5.0. Les limites de déclaration SM5.1 sont plus flexibles et limitées uniquement par les limites du runtime/du matériel. Le space mot clé spécifie à quel espace de registre logique la variable déclarée est liée. Si le space mot clé est omis, l’index d’espace par défaut 0 est implicitement affecté à la plage (la tex2 plage ci-dessus réside donc dans space0). register(t3, space0) ne sera jamais en conflit avec register(t3, space1), ni avec un tableau dans un autre espace qui peut inclure t3.

Une ressource de tableau peut avoir une taille non limitée, qui est déclarée en spécifiant la toute première dimension à vide, ou 0 :

Texture2D<float4> tex1[] : register(t0)

La table de descripteur correspondante peut être :

DescriptorTable( CBV(b1), UAV(u0, numDescriptors = 4), SRV(t0, numDescriptors=unbounded) )

Un tableau non lié dans HLSL correspond à un nombre fixe défini avec numDescriptors dans la table de descripteur, et une taille fixe dans HLSL correspond à une déclaration non liée dans la table de descripteur.

Les tableaux multidimensionnels sont autorisés, y compris d’une taille non délimitée. Ces tableaux multidimensionnels sont aplati dans l’espace de registre.

Texture2D<float4> tex2[3000][10] : register(t0, space0); // t0-t29999 in space0
Texture2D<float4> tex3[0][5][3] : register(t5, space1)

L’alias de plages de ressources n’est pas autorisé. En d’autres termes, pour chaque type de ressource (t, s, u, b), les plages de registre déclarées ne doivent pas se chevaucher. Cela inclut également les plages non délimitées. Les plages déclarées dans différents espaces de registre ne se chevauchent jamais. Notez que non lié tex2 (ci-dessus) réside dans space0, tandis que le non lié tex3 réside dans space1, de sorte qu’ils ne se chevauchent pas.

L’accès aux ressources qui ont été déclarées en tant que tableaux est aussi simple que de les indexer.

Texture2D<float4> tex1[400] : register(t3);
sampler samp[7] : register(s0);
tex1[myMaterialID].Sample(samp[samplerID], texCoords);

Il existe une restriction par défaut importante sur l’utilisation des index (myMaterialID et samplerID dans le code ci-dessus) en ce qu’ils ne sont pas autorisés à varier au sein d’une vague. Même la modification de l’index en fonction de l’instanciation compte comme variant.

Si la variation de l’index est nécessaire, spécifiez le NonUniformResourceIndex qualificateur sur l’index, par exemple :

tex1[NonUniformResourceIndex(myMaterialID)].Sample(samp[NonUniformResourceIndex(samplerID)], texCoords);

Sur certains matériels, l’utilisation de ce qualificateur génère du code supplémentaire pour appliquer l’exactitude (y compris entre les threads), mais à un coût de performances mineur. Si un index est modifié sans ce qualificateur et qu’au sein d’un draw/dispatch, les résultats ne sont pas définis.

Tableaux de descripteurs et tableaux de textures

Les tableaux de textures sont disponibles depuis DirectX 10. Les tableaux de textures nécessitent un descripteur, mais toutes les tranches de tableau doivent partager le même format, la même largeur, la même hauteur et le même nombre de mip. En outre, le tableau doit occuper une plage contiguë dans l’espace d’adressage virtuel. Le code suivant montre un exemple d’accès à un tableau de textures à partir d’un nuanceur.

Texture2DArray<float4> myTex2DArray : register(t0); // t0
float3 myCoord(1.0f,1.4f,2.2f); // 2.2f is array index (rounded to int)
color = myTex2DArray.Sample(mySampler, myCoord);

Dans un tableau de textures, l’index peut être modifié librement, sans avoir besoin de qualificateurs tels que NonUniformResourceIndex.

Le tableau de descripteur équivalent serait :

Texture2D<float4> myArrayOfTex2D[] : register(t0); // t0+
float2 myCoord(1.0f, 1.4f);
color = myArrayOfTex2D[2].Sample(mySampler,myCoord); // 2 is index

Notez que l’utilisation gênante d’un float pour l’index de tableau est remplacée par myArrayOfTex2D[2]. Les tableaux de descripteurs offrent également plus de flexibilité avec les dimensions. Le type, Texture2D est cet exemple, ne peut pas varier, mais le format, la largeur, la hauteur et le nombre de mip peuvent tous varier avec chaque descripteur.

Il est légitime d’avoir un tableau de descripteur de tableaux de textures :

Texture2DArray<float4> myArrayOfTex2DArrays[2] : register(t0);

Il n’est pas légitime de déclarer un tableau de structures, chaque structure contenant des descripteurs, par exemple le code suivant n’est pas pris en charge.

struct myStruct {
    Texture2D                    a; 
    Texture2D                    b;
    ConstantBuffer<myConstants>  c;
};
myStruct foo[10000] : register(....);

Cela aurait permis la disposition de la mémoire abcabcabc...., mais il s’agit d’une limitation de langage et n’est pas pris en charge. Une méthode prise en charge pour effectuer cette opération serait la suivante, bien que la disposition de la mémoire dans ce cas soit aaa... Bbb... ccc....

Texture2D                     a[10000] : register(t0);
Texture2D                     b[10000] : register(t10000);
ConstantBuffer<myConstants>   c[10000] : register(b0);

Pour obtenir la disposition de mémoire abcabcabc.... , utilisez une table de descripteur sans utiliser la myStruct structure.

Alias de ressources

Les plages de ressources spécifiées dans les nuanceurs HLSL sont des plages logiques. Ils sont liés à des plages de tas concrètes au moment de l’exécution via le mécanisme de signature racine. Normalement, une plage logique est mappée à une plage de tas qui ne se chevauche pas avec d’autres plages de tas. Toutefois, le mécanisme de signature racine permet d’alias (chevaucher) les plages de tas de types compatibles. Par exemple, tex2 et tex3 les plages de l’exemple ci-dessus peuvent être mappées à la même plage de tas (ou se chevauchent), ce qui a pour effet de créer des alias des textures dans le programme HLSL. Si un tel alias est souhaité, le nuanceur doit être compilé avec D3D10_SHADER_RESOURCES_MAY_ALIAS option, qui est définie à l’aide de l’option /res_may_alias pour l’outil FXC (Effect-Compiler Tool ). L’option permet au compilateur de produire du code correct en empêchant certaines optimisations de chargement/stockage en supposant que les ressources peuvent être alias.

Divergence et dérivés

SM5.1 n’impose pas de limitations à l’index de ressource ; Autrement dit, tex2[idx].Sample(…) l’idx d’index peut être une constante littérale, une constante cbuffer ou une valeur interpolée. Bien que le modèle de programmation offre une telle flexibilité, il existe des problèmes à connaître :

  • Si l’index diffère d’un quad, les quantités dérivées et dérivées calculées sur le matériel, telles que LOD, peuvent ne pas être définies. Le compilateur HLSL fait tout son possible pour émettre un avertissement dans ce cas, mais n’empêche pas un nuanceur de compiler. Ce comportement est similaire au calcul des dérivés dans un flux de contrôle divergent.
  • Si l’index de ressource est divergent, les performances sont diminuées par rapport au cas d’un index uniforme, car le matériel doit effectuer des opérations sur plusieurs ressources. Les index de ressources qui peuvent être divergents doivent être marqués avec la fonction dans le NonUniformResourceIndex code HLSL. Sinon, les résultats ne sont pas définis.

UAV dans les nuanceurs de pixels

SM5.1 n’impose pas de contraintes sur les plages d’UAV dans les nuanceurs de pixels comme c’était le cas pour SM5.0.

Mémoires tampons constantes

La syntaxe des mémoires tampons constantes SM5.1 (cbuffer) a changé depuis SM5.0 pour permettre aux développeurs d’indexer des mémoires tampons constantes. Pour activer les mémoires tampons constantes indexables, SM5.1 introduit la ConstantBuffer construction « template » :

struct Foo
{
    float4 a;
    int2 b;
};
ConstantBuffer<Foo> myCB1[2][3] : register(b2, space1);
ConstantBuffer<Foo> myCB2 : register(b0, space1);

Le code précédent déclare une variable myCB1 de mémoire tampon constante de type Foo et de taille 6, ainsi qu’une variable myCB2de mémoire tampon scalaire constante . Une variable de mémoire tampon constante peut maintenant être indexée dans le nuanceur comme suit :

myCB1[i][j].a.xyzw
myCB2.b.yy

Les champs 'a' et 'b' ne deviennent pas des variables globales, mais doivent plutôt être traités comme des champs. Pour la compatibilité descendante, SM5.1 prend en charge l’ancien concept cbuffer pour les cbuffers scalaires. L’instruction suivante définit les variables « a » et « b » globales, en lecture seule comme dans SM5.0. Toutefois, un tel cbuffer de style ancien ne peut pas être indexable.

cbuffer : register(b1)
{
    float4 a;
    int2 b;
};

Actuellement, le compilateur de nuanceur prend uniquement en charge le ConstantBuffer modèle pour les structures définies par l’utilisateur.

Pour des raisons de compatibilité, le compilateur HLSL peut affecter automatiquement des registres de ressources pour les plages déclarées dans space0. Si 'space' est omis dans la clause register, la valeur par défaut space0 est utilisée. Le compilateur utilise l’heuristique first-hole-fits pour affecter les registres. L’affectation peut être récupérée via l’API de réflexion, qui a été étendue pour ajouter le champ Espace pour l’espace, tandis que le champ BindPoint indique la limite inférieure de la plage de registre de ressources.

Modifications du bytecode dans SM5.1

SM5.1 modifie la façon dont les registres de ressources sont déclarés et référencés dans les instructions. La syntaxe implique la déclaration d’un registre « variable », comme pour les registres de mémoire partagée de groupe :

Texture2D<float4> tex0          : register(t5,  space0);
Texture2D<float4> tex1[][5][3]  : register(t10, space0);
Texture2D<float4> tex2[8]       : register(t0,  space1);
SamplerState samp0              : register(s5, space0);

float4 main(float4 coord : COORD) : SV_TARGET
{
    float4 r = coord;
    r += tex0.Sample(samp0, r.xy);
    r += tex2[r.x].Sample(samp0, r.xy);
    r += tex1[r.x][r.y][r.z].Sample(samp0, r.xy);
    return r;
}

Cette opération est désassemble pour :

// Resource Bindings:
//
// Name                                 Type  Format         Dim    ID   HLSL Bind     Count
// ------------------------------ ---------- ------- ----------- -----   --------- ---------
// samp0                             sampler      NA          NA     S0    a5            1
// tex0                              texture  float4          2d     T0    t5            1
// tex1[0][5][3]                     texture  float4          2d     T1   t10        unbounded
// tex2[8]                           texture  float4          2d     T2    t0.space1     8
//
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// COORD                    0   xyzw        0     NONE   float   xyzw
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_TARGET                0   xyzw        0   TARGET   float   xyzw
//
ps_5_1
dcl_globalFlags refactoringAllowed
dcl_sampler s0[5:5], mode_default, space=0
dcl_resource_texture2d (float,float,float,float) t0[5:5], space=0
dcl_resource_texture2d (float,float,float,float) t1[10:*], space=0
dcl_resource_texture2d (float,float,float,float) t2[0:7], space=1
dcl_input_ps linear v0.xyzw
dcl_output o0.xyzw
dcl_temps 2
sample r0.xyzw, v0.xyxx, t0[0].xyzw, s0[5]
add r0.xyzw, r0.xyzw, v0.xyzw
ftou r1.x, r0.x
sample r1.xyzw, r0.xyxx, t2[r1.x + 0].xyzw, s0[5]
add r0.xyzw, r0.xyzw, r1.xyzw
ftou r1.xyz, r0.zyxz
imul null, r1.yz, r1.zzyz, l(0, 15, 3, 0)
iadd r1.y, r1.z, r1.y
iadd r1.x, r1.x, r1.y
sample r1.xyzw, r0.xyxx, t1[r1.x + 10].xyzw, s0[5]
add o0.xyzw, r0.xyzw, r1.xyzw
ret
// Approximately 12 instruction slots are used.

Chaque plage de ressources de nuanceur a désormais un ID (un nom) qui est unique au bytecode du nuanceur. Par exemple, le tableau de texture tex1 (t10) devient « T1 » dans le bytecode du nuanceur. L’attribution d’ID uniques à chaque plage de ressources permet deux choses :

  • Identifiez sans ambiguïté la plage de ressources (voir dcl_resource_texture2d) qui est indexée dans une instruction (voir exemple d’instruction).
  • Attachement d’un ensemble d’attributs à la déclaration, par exemple, le type d’élément, la taille de la foulée, le mode de fonctionnement raster, etc.

Notez que l’ID de la plage n’est pas lié à la déclaration de limite inférieure HLSL.

L’ordre des liaisons de ressources de réflexion (liste en haut) et des instructions de déclaration du nuanceur (dcl_*) est le même pour faciliter l’identification de la correspondance entre les variables HLSL et les ID de bytecode.

Chaque instruction de déclaration dans SM5.1 utilise un opérande 3D pour définir : ID de plage, limites inférieure et supérieure. Un jeton supplémentaire est émis pour spécifier l’espace d’inscription. D’autres jetons peuvent également être émis pour transmettre des propriétés supplémentaires de la plage, par exemple, cbuffer ou l’instruction de déclaration de mémoire tampon structurée émet la taille du cbuffer ou de la structure. Vous trouverez les détails exacts de l’encodage dans d3d12TokenizedProgramFormat.h et D3D10ShaderBinary::CShaderCodeParser.

Les instructions SM5.1 n’émettent pas d’informations supplémentaires sur l’opérande de ressources dans le cadre de l’instruction (comme dans SM5.0). Ces informations figurent désormais dans les instructions de déclaration. Dans SM5.0, les instructions d’indexation des ressources nécessitaient que les attributs de ressource soient décrits dans des jetons opcode étendus, car l’indexation a masqué l’association à la déclaration. Dans SM5.1, chaque ID (tel que « t1 ») est associé sans ambiguïté à une déclaration unique qui décrit les informations de ressource requises. Par conséquent, les jetons opcode étendus utilisés dans les instructions pour décrire les informations de ressource ne sont plus émis.

Dans les instructions de non-déclaration, un opérande de ressources pour les échantillonneurs, les SRV et les UAV est un opérande 2D. Le premier index est une constante littérale qui spécifie l’ID de plage. Le deuxième index représente la valeur linéarisée de l’index. La valeur est calculée par rapport au début de l’espace de registre correspondant (et non par rapport au début de la plage logique) pour mieux corréler avec la signature racine et réduire la charge du compilateur de pilotes liée à l’ajustement de l’index.

Un opérande de ressources pour les cbV est un opérande 3D, contenant : ID littéral de la plage, index de la mémoire tampon constante, décalage dans le instance particulier de la mémoire tampon constante.

Exemples de déclarations HLSL

Les programmes HLSL n’ont pas besoin de connaître quoi que ce soit sur les signatures racines. Ils peuvent affecter des liaisons à l’espace de liaison « register » virtuel, t# pour les SSR, u# pour les UAV, b# pour les cbV, s# pour les échantillonneurs, ou s’appuyer sur le compilateur pour sélectionner les affectations (et interroger les mappages résultants à l’aide de la réflexion du nuanceur par la suite). La signature racine mappe les tables de descripteurs, les descripteurs racine et les constantes racines à cet espace de registre virtuel.

Voici quelques exemples de déclarations qu’un nuanceur HLSL peut avoir. Notez qu’il n’existe aucune référence aux signatures racines ou aux tables de descripteurs.

Texture2D foo[5] : register(t2);
Buffer bar : register(t7);
RWBuffer dataLog : register(u1);

Sampler samp : register(s0);

struct Data
{
    UINT index;
    float4 color;
};
ConstantBuffer<Data> myData : register(b0);

Texture2D terrain[] : register(t8); // Unbounded array
Texture2D misc[] : register(t0,space1); // Another unbounded array 
                                        // space1 avoids overlap with above t#

struct MoreData
{
    float4x4 xform;
};
ConstantBuffer<MoreData> myMoreData : register(b1);

struct Stuff
{
    float2 factor;
    UINT drawID;
};
ConstantBuffer<Stuff> myStuff[][3][8]  : register(b2, space3)