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

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

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



Interface Block


일단 HLSL 의 경우에는 일반 구조체를 사용해서 각 셰이더 스테이지( Shader Stage )의 입력과 출력을 정의할 수 있도록 하고 있습니다. 버텍스( Vertex ) 셰이더의 예를 하나 살펴 보죠.



VertexInputVertexOutput 이라는 structGlobalBuffer 라는 cbuffer 가 정의되어 있습니다. 그리고 CommonVertexShader() 함수에서는 VertexInput 을 입력으로 받아 VertexOutput 을 산출합니다.


그리고 VertexInput VertexOutput 의 각 필드)에는 시맨틱( Semantic ) 이 지정되어 있습니다. 언어에서 신택스( Syntax )는 우리말로는 "문법" 으로 표현될 수 있으며, 언어의 문법이 옳은지 판단할 때 사용될 수 있습니다. 시맨틱은 "의미" 로 표현될 수 있으며, 말 그대로 의미상 옳은 지를 판단할 때 사용될 수 있습니다. 시맨틱을 사용하는 것의 장점이라면, 변수 이름이 아니라 시맨틱으로 그 변수의 의미를 특정할 수 있다는 것입니다.


OpenGL 이나 Vulkan 같은 경우에는 셰이더의 입력이나 출력을 인터페이스( Interface )라 정의하고 있습니다. 두 개의 스테이지를 각각 노드라 생각하고 데이터가 인터페이스를 통해서 왔다갔다 한다고 생각하면 무리가 없는 설정인 것 같습니다.


그런데 그 인터페이스에 매우 많은 데이터가 왔다갔다 하면 관리하기가 힘드니까 그것을 인터페이스 블록( Block )이라는 것으로 묶을 수 있도록 하고 있습니다[ 3 ][ 4 ]. 


인터페이스 블록의 구문은 구조체를 선언하는 것과 유사합니다.


storage_qualifier block_name

{

    <define members here>

} instance_name;


storage_qualifier 로는 uniform, in, out, buffer 등이 있습니다. 


  • uniform : global shader variable.

  • in : OpenGL 3.2 이상에서 지원되는 shader input.

  • out : OpenGL 3.2 이상에서 지원되는 shader output.

  • buffer : ARB_shader_storage_buffer_object 에 의해 지원되는 storage_buffer.


몇 개의 예를 [ 3 ] 에서 가져 와 보겠습니다.



Layout Qualifiers


그런데 storage_qualifier 들 중에서는 OpenGL 3.2 이상에서만 블록 형태로 지원되는 것들이 존재하기 때문에, 3.1 까지는 그냥 변수를 사용해서 입출력 데이터를 표현해야 했습니다. 이것 때문에 layout qualifier 라는 것이 필요했죠.


layout(qualifier1​, qualifier2​ = value, ...) variable_definition


여러 개의 변수들을 만들어 놓은 다음에 그것에 순서와 특성을 부여합니다. layout 으로 묶여 있는 괄호 안 쪽에 한정자( qualifier )들을 삽입합니다.


이 한정자의 종류는 다음과 같습니다. 물론 더 많은 한정자들이 있기는 하지만, Vulkan Spec 에서 언급한 한정자들만 나열했습니다.


  • location : 애트리뷰트( attribute ) 바인딩 순서를 지정합니다.

  • component : 애트리뷰트의 컴포넌트 개수를 지정합니다.


다음과 같은 식이죠. 



이렇게 되어 있으면 프로그램에서 position, texCoord, normal 이 들어 있는 구조체를 한 번에 glBindVertexArray() 로 지정했을 때 하나의 버텍스 내부에서 순차적으로 위치가 지정된 것처럼 동작하게 됩니다. location 이 하나 증가하면 float 4 개만큼의 메모리 offset 을 가진다고 보면 됩니다. HLSL 로 치자면 layout 은 레지스터 인덱스라고 생각해도 무방할 것 같습니다.


그러므로 다음과 같이 배열을 가지고 있는 경우에는 일반적으로는 float 4 배열 요소 개수나 구조체의 크기만큼 증가하게 됩니다.



그런데 가끔 float 4 크기보다는 작은 변수들을 하나의 레지스터로 묶고 싶을 때까 있습니다. HLSL 로 치자면 packoffset 과 유사한 개념이겠네요. 이 때 component 라는 한정자를 사용하게 됩니다. 이 컴포넌트 한정자를 지정하지 않으면 기본값은 0 이라고 합니다. 그런데 component qualifier 는 OpenGL 3.2 부터 ARB_explicit_attrib_location 을 통해서 버텍스 셰이더와 프래그먼트( fragment ) 셰이더에서만 지원하는 개념입니다.


아래의 몇 가지 예가 있습니다.



어쨌든 매우 불편한 개념인데요, HLSL 을 사용하면 딱히 신경쓰실 필요는 없습니다. 왜냐하면 HLSL 은 이미 인터페이스 블록같은 개념을 구조체를 통해 지원하고 있을 뿐더러 DXC 로 컴파일하게 되면 레이아웃을 자동으로 지정해 주기 때문입니다. 게다가 DXC 에 실행인자로 "-fspv-reflect" 를 지정하면 시맨틱으로 애트리뷰트를 획득하는 것도 가능합니다.


제일 위쪽에 있는 HLSL 코드 중에 VertexInput 을 리플렉션( reflection )하는 소스 코드를 한 번 살펴 보겠습니다.



그러면 다음과 같은 결과를 얻을 수 있습니다. 저같은 경우에는 사용하기 편하게 일부러 VkVertexInputAttributeDescription 맵의 형태로 만들었습니다. 소스코드를 보면 대부분의 내용은 이해하실 수 있을 것 같네요.


그림 1.


그림 1 의 디버깅 정보에는 "TANGENT0" 시맨틱에 대한 정보가 나와 있습니다.


DXC layout


이 HLSL 에서 이 레이아웃을 강제로 지정하는 방법도 있는데요, [ 4 ] 에서는 [[vk::location(x)]] 애트리뷰트를 사용하도록 하고 있습니다. 실제로 순서를 맘대로 바꿔 보도록 하겠습니다.



그런데 vk 애트리뷰트를 사용해서 레이아웃을 결정하신다면 DXC 의 실행 인자로 "-fvk-use-dx-layout" 를 지정하시는 것을 잊으시면 안 됩니다. 안 그러면 에러가 발생합니다.


어쨌든 DXC 를 실행하면 다음과 같이 "TANGENT0" 시맨틱의 location 이 5 번으로 변경되어 있는 것을 확인하실 수 있습니다.


그림 2.


그런데, 그림 2 를 자세히 보신 분들은 알아 차렸겠지만, offset 이 변하지 않았습니다. 이것은 변수 순서가 변경되지 않았기 때문에 발생하는 문제( ? )입니다.


그러므로 이런 경우에는 offset 을 구할 때 location 순서대로 정렬한 다음에 구하는 것이 좋습니다. 물론 offset 은 구조체 내의 메모리 위치이고 location 은 GPU 레지스터에서의 인덱스와 같은 것이기 때문에 굳이 offset 을 순서와 맞출 필요는 없습니다. 하지만 헷갈릴 우려가 있고 이게 성능에 어떤 영향을 미치는 지는 잘 모르겠습니다. 그러므로 순서를 맞추는 것이 좋을 것 같습니다.


결론


Spirv-Reflect 에 대해서 분석하다가 보니, 굳이 레이아웃에 대한 분석을 하기는 했지만, 일반적으로는 아무것도 하지 말고 그냥 순서대로 레이아웃이 결정되도록 놔두는 것이 좋다는 생각이 듭니다.


참고자료


[ 1 ] Vulkan 1.1.122 Specification. Khronos Group.


[ 2 ] Layout Qualifier (GLSL), OpenGL Wiki. https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL).


[ 3 ] Interface Block (GLSL). https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL).


[ 4 ] HLSL to SPIR-V Feature Mapping Manual. https://github.com/microsoft/DirectXShaderCompiler/blob/master/docs/SPIR-V.rst.

+ Recent posts