주의 : 이 문서는 초심자 튜토리얼이 아닙니다. 기본 개념 정도는 안다고 가정합니다. 초심자는 [ Vulkan Tutorial ] 이나 [ Vulkan Samples Tutorial ] 을 보면서 같이 보시기 바랍니다.

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

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

정보 : 본문의 소스 코드는 Vulkan C++ exmaples and demos 를 기반으로 하고 있습니다. 


 

Command & Queue

 

커맨드라는 것은 디바이스에 그리기, 계산, 메모리 전송 등의 연산을 할 것을 요구하는 함수를 의미합니다. 이러한 명령은 함수 호출과 동시에 디바이스에서 실행되지는 않습니다. 큐로 보내져서 나중에 vkQueueSubmit() 이라는 함수된 후에 실제로 실행됩니다.

 

커맨드와 관련한 함수들은 "vkCmd" 라는 접두어를 가지고 있습니다. 예를 들면 vkCmdBeginRenderPass() 같은 함수가 있습니다. 이름만 봐도 딱 커맨드 관련 함수라는 것을 알 수 있습니다. 그리고 반드시 첫 번째 파라미터로 VkCommandbuffer 를 받습니다.

 

아래 코드는 1.1 명세에서의 커맨드 관련 함수 리스트를 보여 줍니다. 생각보다는 많지 않습니다.

 

 

이러한 커맨드들은 반드시 vkBeginCommandBuffer() 와 vkEndCommandBuffer() 호출 사이에서 호출되어야만 합니다. 커맨드 버퍼에 대해서는 뒤에서 다루도록 하겠습니다.

 

 

이 절의 제목에서 언급하고 있듯이 커맨드들은 큐의 종류에 의해서 카테고리화될 수 있습니다.

 

 

그런데 여기에서 여러분은 하나의 의문을 가지게 될 것입니다. 

 

저 많은 커맨드들이 어떤 큐를 위해 사용되는지 어떻게 알아?

 

다 방법이 있습니다. Vulkan 명세가 꽤 탄탄하므로 모르는 것은 거기에서 찾아 볼 수 있습니다. Vulkan 명세에 보면 커맨드에 대한 설명에는 "Supported Queue Types" 라는 항목이 있습니다. 예를 들어 vkCmdPipelineBarrier() 는 동기화 관련 커맨드인데요, 명세에서 그것을 기술하는 챕터의 마지막에 보면 커맨드 속성 테이블이 나옵니다.

 

 

vkCmdPipelineBarrier() 는 Transfer/Graphics/Compute 타입의 큐에서 사용이 가능한 커맨드라는 것을 알 수 있습니다. 다시 말해 엉뚱한 유형의 큐에 넣으면 제대로 동작할 수 없다는 것이죠.

 

이 외에도 여러 가지 속성들이 있습니다:

 

  • "Command Buffer Levels" 는 이 커맨드가 삽입될 수 있는 커맨드 버퍼의 종류에 대해서 지정하고 있습니다. 자세한 건 뒤에서 언급하도록 하겠습니다.
  • "Render Pass Scope" 는 RenderPass 범위 안에서 정의되어야 하느냐를 의미합니다. vkCmdBeginRenderPass() 호출과 vkCmdEndRenderPass() 호출 사이에 있어야 한다는 의미입니다.
  • "Pipeline Type" 은 이 커맨드가 사용될 수 있는 파이프라인 스테이지의 종류를 의미합니다. 자세한 건 다른 글에서 언급하도록 하겠습니다.

 

Command Buffer & Levels

 

그런데 커맨드의 종류와 양이 많기 때문에 적절히 그룹화해서 관리할 필요가 있습니다. 예를 들어 Shadow 패스를 위한 모두 모을 수도 있고 G-Buffer 패스를 위한 커맨드를 모두 모을 수도 있습니다. 이렇게 커맨드를 그룹화할 수 있는 버퍼가 바로 커맨드 버퍼입니다. 위에서 언급했던 vkBeginCommandBuffer() 와 vkEndCommandBuffer() 가 커맨드 버퍼를 만들기 위한 함수들입니다.

 

출처 : [ 2 ].

 

 

그런데 커맨드 버퍼에는 크게 두 종류가 있습니다. 이것을 Command Buffer Levels 라 부릅니다( 여전히 양키들의 네이밍 센스는 어렵군요 ). 하나는 프라이머리( primary ) 커맨드 버퍼이고 다른 하나는 세컨더리( secondary ) 커맨드 버퍼입니다. 프라이머리 커맨드 버퍼는 큐에 직접적으로 서밋되는 커맨드 버퍼입니다. 세컨더리 커맨드버퍼는 프라이머리 커맨드 버퍼에 의해 간접적으로 실행될 수 있으며, 큐에 직접적으로 서밋되지 않습니다.

 

출처 : [ 2 ].

 

세컨더리 커맨드 버퍼는 프라이머리 커맨드를 위한 vkBeginCommandBuffer() 호출과 vkEndCommandBuffer() 호출 사이에서 실행 커맨드인 VkCmdExecuteCommands() 라는 커맨드를 삽입함으로써 실행됩니다. 나중에 프라이머리 커맨드 버퍼가 실제로 실행되는 중에 그것들이 실행되는 것이죠.

 

이런 세컨더리 커맨드 버퍼는 재사용이 가능하지만 커맨드 버퍼끼리 상태를 상속해 주지 않습니다. 예를 들어 A 와 B 가 세컨더리 커맨드 버퍼인데 A 의 상태가 B 의 상태에 전이되지 않는다는 것입니다.

 

그런데 이 세컨더리 커맨드 버퍼는 왜 사용하는 것일까요? 명세에서는 이유에 대해서 언급하지는 않습니다. 매우 안타까운 일이죠. 구글링을 해 보니 그 이유에 대해서 갑론을박을 하고 있더군요.

 

어쨌든 납득이 갈 수 있는 한도 내에서 정리해 보면 그 이유는 다음과 같습니다. 세컨더리 커맨드 버퍼를 사용하는 가장 큰 이유는 재사용성과 효율성을 높이기 위해서입니다.

 

  • 재사용성 : 세컨더리 커맨드 버퍼는 한 번 만들어 놓으면 다른 프라이머리 커맨드 버퍼에서 사용될 수 있습니다.
  • 효율성 : 프라이머리 커맨드 버퍼를 만들고 있는 도중에 다른 스레드에서 세컨더리 커맨드 버퍼를 만들 수 있습니다.

 

Secondary Command Buffer Sample

 

예제를 통해 세컨더리 커맨드 버퍼를 실행하는 방법에 대해서 설펴 보겠습니다. "multithreading" 샘플에서는 Background 와 UI 를 세컨더리 커맨드로 렌더링합니다. 

 

 

아래의 updateCommandBuffers() 는 프라이머리 커맨드 버퍼를 채웁니다. 그 와중에 vkCmdExecuteCommands() 함수를 호출해 실행 커맨드를 프라이머리 커맨드 큐에 삽입하고 있습니다.

 

 

실제로 서밋은 프라이머리 커맨드 버퍼에 대해서만 수행하죠.

 

 

이 예제는, 프라이머리 커맨드 버퍼가 GPU 에서 실행될 때, SkyBox, UFO, UI 를 순차적으로 그리도록 만듭니다.

 

정리

 

커맨드는 디바이스에 명령을 내리기 위한 함수들이며 "vkCmd" 로 시작합니다. 각 큐마다 사용할 수 있는 커맨드의 종류는 다르며, 명세의 "Supported Queue Types" 에는 그 커맨드가 사용될 수 있는 큐의 종류가 명시되어 있습니다.

 

커맨드 버퍼는 커맨드들의 집합이며, 프라이머리 커맨드 버퍼와 세컨더리 커맨드 버퍼로 나뉩니다. 프라이머리 커맨드 버퍼는 큐에 직접적으로 추가되며 세컨더리 커맨드 버퍼를 실행할 수 있습니다. 세컨더리 커맨드 버퍼는 큐에 직접적으로 추가되지 않습니다.

 

세컨더리 커맨드 버퍼를 사용하면 재사용성과 효율성을 높일 수 있습니다.

 

참고 자료

 

[ 1 ] Vulkan 1.1 Specification, Khronos.

 

[ 2 ] Engaging the Voyage to Vulkan, NVidia.

+ Recent posts