HLSL의 리소스 바인딩

이 항목에서는 Direct3D 12에서 HLSL(High Level Shader Language) 셰이더 모델 5.1 을 사용하는 몇 가지 특정 기능에 대해 설명합니다. 모든 Direct3D 12 하드웨어는 셰이더 모델 5.1을 지원하므로, 이 모델에 대한 지원은 하드웨어 기능 수준에 따라 달라지지 않습니다.

리소스 유형 및 배열

셰이더 모델 5(SM5.0) 리소스 구문은 키워드(keyword) 사용하여 register 리소스에 대한 중요한 정보를 HLSL 컴파일러에 릴레이합니다. 예를 들어, 다음 명령문은 슬롯 t3, t4, t5 및 t6에서 바인딩된 4가지 질감의 배열을 선언합니다. t3은 명령문에 나타나는 유일한 레지스터 슬롯이며 단순히 4 배열에서 첫 번째가 됩니다.

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

HLSL의 SM5.1(셰이더 모델 5.1) 리소스 구문은 기존 레지스터 리소스 구문에 기반하기 때문에 쉽게 이식할 수 있습니다. HLSL의 Direct3D 12 리소스는 논리 레지스터 공간 내의 가상 레지스터에 바인딩됩니다.

  • t – SRV(셰이더 리소스 뷰)
  • s – 샘플러
  • u – UAV(순서가 지정되지 않은 액세스 뷰)
  • b – CBV(상수 버퍼 보기)

셰이더를 참조하는 루트 서명은 선언된 레지스터 슬롯과 호환되어야 합니다. 예를 들어, 루트 서명의 다음 부분은 슬롯 t0~t98까지 설명자 테이블을 기술하기 때문에 질감 슬롯 t3~t6의 사용과 호환이 가능합니다.

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

리소스 선언은 스칼라, 1D 배열 또는 다차원 배열일 수 있습니다.

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

SM5.1은 SM5.0과 동일한 리소스 유형 및 요소 형식을 사용합니다. SM5.1 선언 제한은 더 유연하며 런타임/하드웨어 제한에 의해서만 제한됩니다. space 키워드(keyword) 선언된 변수가 바인딩되는 논리 레지스터 공간을 지정합니다. space 키워드(keyword) 생략하면 기본 공간 인덱스 0이 범위에 tex2 암시적으로 할당되므로 위의 범위는 에 space0있습니다. register(t3, space0) 는 와 충돌하지 않으며 t3을 포함할 수 있는 다른 공간의 배열과 충돌 register(t3, space1)하지 않습니다.

배열 리소스에는 첫 번째 차원을 비워 두거나 0으로 지정하여 선언된 바인딩되지 않은 크기가 있을 수 있습니다.

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

일치하는 설명자 테이블은 다음과 같습니다.

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

HLSL의 바인딩되지 않은 배열은 설명자 테이블의 numDescriptors로 설정된 고정 숫자와 일치하며 HLSL의 고정 크기는 설명자 테이블의 제한되지 않은 선언과 일치합니다.

다차원 배열은 바인딩되지 않은 크기를 포함하여 허용됩니다. 이러한 다차원 배열은 레지스터 공간에서 병합됩니다.

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

리소스 범위의 앨리어싱은 허용되지 않습니다. 즉, 각 리소스 종류(t, s, u, b)에 대해 선언된 레지스터 범위는 겹치지 않아야 합니다. 여기에는 바인딩되지 않은 범위도 포함됩니다. 다른 레지스터 공간에서 선언된 범위는 절대 중복되지 않습니다. 바인딩 tex2 되지 않은(위)는 에 space0상주하지만 바인딩 tex3 되지 않은 는 에 space1있으므로 겹치지 않습니다.

배열로 선언된 리소스에 액세스하는 것은 인덱싱하는 것만큼 간단합니다.

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

인덱스(myMaterialIDsamplerID 위의 코드에서)의 사용에 대한 중요한 기본 제한은 웨이브 내에서 다를 수 없다는 것입니다. 인스턴스를 기반으로 인덱스를 변경하는 것조차 다른 것으로 간주됩니다.

인덱스를 변경해야 하는 경우에는 인덱스에 NonUniformResourceIndex 한정자를 지정합니다. 예:

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

일부 하드웨어에서 이 한정자를 사용하면 정확성 (스레드 간 포함)은 강화되지만 성능 비용이 아주 미미하게 드는 추가 코드가 생성됩니다. 이 한정자 없이 그리기/디스패치 내에서 인덱스가 변경되면 결과가 정의되지 않습니다.

설명자 배열 및 질감 배열

질감 배열은 DirectX 10부터 제공되었습니다. 질감 배열에는 설명자가 하나 필요하지만 모든 배열 조각은 동일한 형식, 너비, 높이 및 밉 수를 공유해야 합니다. 또한 배열은 가상 주소 공간에서 연속 범위를 차지해야 합니다. 다음 코드는 셰이더에서 질감 배열에 액세스하는 예제입니다.

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

질감 배열에서는 인덱스를 자유롭게 변경할 수 있으며, NonUniformResourceIndex와 같은 한정자가 필요하지 않습니다.

해당하는 설명자 배열은 다음과 같습니다.

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

배열 인덱스에 대한 float 사용이 myArrayOfTex2D[2]로 대체됩니다. 또한 설명자 배열은 차원에 더 많은 유연성을 제공합니다. 이 예제의 Texture2D 유형은 다를 수 없지만 형식, 너비, 높이 및 밉 수는 모두 각 설명자마다 다를 수 있습니다.

질감 배열의 설명자 배열을 사용할 수 있습니다.

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

각 구조체에 설명자가 포함된 구조체 배열을 선언할 수 없습니다. 예를 들어 다음 코드는 지원되지 않습니다.

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

이렇게 하면 메모리 레이아웃 abcabcabc....이 가능하지만 언어 제한이므로 지원되지 않습니다. 이 작업을 수행하는 한 가지 방법은 다음과 같습니다. 단, 이 경우 메모리 레이아웃은 aaa...bbb...ccc...입니다.

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

abcabcabc.... 메모리 레이아웃을 구현하려면 myStruct 구조체를 사용하지 않고 설명자 테이블을 사용합니다.

리소스 앨리어싱

HLSL 셰이더에 지정되는 리소스 범위는 논리적 범위입니다. 이것은 루트 서명 메커니즘을 통해 런타임 시 구체적인 힙 범위에 바인딩됩니다. 일반적으로 논리 범위는 다른 힙 범위와 겹치지 않는 힙 범위에 매핑됩니다. 하지만 루트 서명 메커니즘을 사용하면 호환 가능한 유형의 힙 범위를 앨리어싱(겹치기)할 수 있습니다. 예를 들어 위 예제의 tex2tex3 범위는 동일한(또는 겹치는) 힙 범위로 매핑될 수 있으며 이것은 HLSL 프로그램에서 질감 앨리어싱 효과가 있습니다. 이러한 별칭 지정이 필요한 경우 셰이더는 FXC(효과 컴파일러 도구)의 /res_may_alias 옵션을 사용하여 설정된 D3D10_SHADER_RESOURCES_MAY_ALIAS 옵션을 사용하여 컴파일해야 합니다. 이 옵션은 리소스가 앨리어싱될 수 있다는 가정 하에 특정 로드/저장소 최적화를 방지하여 컴파일러가 올바른 코드를 생성하도록 합니다.

발산 및 파생 항목

SM5.1은 리소스 인덱스에 제한을 두지 않습니다. 즉, tex2[idx].Sample(…) - 인덱스 idx는 리터럴 상수, cbuffer 상수 또는 보간된 값일 수 있습니다. 프로그래밍 모델은 뛰어난 유연성을 제공하지만 다음 사항에 유의해야 합니다.

  • 인덱스가 쿼드 간에 분기되면 하드웨어 컴퓨팅 파생 항목 및 LOD와 같은 파생된 수량이 정의되지 않을 수 있습니다. 이 경우 HLSL 컴파일러는 경고를 발생시키기 위해 최선을 다하지만 셰이더가 컴파일되는 것은 막지 못합니다. 이 동작은 서로 다른 제어 흐름의 파생 항목 컴퓨팅과 유사합니다.
  • 리소스 인덱스가 서로 다르면 같은 형태의 인덱스보다 성능이 떨어집니다. 하드웨어가 여러 리소스에 대한 작업을 수행해야 하기 때문입니다. 서로 다를 수 있는 리소스 인덱스는 HLSL 코드에 NonUniformResourceIndex 함수로 표시되어야 합니다. 그렇지 않으면 결과가 정의되지 않습니다.

픽셀 셰이더의 UAV

SM5.1은 SM5.0의 경우처럼 픽셀 셰이더의 UAV 범위에 제약 조건을 적용하지 않습니다.

상수 버퍼

SM5.1 상수 버퍼(cbuffer) 구문은 개발자가 상수 버퍼를 인덱싱할 수 있도록 SM5.0에서 변경되었습니다. 인덱싱 가능 상수 버퍼를 사용할 수 있도록 ConstantBuffer "template" 구조체가 SM5.1에 도입되었습니다.

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

앞의 코드는 유형이 Foo이고 크기가 6인 상수 버퍼 변수 myCB1과 스칼라 상수 버퍼 변수 myCB2를 선언합니다. 이제 상수 버퍼 변수를 셰이더에서 다음과 같이 인덱싱할 수 있습니다.

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

'a' 및 'b' 필드는 전역 변수가 되지 않고 필드로 처리되어야 합니다. 이전 버전과의 호환성을 위해 SM5.1은 스칼라 cbuffer에 대한 이전 cbuffer 개념을 지원합니다. 다음 명령문은 SM5.0처럼 'a' 및 'b' 전역 읽기 전용 변수를 만듭니다. 하지만 이전 스타일의 cbuffer는 인덱싱할 수 없습니다.

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

현재 셰이더 컴파일러는 사용자 정의 구조체에 대해서만 ConstantBuffer 템플릿을 지원합니다.

호환성을 위해 HLSL 컴파일러는 space0에 선언된 범위에 리소스 레지스터를 자동으로 할당할 수 있습니다. register 절에서 'space'가 생략되면 기본 space0이 사용됩니다. 컴파일러는 first-hole-fits 추론을 사용하여 레지스터를 할당합니다. 할당은 공간에 대한 Space 필드를 추가하도록 확장된 Reflection API를 통해 검색 할 수 있으며 BindPoint 필드는 리소스 레지스터 범위의 하한을 나타냅니다.

SM5.1의 바이트코드 변경 내용

SM5.1은 명령에서 리소스 레지스터가 선언되고 참조되는 방법을 변경합니다. 구문은 레지스터를 "변수"로 선언하는 것을 포함하며, 그룹 공유 메모리 레지스터에 대해 수행되는 것과 유사합니다.

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

이것은 다음으로 디스어셈블됩니다.

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

이제 각 셰이더 리소스 범위에는 셰이더 바이트코드에 고유한 ID(이름)가 있습니다. 예를 들어, tex1 (t10) 질감 배열은 셰이더 바이트코드에서 'T1'이 됩니다. 각 리소스 범위에 고유한 ID를 부여하면 다음 두 가지가 가능합니다.

  • 명령에서 인덱싱되는 리소스 범위(dcl_resource_texture2d 참조)를 명확하게 식별합니다(샘플 명령 참조).
  • 선언에 특성 집합을 연결합니다(예: 요소 유형, 진행 크기, 래스터 작업 모드 등).

참고: 범위의 ID는 HLSL 하한 선언과 관련이 없습니다.

리플렉션 리소스 바인딩(맨 위에 나열됨) 및 셰이더 선언 명령(dcl_*)의 순서는 HLSL 변수와 바이트코드 ID 간의 대응을 식별하는 데 도움이 됩니다.

SM5.1에서 각 선언 명령은 3D 피연산자를 사용하여 범위 ID, 상한 및 하한을 정의합니다. 레지스터 공간을 지정하기 위해 추가 토큰을 내보냅니다. 범위의 추가 속성을 전달하기 위해 다른 토큰도 내보낼 수 있으며, 예를 들어, cbuffer 또는 구조화된 버퍼 선언 명령은 cbuffer 또는 구조체의 크기를 내보냅니다. 인코딩의 정확한 세부 정보는 d3d12TokenizedProgramFormat.h 및 D3D10ShaderBinary::CShaderCodeParser에서 찾을 수 있습니다.

SM5.1 명령은 명령의 일부로 추가 리소스 피연산자 정보를 내보내지 않습니다(SM5.0처럼). 이 정보는 이제 선언 명령에 있습니다. SM5.0에서 인덱싱은 선언에 대한 연결을 난독처리하기 때문에 명령 인덱싱 리소스에는 확장된 opcode 토큰에 기술할 리소스 특성이 필요합니다. SM5.1에서는 각 ID(예: 't1')가 필수 리소스 정보를 설명하는 단일 선언과 명확하게 연관되어 있습니다. 따라서 리소스 정보를 설명하기 위해 명령에 사용되는 확장된 opcode 토큰을 더 이상 내보내지 않습니다.

선언되지 않은 명령에서는 샘플러, SRV 및 UAV에 대한 리소스 피연산자가 2D 피연산자입니다. 첫 번째 인덱스는 범위 ID를 지정하는 리터럴 상수입니다. 두 번째 인덱스는 인덱스의 선형화된 값을 나타냅니다. 이 값은 루트 서명과의 상관 관계를 향상시키고 인덱스를 조정하는 드라이버 컴파일러 부담을 줄이기 위해 해당 레지스터 공간의 시작 부분을 기준으로(논리적 범위의 시작과 관련이 없음) 컴퓨팅됩니다.

CBV에 대한 리소스 피연산자는 범위의 리터럴 ID, 상수 버퍼의 인덱스, 상수 버퍼의 특정 인스턴스에 대한 오프셋을 비롯한 3D 피연산자입니다.

HLSL 선언 예시

HLSL 프로그램은 루트 서명에 대해 알 필요가 없습니다. 가상 "레지스터" 바인딩 공간, SRV의 경우 t#, UV의 경우 u#, CBV의 경우 b#, 샘플러의 경우 s#에 바인딩을 할당하거나 컴파일러를 사용하여 할당을 선택하거나 나중에 셰이더 리플렉션을 사용하여 결과 매핑을 쿼리할 수 있습니다. 루트 서명은 설명자 테이블, 루트 설명자 및 루트 상수를 이 가상 레지스터 공간에 매핑합니다.

다음은 HLSL 셰이더에 있을 수 있는 몇 가지 예제 선언입니다. 참고: 루트 서명 또는 설명자 테이블에 대한 참조가 없습니다.

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)