주의 : 초심자 튜토리얼은 아닙니다. 그러므로, 실제 API 호출 용례를 알고자 한다면, 샘플이나 튜토리얼을 찾아서 확인해 보세요.

주의 : 완전히 이해하고 작성한 글이 아니므로 잘못된 내용이 포함되어 있을 수 있습니다.

주의 : 이상하면 참고자료를 확인하세요.



D3D 에 익숙한 사람이 벌칸을 할 때 발견하게 되는 어려운 개념 중에 하나는 디스크립터 셋 레이아웃( Descriptor Set Layout )입니다. 일단 디스크립터라는 것의 개념 자체를 모르는 상황이기 때문에 당연합니다.


사실 어려운 개념은 아니지만 생소하기 때문에 어렵게 느껴집니다. 디스크립터에 대해서 이해하기 위해서는 벌칸 명세에 있는 파이프라인 블록 다이어그램을 보는 것이 도움이 됩니다.


그림 1. Block diagram of the Vulkan pipeline. 출처 : 1 ].


그림 1 에 나타나 있듯이 디스크립터는 셰이더에 바인딩할 수 있는 리소스에 대한 정보를 의미하며 그것들의 모임이 디스크립터 셋이 됩니다.


이제 명세로부터 정확한 정의를 찾아 보도록 하죠.


디스크립터는 버퍼, 버퍼 뷰, 이미지 뷰, 조합된 이미지 샘플러와 같은 셰이더 리소스들을 표현하는 불투명( opaque ) 데이터 구조입니다. 디스크립터들은 디스크립터 셋을 구성하는데요, 그것들은 일련의 드로 커맨드들 내에서 사용할 커맨드들을 기록하는 동안 바인딩됩니다. 각 디스크립터 셋에서 칸텐트의 배열( arrangement )은 디스크립터 셋 레이아웃에 의해 결정됩니다. 그것은 어떤 디스크립터들이 저장되어 있는지를 결정합니다. 일련의 디스크립터 셋 레이아웃들은 파이프라인 레이아웃( pipeline layout )에 지정되어 있는 파이프라인에 의해 사용될 수 있습니다. 각 파이프라인 오브젝트들은 maxBoundDescriptorSets 만큼 까지의 디스크립터 셋들을 사용할 수 있습니다.


셰이더들은 디스크립터 셋과 디스크립터 셋 내에서 리소스와 디스크립터를 연결하는 바인딩 번호가 부여된( decorated ) 변수들을 통해 리소스들에 접근하게 됩니다. 디스크립터 셋들을 바운딩하기 위한 셰이더 인터페이스 매핑( shader interface mapping )에 대해서는 Shader Resource Interface 섹션에서 다루고 있습니다.


출처 : Chapter 13. Resource Descriptors, [ 1 ].


우리는 디스크립터 셋 레이아웃을 렌더패스/서브패스 개념과 유사하게 생각해 볼 수 있습니다. 렌더패스와 서브패스는 프레임버퍼의 설계도처럼 동작합니다. 렌더패스와 서브패스가 프레임버퍼의 바인딩 구조를 결정하고 실제 렌더링시에는 프레임버퍼가 바인딩됩니다. 그리고 이 프레임버퍼에는 실제 이미지 뷰들이 바인딩되어 있죠. 


마찬가지로 디스크립터 셋 레이아웃은 디스크립터 셋의 설계도라고 생각하시면 됩니다. 그리고 실제 렌더링시에는 디스크립터의 묶음인 디스크립터 셋이 바인딩되는 것입니다.


그림 2. 디스크립터 셋 레이아웃. 출처 : [ 2 ] 의 7 분 47 초 부분.


디스크립터 종류는 VkDescriptorType 에 기술되어 있습니다.



그런데 이 디스크립터라는 것은 리소스 오브젝트를 의미하고 있기 때문에 실제로는 디스크립터라는 오브젝트가 따로 존재하지는 않습니다. 디스크립터 셋에 의해서 디스크립터라는 것이 규정되는 것이죠. 마치 프레임버퍼라는 오브젝트에 이미지 뷰를 바인딩하는 것과 유사합니다.


디스크립터 셋은 디스크립터 풀( Descriptor Pool )인 VkDescriptorPool 오브젝트에 할당됩니다. 디스크립터 셋이라는 것은 셰이더의 종류가 늘어나면 많이 존재할 수 있기 때문에 풀이 필요합니다. 


풀은 외부 동기화( externally synchronized )되기 때문에 여러 스레드에서 동시에 접근해서는 안 됩니다. 그러므로 가급적이면 스레드마다 하나씩 존재하거나 직접 동기화 처리를 해야겠죠. 하지만 동기화 비용을 없애기 위해서는 스레드 별로 존재하는 것이 좋을 것입니다.


이 시점에 다음과 같은 그림을 그려 볼 수 있습니다.



그림 3. 디스크립터 풀과 디스크립터 셋.


이것을 파이프라인 스테이지와 연동시켜서 보면 다음과 같은 형태를 띠게 됩니다.


그림 4. 파이프라인 스테이지와 디스크립터 셋 레이아웃. 출처 : [ 3 ].


그런데 여기에서 하나 알아 둘 것이 있습니다. 명세에서는 파이프라인 내의 스테이지들이 같은 바인딩 번호를 통해 리소스에 접근할 수 있다고 하고 있습니다.


그림 4 를 보시면 알겠지만, 하나의 파이프라인에 여러 개의 디스크립터 셋들이 바인딩될 수 있습니다. 그러므로 그 디스크립터 셋들을 정의하면서 바인딩 번호를 동일하게 설정하면 같은 데이터에 접근하는 것이 가능합니다. 셰이더에서 레이아웃은 다음과 같은 식으로 지정됩니다.



만약 HLSL 과 DXC 를 사용한다면 register 를 통해서 디스크립터 셋의 레이아웃을 지정할 수 있습니다.



'X' 는 바인딩 번호이고, 'Y' 는 디스크립터 셋을 의미합니다. 


그런데 register 에 의존하지 않고 [[vk::binding(X[, Y])]] 와 [[vk::counter_binding(X)]] 애트리뷰트를 사용하는 것도 가능합니다. 자세한 사항은 [ HLSL to SPIR-V Feature Mapping Manual ] 에서 확인하시기 바랍니다.


튜토리얼 개념으로 쓴 글이 아니다 보니 여기에서 구체적인 API 와 코드에 대해서 집중적으로 다루지는 않았습니다. 개념을 이해하는 것이 중요하다는 생각에 정리해 봤으니 자세한 코드는 튜토리얼들에서 살펴 보시기 바랍니다. 잘못된 내용이 있으면 지적해 주시기 바랍니다.


참고자료


[ 1 ] Vulkan Specification 1.1.123. Khronos Group.


[ 2 ] Vulkan Tutorial 5: Triangle Sample Walkthrough, Qualcomm Developer Network.


[ 3 ] Siggraph 2016 - Vulkan and NVidia : the essentials.

+ Recent posts