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

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

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

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



Vulkan 명세 [ 6. Synchronization and Cache Control ] 에서 소개하고 있는 주요 동기화 메커니즘은 다음과 같습니다. 


  • Fences : 펜스는 디바이스 상에서 실행되는 어떤 태스크가 완료되었음을 호스트에게 알려주기 위해서 사용될 수 있습니다.
  • Semaphores : 세마포어는 여러 개의 큐 사이에서 리소스에 대한 접근을 제어하기 위해서 사용될 수 있습니다.
  • Events : 이벤트는 미세한 작업 단위의 동기화( fine-grained synchronization )이며, 단일 큐 내에서만 사용이 가능합니다. 작업의 순서를 지정하기 위해서 사용됩니다.
  • Pipeline Barriers : 파이프라인 베리어는 같은 큐에 제출되는 커맨드들이나 같은 서브패스 내의 커맨드들 사이에 종속성을 주입하기 위해 사용됩니다.
  • Render passes : 렌더 패스는 거의 대부분의 렌더링 태스크를 위한 유용한 동기화 프레임워크를 제공합니다. 다른 동기화 요소들을 사용하기 보다는 렌더 패스 단위로 동기화하는 것이 효율적입니다.


일단 이 문서에서는 커맨드 제어를 위해서 가장 많이 사용되는 펜스, 세마포어, 배리어에 대해서 알아보도록 하겠습니다.


[ 1 ] 에서는 동기화 개체에 대해 아래와 같이 다이어그램으로 잘 정리해 뒀습니다.


동기화 용례 : 출처 : [ 1 ].


위의 다이어그램에서는 다음과 같은 용례를 보여 줍니다.


  • 이벤트를 사용해서 커맨드 버퍼 내의 커맨드들 끼리의 동기화를 보장합니다.
  • 세마포어를 사용해 큐 사이의 동기화를 보장합니다.
  • 펜스를 사용해 호스트와 디바이스 사이의 동기화를 보장합니다.


그런데 멀티스레딩에 대해 "제대로" 공부해 보지 않은 분들은 펜스, 이벤트, 세마포어, 배리어 등의 개념에 익숙하지 않을 겁니다( 저도 그렇습니다 ). 그러므로 그 개념들에 대해서 살펴 보고 오시는 것을 추천합니다. 저같은 경우에는, 이벤트나 세마포어같은 걸 스레드 동기화에 대해 공부하면서 종종 접해 봤지만, 펜스와 배리어라는 개념은 그래픽스를 하면서 처음 봤습니다. 


동기화 개체들에 대해서 대해서 간단하게( ? ) 잘 정리한 분들이 있는데, 아래 링크를 확인하시면 좋을 것 같습니다.



사실 뮤텍스와 이벤트는 모두 세마포어의 변종이라고 할 수 있습니다. 뮤텍스는 키가 한개인 세마포어이고, 이벤트는 통지 기능을 가진 뮤텍스나 세마포어죠. 그리고 [ 2 ] 에 의하면 배리어와 펜스는 기존의 드라이버 단에 존재하던 동기화 기법이 호스트단에 노출된 것에 불과합니다.


그러므로 동기화 메커니즘에 사용되는 동기화 개체들에 대해서 사고할 때, 원래의 의미에 대해서 너무 깊게 생각하는 것은 Vulkan 에서 동기화 개체를 사용할 때 혼란의 여지를 준다고 생각합니다. 그래서 어떤 오브젝트에 대해 어떤 시점에 어떤 방식으로 동기화 개체들이 사용되는지를 이해하는 것이 중요하다고 생각합니다.


세마포어


Vulkan 에서 세마포어는 큐들에 제출된 배치들( batches submitted to queues ) 사이에 의존성을 삽입하는 데 사용될 수 있습니다. 세마포어는 시그널( signaled )과 언시그널( unsignaled ) 상태를 가집니다.


세마포어는 커맨드 배치가 완료된 후에 시그널 상태가 변할 수 있습니다. 일반적으로 어떤 배치를 실행하기 전에 세마포어가 시그널 상태로 되기를 기다렸다가, 배치를 실행하기 전에 세마포어를 언시그널 상태로 설정할 수 있습니다. 이 과정을 통해서 순서대로 배치가 실행되도록 보장하는거죠.


다음과 같이 두 개의 커맨드 버퍼가 있다고 가정해 보죠; A 와 B. 그런데 A 와 B 가 순차적으로 제출되는데 반드시 A 의 작업이 종료된 후에 B 가 제출되었으면 한다고 합시다. 그렇다면 동기화를 걸 필요가 있겠죠. 이럴 때 세마포어를 사용할 수 있습니다.


예를 들면  "deferredshadows" 샘플에서는 다음과 같은 식으로 세마포어를 사용해 커맨드버퍼들의 제출 시점을 제어합니다.



VkSubmitInfo 에다가 pWaitSemaphores 와 pSignalSemaphores 를 지정하는 것을 확인하실 수 있습니다. pWaitSemaphores 는 종료될 때까지( 시그널링될 때까지 ) 대기해야 하는 세마포어이고, pSignalSemaphores 는 자신이 완료되면 시그널링시킬 세마포어입니다.


배리어와 펜스


배리어와 펜스의 개념에 대해서는 잘 정리한 글들이 있으므로 제가 직접 정리하기 보다는 그것들을 참조하는 게 좋을 것 같습니다.


[ 번역 : Vulkan barriers expained ].

[ 번역 : Performance Tweets series : Barrier, fences, synchronization ].


커맨드 풀


[ 1 ] 에서는 커맨드 풀을 프레임 단위로 유지함으로써 커맨드 버퍼의 라이프사이클을 유지하는 수단으로 사용할 수 있다고 이야기하고 있습니다. 왜냐하면 커맨드 풀을 통째로 리셋함으로써 커맨드 버퍼를 모두 지워버릴 수 있기 때문입니다.



커맨드 풀 자체는 동기화 개체는 아닙니다. 하지만 어떻게 보면 커맨드 버퍼가 삭제되지 않도록 만드는 일종의 배리어같은 역할을 한다는 생각이 듭니다.


정리


벌칸은 여러 종류의 동기화 개체를 가지고 있습니다. 대표적인 것이 세마포어, 배리어, 펜스, 이벤트 등인데요, 각각의 사용처가 다릅니다. 적절한 위치에서 사용하는 것이 중요합니다. 


안타깝게도 이벤트같은 경우에는 자세한 설명이나 샘플을 찾아 볼 수가 없어서 여기에서 정리를 하지 못했습니다.


일단은 개념적인 부분들에 대해서 살펴 보았지만, 기회가 되면 나중에 실제로 구현해 보고 공유하도록 하겠습니다( 아직까지 렌더러 구조에 대해 고민하는 중이라 샘플을 못 만든 상태입니다 ).


참고자료


[ 1 ] Muti-Threading in Vulkan. arm Community.


[ 2 ] Performance Tweets series: Barriers, fences, synchronization. GPU Open.


[ 3 ] Vulkan 1.1 Specification. Khronos Group.

+ Recent posts