DirectX12의 Root SIgnature개념에 대한 정리
루트 시그니처를 간단히 말하자면 GPU에서 돌아가는 Shader프로그램이 특정 Register에 어떤 데이터를 가리키게 할것인가? 를 정의해주는 GPU에 할당된 정보라고 할 수 있다.
HRESULT hr = S_OK;
ComPtr<ID3DBlob> signature(nullptr);
ComPtr<ID3DBlob> error(nullptr);
hr = D3D12SerializeRootSignature(
&rootSignatureDesc,
D3D_ROOT_SIGNATURE_VERSION_1,
&signature,
&error
);// 루트 시그니처의 binary화
if (FAILED(hr))
{
return hr;
}
hr = pDevice->CreateRootSignature(
1,
signature->GetBufferPointer(),
signature->GetBufferSize(),
IID_PPV_ARGS(&m_rootSignature)
);//루트 시그니처 생성
if (FAILED(hr))
{
return hr;
}
문법은 위와 같다. 특이한 점이 있다면, D3D12SerializeRootSignature과정으로 루트 시그니처의 바이너리화 과정이 버퍼 생성과정과 분리되어있다. 이는 루트 시그니처를 미리 오프라인 환경에서 바이너리화 시킨다음에 프로그램을 배포할 수 있도록 기능을 제공하기 위한 API설계인듯 하다. 내 프로젝트에서는 그런거 할 일은 없을테니 그냥 매 실행마다 바이너리화를 진행한다.
다음으로는 Root Signature에 어떤 데이터들을 집어넣을 수 있는가? 에 대해 적어본다.
위 사진은 MicroSoft문서에 나온 자료인데, Root Constant,Inline Root Descriptor,Descriptor Table에 대한 설명에서는 이 자료를 기준으로 진행한다.
먼저, Root Signature에는 1DWORD(32비트)크기의 Root Constant를 할당할 수 있다.
이 메모리에는 Shader에서 참조할 데이터를 바로 넣어줄 수 있다. 위 그림의 5번째 DWORD메모리 영역에 있는 uint가 Root Constant값이다. 32비트 상수 4개를 Shader자원으로 할당하는 예시코드는 다음과 같다.
생성 코드
CD3DX12_ROOT_PARAMETER m_rootParameter[1]; m_rootParameter[0].InitAsConstants(4u, 1u);//1번 레지스터에 32비트 상수 4개할당 CD3DX12_ROOT_SIGNATURE_DESC localRootSignatureDesc( ARRAYSIZE(m_rootParameter), //local root signature의 파라미터 개수 m_rootParameter, //파라미터 정보 1u, //static sampler개수 &staticSamplerDesc //static sampler정보 ); //이후 ROOT_SIGNATURE_DESC로 Root Signature생성
Command List에 바인딩하는 코드
struct FourNum{ int a; int b; int c; int d; } FourNum fourNum = { .a = 1, .b = 2, .c = 3, .d = 4 } pCommandList->SetComputeRoot32BitConstants( 0,//0번째 Root Parameter의 4,//4개의 Root Constant데이터를 static_cast<void*>(&fourNum),//이 녀석들로 채운다 0 );
루트 시그니처에는 Inline Root Descriptor도 넣을 수 있다.위 그림에서 0번째 DWORD메모리에 있는게 Inline Root Descriptor이다. 이전 글인 Descriptor Heap에서 언급한 그 Descriptor맞다. 차이점은 Descriptor긴 한데 저장되는 위치가 루트 시그니처라는 것이다. 그래서 Inline이라 이름붙었다. Inline Descriptor는 2DWORD를 차지한다.
생성 코드
CD3DX12_ROOT_PARAMETER m_rootParameter[2]; m_rootParameter[0].InitAsConstants(4u, 1u);//1번 레지스터에 32비트 상수 4개할당 m_rootParameter[1].InitAsConstantBufferView(2);//2번 레지스터에 SRV Descriptor할당 CD3DX12_ROOT_SIGNATURE_DESC localRootSignatureDesc( ARRAYSIZE(m_rootParameter), //local root signature의 파라미터 개수 m_rootParameter, //파라미터 정보 1u, //static sampler개수 &staticSamplerDesc //static sampler정보 ); //이후 ROOT_SIGNATURE_DESC로 Root Signature생성
Command List에 바인딩하는 코드
pCommandList->SetComputeRootConstantBufferView( 1,//1번째 Root Parameter에 m_camera.GetConstantBuffer()->GetGPUVirtualAddress()//Descriptor가 가리키는 GPU가상주소 );//Camera CB바인딩
루트 시그니처에는 마지막으로 Descriptor Table을 넣어줄 수 있다(Table을 만들어주는게 아니라 Descriptor Heap에 대한 포인터만 만들어주는 것). Descriptor Table은 Command List에 바인딩된 Descriptor Heap의 어느 부분을 데이터로 사용할 것인가? 에 대한 정보가 담겨있다.Descriptor Table은 1DWORD를 차지한다.
생성 코드
CD3DX12_ROOT_PARAMETER m_rootParameter[3]; m_rootParameter[0].InitAsConstants(4u, 1u);//1번 레지스터에 32비트 상수 4개할당 m_rootParameter[1].InitAsConstantBufferView(2);//2번 레지스터에 SRV Descriptor할당 CD3DX12_DESCRIPTOR_RANGE ranges[1] = {}; ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 3); //Base GPU Handle부터 Descriptor1개만큼은 3번 레지스터는 SRV(Texture)이다. m_rootParameter[2].InitAsDescriptorTable(1, &ranges[0]); CD3DX12_ROOT_SIGNATURE_DESC localRootSignatureDesc( ARRAYSIZE(m_rootParameter), //local root signature의 파라미터 개수 m_rootParameter, //파라미터 정보 1u, //static sampler개수 &staticSamplerDesc //static sampler정보 ); //이후 ROOT_SIGNATURE_DESC로 Root Signature생성
Command List바인딩 코드
pCommandList->SetComputeRootDescriptorTable( 2,//2번 Root Parameter에 texture->GetGPUHandle()//Texture에 대한 GPU Handle바인딩 )
특이하게 Sampler는 Root Signature마다 1개씩 만들어 줄 수가 있다.
이때 생성하는 Sampler는 Root Signature의 크기제한에서 제외된다.
링크 <<이 링크에 있는 질문글에 따르면 Root Signature에 생성하는 Static Sampler나 Descriptor로 생성하는 Sampler나 둘다 정상 동작함을 알 수 있다.
그냥 취향에 따라서 쓰면 될듯하다. 근데, Descriptor Heap에 뭘 만드는거 자체가 귀찮은 과정인지라 나는 그냥 Static Sampler를 계속 쓸 듯하다.
다이렉트x12는 Shader에 데이터를 세팅하는 방법이 위 3가지가 있다. MicroSoft 문서에 따르면 다음과 같은 판단을 거쳐서 어떤 Root Signature아이템을 사용할지 결정하면 된다.
Root Constant > Inline Root Descriptor > Descriptor Table
성능순은 위와 같다. 그 이유는 Root Constant는 실제 데이터를 Root Signature에서 바로 얻어올 수 있는 반면, Inline Root Descriptor는 실제 데이터를 Root Descriptor->실제 메모리 순으로 참조해야하고. 또, Descriptor Table은 실제 데이터를 Descriptor Table->Descriptor Heap->실제 메모리 순으로 참조해야한다. 메모리 참조 연산이 적을수록 성능이 좋기때문에 성능차이가 나게 된다.
Root Signature는 64 개의 DWORD (2048 비트)의 크기 제한이 있다. 아마 GPU하드웨어 설계에 따른 제한일 것 같다는 추측이 든다. 그런데 앞서 언급했다 시피 Root Signature내에서 각 item의 데이터 크기는 다음과 같다.
Root Constant : 1DWORD,
Inline Root Descriptor : 2DWORD,
Descriptor Table : 1DWORD
만약에 Descriptor Table의 사용을 배제하고 Root Constant와 Inline Root Descriptor만 주구장창 사용한다면? Root Signature의 크기제한에 위반하게 되고 프로그램이 죽게 될것이다. 그러므로 Root Signature의 크기제한을 염두해두면서 Shader 데이터를 세팅해야한다.
Inline Descriptor는 Descriptor Heap의 Descriptor와 달리 일부 데이터를 포인팅하지 못한다.
그 일부 데이터는 바로 size가 정해진 데이터(예시:Texture2D)이다.
그 이유는 Inline Root Descriptor는 생성시 Size를 입력해주지 않기 때문이라고 한다.
때문에, Inline Descriptor는 CBV,raw or structured UAV or SRV로만 사용이 제한된다.
그러므로 Texture2D같은 자료형을 사용한다? 꼼짝없이 Descriptor Table을 사용할 수 밖에 없다.
Root Signature를 마스터 해서 기분이 좋다