Udostępnij przez


Powiązanie zasobów w programie HLSL

W tym temacie opisano niektóre specyficzne funkcje korzystania z języka cieniowania wysokiego poziomu (HLSL) Shader Model 5.1 z funkcją Direct3D 12. Wszystkie sprzęty Direct3D 12 obsługują model Shader Model 5.1, więc obsługa tego modelu nie zależy od poziomu funkcji sprzętu.

Typy zasobów i tablice

Składnia zasobów modelu cieniowania 5 (SM5.0) używa słowa kluczowego register do przekazywania ważnych informacji o zasobie do kompilatora HLSL. Na przykład poniższa instrukcja deklaruje tablicę czterech tekstur powiązanych w slotach t3, t4, t5 i t6. t3 jest jedynym gniazdem rejestru występującym w instrukcji, po prostu jest pierwszym w tablicy czterech elementów.

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

Składnia zasobów modelu cieniowania 5.1 (SM5.1) w programie HLSL jest oparta na istniejącej składni zasobu rejestru, aby ułatwić przenoszenie. Zasoby Direct3D 12 w HLSL są powiązane z rejestrami wirtualnymi w logicznych przestrzeniach rejestrów.

  • t — dla widoków zasobów cieniowania (SRV)
  • s — dla próbników
  • u — w przypadku widoków dostępu bez kolejności (UAV)
  • b — dla widoków stałych buforów (CBV)

Sygnatura źródłowa odwołująca się do shadera musi być zgodna z zadeklarowanymi miejscami rejestru. Na przykład następująca część podpisu głównego będzie zgodna z użyciem gniazd tekstury t3 do t6, ponieważ opisuje tabelę deskryptora z gniazdami t0 do t98.

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

Deklaracja zasobu może być skalarną, tablicą 1D lub tablicą wielowymiarową:

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

Protokół SM5.1 używa tych samych typów zasobów i typów elementów, co sm5.0. Limity deklaracji SM5.1 są bardziej elastyczne i ograniczone tylko przez limity środowiska uruchomieniowego/sprzętu. Słowo kluczowe space określa, do którego miejsca rejestru logicznego jest powiązana zadeklarowana zmienna. Jeśli słowo kluczowe space zostanie pominięte, domyślny indeks przestrzeni 0 jest niejawnie przypisywany do zakresu (więc powyższy zakres tex2 znajduje się w space0). register(t3, space0) nigdy nie będzie powodować konfliktów z register(t3, space1), ani z żadną tablicą w innej przestrzeni, mogącą zawierać t3.

Zasób tablicy może mieć niezwiązany rozmiar, który jest zadeklarowany przez określenie pierwszego wymiaru, który ma być pusty lub 0:

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

Zgodna tabela deskryptora może być:

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

Tablica niezwiązana w HLSL jest zgodna z stałą liczbą ustawioną z numDescriptors w tabeli deskryptora, a stały rozmiar w HLSL jest zgodny z niezwiązaną deklaracją w tabeli deskryptora.

Tablice wielowymiarowe są dozwolone, w tym o nieograniczonym rozmiarze. Te tablice wielowymiarowe są spłaszczone w przestrzeni rejestrowej.

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

Aliasowanie zakresów zasobów jest niedozwolone. Innymi słowy, dla każdego typu zasobu (t, s, u, b), zadeklarowane zakresy rejestrów nie mogą się nakładać. Obejmuje to również niezwiązane zakresy. Zakresy zadeklarowane w różnych miejscach rejestru nigdy się nie nakładają. Należy pamiętać, że niezwiązane tex2 (powyżej) znajdują się w space0, podczas gdy niezwiązane tex3 znajdują się w space1, tak aby nie nakładały się na siebie.

Uzyskiwanie dostępu do zasobów, które zostały zadeklarowane jako tablice, jest tak proste, jak ich indeksowanie.

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

Istnieje ważne ograniczenie domyślne dotyczące używania indeksów (myMaterialID i samplerID w powyższym kodzie), ponieważ nie mogą się one różnić w obrębie fali. Nawet zmiana indeksu na podstawie liczby wystąpień liczy się jako zmienność.

Jeśli konieczna jest zmiana indeksu, określ kwalifikator NonUniformResourceIndex w indeksie, na przykład:

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

Na niektórych sprzętach użycie tego kwalifikatora generuje dodatkowy kod w celu wymuszania poprawności (w tym między wątkami), ale przy niewielkim koszcie wydajności. Jeśli indeks zostanie zmieniony bez tego kwalifikatora, podczas operacji rysowania lub wysyłania, wyniki są niezdefiniowane.

Tablice deskryptorowe i tablice tekstur

Tablice tekstur są dostępne od wersji DirectX 10. Tablice tekstur wymagają jednego deskryptora, jednak wszystkie wycinki tablic muszą mieć ten sam format, szerokość, wysokość i liczbę mip. Ponadto tablica musi zajmować ciągły zakres w wirtualnej przestrzeni adresowej. Poniższy kod przedstawia przykład dostępu do tablicy tekstur z poziomu shadera.

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);

W tablicy tekstur indeks może być swobodnie różny, bez konieczności kwalifikatorów, takich jak NonUniformResourceIndex.

Równoważna tablica deskryptora wyglądałaby następująco:

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

Zwróć uwagę, że niezręczne użycie zmiennoprzecinku dla indeksu tablicy jest zastępowane myArrayOfTex2D[2]. Ponadto tablice deskryptorowe oferują większą elastyczność w odniesieniu do wymiarów. Typ, Texture2D jest tym przykładem, nie może się różnić, ale format, szerokość, wysokość i liczba mip mogą się różnić w zależności od deskryptora.

Jest uzasadnione posiadanie tablicy deskryptorów tablic tekstury.

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

Nie jest właściwe deklarowanie tablicy struktur, gdzie każda struktura zawiera deskryptory. Na przykład, poniższy kod nie jest obsługiwany.

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

Umożliwiłoby to układ pamięci abcabcabc...., ale jest ograniczeniem języka i nie jest obsługiwany. Jedną z obsługiwanych metod tego działania jest następująca, chociaż układ pamięci w tym przypadku jest aaa... Bbb... ccc....

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

Aby osiągnąć układ pamięci abcabcabc...., użyj tabeli deskryptorów, nie korzystając ze struktury myStruct.

Aliasowanie zasobów

Zakresy zasobów określone w cieniowaniach HLSL są zakresami logicznymi. Są one powiązane z konkretnymi zakresami sterty w czasie działania za pośrednictwem mechanizmu podpisu korzeniowego. Zwykle zakres logiczny jest odwzorowany na zakres sterty, który nie pokrywa się z innymi zakresami sterty. Jednak mechanizm sygnatury korzenia umożliwia aliasowanie (nakładanie się) zakresów stert kompatybilnych typów. Na przykład zakresy tex2 i tex3 z powyższego przykładu mogą być mapowane na ten sam (lub nakładający się) zakres stert, co powoduje aliasowanie tekstur w programie HLSL. Jeśli takie aliasowanie jest pożądane, cieniowanie musi być skompilowane z opcją D3D10_SHADER_RESOURCES_MAY_ALIAS, która jest ustawiana przy użyciu opcji /res_may_alias dla narzędzia Effect-Compiler (FXC). Opcja sprawia, że kompilator tworzy prawidłowy kod, uniemożliwiając pewne optymalizacje operacji ładowania/zapisywania zgodnie z założeniem, że zasoby mogą nakładać się.

Rozbieżność i pochodne

Protokół SM5.1 nie nakłada ograniczeń na indeks zasobów; to znaczy, indeks tex2[idx].Sample(…) może być wartością stałą, stałą cbuffer lub wartością interpolowaną. Chociaż model programowania zapewnia taką dużą elastyczność, należy pamiętać o następujących kwestiach:

  • Jeśli indeks różni się na czworokącie, obliczone sprzętowo pochodne i wartości pochodne, takie jak LOD, mogą być niezdefiniowane. Kompilator HLSL stara się wydać ostrzeżenie w tym przypadku, ale nie zapobiegnie kompilacji shaderu. To zachowanie jest podobne do obliczania pochodnych w rozbieżnym przepływie sterowania.
  • Jeśli indeks zasobów jest rozbieżny, wydajność jest zmniejszona w porównaniu z przypadkiem jednolitego indeksu, ponieważ sprzęt musi wykonywać operacje na kilku zasobach. Indeksy zasobów, które mogą być rozbieżne, muszą być oznaczone funkcją NonUniformResourceIndex w kodzie HLSL. W przeciwnym razie wyniki są niezdefiniowane.

UAV-y w cieniowaniu pikseli

SM5.1 nie nakłada ograniczeń na zakresy UAV w shaderach pikseli, jak miało to miejsce w przypadku SM5.0.

Bufory stałe

Składnia SM5.1 buforów stałych (cbuffer) została zmieniona z SM5.0, aby umożliwić deweloperom indeksowanie buforów stałych. Aby włączyć bufor o stałej z możliwością indeksowania, SM5.1 wprowadza konstrukcję "szablon" ConstantBuffer:

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

Powyższy kod deklaruje zmienną stałego buforu myCB1 typu Foo i rozmiar 6 oraz zmienną skalarną, stałą myCB2. Zmienna buforu stałego może być teraz indeksowana w cieniatorze jako:

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

Pola "a" i "b" nie stają się zmiennymi globalnymi, ale raczej muszą być traktowane jako pola. W celu zapewnienia zgodności z poprzednimi wersjami, SM5.1 obsługuje starą koncepcję buforów stałych dla skalarnych buforów stałych. Poniższa instrukcja sprawia, że zmienne "a" i "b" stają się globalne i tylko do odczytu, jak w wersji SM5.0. Jednak taki stary styl cbuffer nie może być indeksowalny.

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

Obecnie kompilator cieniowania obsługuje szablon ConstantBuffer tylko dla struktur zdefiniowanych przez użytkownika.

Ze względów zgodności kompilator HLSL może automatycznie przypisywać rejestry zasobów dla zakresów zadeklarowanych w space0. Jeśli "spacja" zostanie pominięta w klauzuli register, zostanie użyta domyślna space0. Kompilator używa heurystyki pierwszego dopasowania do przypisywania rejestrów. Przypisanie można pobrać za pomocą interfejsu API refleksji, który został rozszerzony w celu dodania pola Space do określania miejsca, a pole BindPoint wskazuje dolną granicę zakresu rejestru zasobów.

Zmiany kodu bajtowego w programie SM5.1

SM5.1 zmienia sposób deklarowanego rejestrowania zasobów i odwołuje się do tego w instrukcjach. Składnia wymaga deklaracji rejestru zmiennej, podobnie jak w przypadku rejestrów pamięci współdzielonej przez grupę.

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;
}

To zostanie rozmontowane do:

// 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.

Każdy zakres zasobów cieniowania ma teraz identyfikator (nazwę), który jest unikatowy dla kodu bajtowego cieniowania. Na przykład tablica tekstur tex1 (t10) staje się "T1" w bajtowym kodzie shadera. Nadawanie unikatowych identyfikatorów każdemu zakresowi zasobów umożliwia wykonywanie dwóch czynności:

  • Jednoznacznie określ zakres zasobów (zobacz dcl_resource_texture2d) indeksowany w instrukcji (zobacz przykładową instrukcję).
  • Dołączanie zestawu atrybutów do deklaracji, np. typ elementu, rozmiar kroku, tryb operacji rastra itp.

Należy pamiętać, że identyfikator zakresu nie jest powiązany z deklaracją niższej granicy HLSL.

Kolejność powiązań zasobów odbicia (lista u góry) i instrukcje deklaracji cieniowania (dcl_*) są takie same, aby ułatwić identyfikację korespondencji między zmiennymi HLSL i identyfikatorami kodu bajtowego.

Każda instrukcja deklaracyjna w SM5.1 używa operandu 3D do definiowania: identyfikatora zakresu, dolnej i górnej granicy. Dodatkowy token jest emitowany w celu określenia miejsca w rejestrze. Inne tokeny mogą być również emitowane, aby przekazać dodatkowe właściwości zakresu, np. cbuffer lub instrukcja deklaracji buforu strukturalnego emituje rozmiar cbuffer lub struktury. Szczegółowe informacje dotyczące kodowania można znaleźć w d3d12TokenizedProgramFormat.h i D3D10ShaderBinary::CShaderCodeParser.

Instrukcje SM5.1 nie będą emitować dodatkowych informacji dotyczących operandów zasobów jako części instrukcji (jak w SM5.0). Te informacje znajdują się teraz w instrukcjach deklaracji. W wersji SM5.0 instrukcje indeksowania zasobów wymagały opisania atrybutów zasobów w rozszerzonych tokenach opcode, ponieważ indeksowanie zaciemniało skojarzenie z deklaracją. W wersji SM5.1 każdy identyfikator (taki jak "t1") jest jednoznacznie skojarzony z pojedynczą deklaracją, która opisuje wymagane informacje o zasobie. W związku z tym rozszerzone tokeny kodu opcode używane w instrukcjach do opisywania informacji o zasobach nie są już emitowane.

W instrukcjach innych niż deklaracje, operand zasobów dla samplerów, SRV i UAV to operand 2D. Pierwszy indeks jest stałą literalną, która określa identyfikator zakresu. Drugi indeks reprezentuje liniową wartość indeksu. Wartość jest obliczana względem początku odpowiedniego obszaru rejestru (nie względem początku zakresu logicznego) w celu lepszego skorelowania z podpisem głównym i zmniejszenia obciążenia kompilatora sterownika dostosowywania indeksu.

Operand zasobów dla CBVs to operand 3D, zawierający: literalny identyfikator zakresu, indeks buforu stałego, przesunięcie w ramach konkretnego egzemplarza buforu stałego.

Przykładowe deklaracje HLSL

Programy HLSL nie muszą nic wiedzieć o podpisach głównych. Mogą przypisywać powiązania do wirtualnej przestrzeni powiązania "rejestr", t# dla SRV, u# dla UAV, b# dla CBV, s# dla próbników lub polegać na kompilatorze, aby wybrać przypisania (i zapytanie wynikowych mapowań przy użyciu odbicia shaderów później). Sygnatura główna mapuje tabele deskryptorów, deskryptory główne i stałe główne na tę wirtualną przestrzeń rejestru.

Poniżej przedstawiono kilka przykładowych deklaracji, które może zawierać shader HLSL. Należy pamiętać, że nie ma odwołań do podpisów głównych ani tabel deskryptorów.

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)