본문으로 건너뛰기
뒤로가기

[Vulkan] 프레임 렌더링을 위한 동기화 객체 생명주기 관리

[Vulkan] 프레임 렌더링을 위한 동기화 객체 생명주기 관리

이 글은 Vulkan을 학습하는 개발자의 관점에서, 렌더링 루프의 동기화 객체(VkFence, VkSemaphore) 생명주기 관리 방법을 사실 위주로 정리한 기록입니다.


Vulkan에서 안정적인 렌더링 루프를 구현하려면 동기화 객체들의 생명주기를 올바르게 설정해야 합니다. 학습 과정에서 VkFenceVkSemaphore를 프레임마다 재사용하는 방식이 다르다는 점을 알게 되었고, 그 이유를 정리해 보았습니다. 세마포어의 생명주기 관리에 대한 참고 문서는 swapchain_semaphore_reuse입니다.

1. Per-Frame 리소스와 Per-Image 리소스의 구분

동기화 리소스는 아래 표와 같이 두 그룹으로 나눌 수 있습니다.

구분관리 단위동기화 객체개수
Per-Frame프레임 인덱스 (current_frame)VkFence, imageAvailable VkSemaphoreMAX_FRAMES_IN_FLIGHT
Per-Image스왑체인 이미지 인덱스 (image_index)renderFinished VkSemaphore스왑체인 이미지 개수

2. Per-Frame(frames in flight) 리소스: VkFenceimageAvailable 세마포어

VkFenceimageAvailable VkSemaphoreMAX_FRAMES_IN_FLIGHT 개수만큼 생성하고, current_frame 인덱스를 사용해 순환하며 재사용합니다.

CPU-GPU 동기화를 위한 VkFence

펜스는 CPU와 GPU 사이의 동기화를 위한 Vulkan의 객체입니다. 일반적인 렌더 루프에서 펜스는 이전 루프에서 제출한 커맨드가 종료되었는지 확인을 하기 위한 용도로 사용됩니다.

  1. CPU는 렌더링 루프 시작 부분에서 vkWaitForFences를 호출하여, 이전에 동일한 current_frame 인덱스를 사용해 제출했던 작업이 GPU에서 완전히 끝났는지 기다립니다.
  2. 펜스가 신호(signaled) 상태가 되면, 해당 프레임에 할당되었던 리소스들(예: VkCommandBuffer)이 모두 안전하게 재사용될 수 있음을 CPU가 확신하게 됩니다.
  3. 따라서 VkFence는 CPU가 제출한 작업(Submission) 꾸러미 전체의 완료 시점을 알려주는 역할을 하므로, 프레임 단위로 관리하는 것이 논리적인 접근입니다.

이렇게 펜스 대기를 마치고 나서 CPU는 현재 렌더 루프의 커맨드를 제출하기 위한 작업을 진행하게 됩니다. 이전 루프에서 제출한 커맨드를 기다릴 수 있는 것은 VkQueueSubmit에서 커맨드와 함께 제출한 펜스 덕분입니다. 펜스는 VkQueueSubmit으로 전달한 커맨드가 모두 처리되고 나서 신호 상태가 됩니다. 참고로 VkQueueSubmit으로 제출한 커맨드는 imageAvailable 세마포어를 대기(wait) 하고 있다가, 프레젠트 엔진에서 GPU가 해당 이미지를 모두 사용했음을 알리기 위해 imageAvailable 세마포어를 신호(signaled) 상태로 만듭니다. 그리고 이 상태가 되서야 커맨드에 기록된 렌더패스와 파이프라인 작업이 수행되는 것입니다.

그러면 우리는 여기에서 imageAvailable 세마포어는 펜스와 동일한 생명주기를 가져도 되겠다는 생각을 할 수 있게 됩니다. VkQueueSubmit으로 펜스를 함께 전달하는데, 그 내부에서 진행하는 GPU 작업 동기화를 imageAvailable 세마포어로 처리합니다. 그리고 큐에 제출한 작업이 모두 끝나면 펜스가 신호 상태가 됩니다. 따라서 imageAvailable 세마포어는 CPU가 큐에 함께 제출한 작업 꾸러미의 일부로, 펜스가 신호처리가 되었다면 해당 세마포어를 다른 작업에 사용하고 있지 않다는 것을 보장받을 수 있기 때문입니다.

정리: vkWaitForFences 함수를 호출하여 이전에 제출한 펜스가 신호될 때까지 대기한 후에 작업을 하기 때문에 사용 중인 펜스를 다시 사용할 걱정을 하지 않아도 됩니다. VkQueueSubmit에서 제출한 작업이 GPU에서 모두 종료됐을 때 펜스가 신호 상태가 되기 때문입니다. imageAvailable 세마포어는 큐에 함께 제출되어 GPU 내부의 동기화를 담당하니 펜스의 울타리 안에서 작동한다고 볼 수 있습니다. 결과적으로 프레임 개수만큼만(Per-Frame) 만들어 사용해도 동기화에 문제가 없다는 것입니다.

3. Per-Image 리소스: renderFinished 세마포어

반면 renderFinished 세마포어는 스왑체인의 이미지 개수만큼 생성하고, vkAcquireNextImageKHR로부터 받은 image_index를 사용해 관리하는 것이 중요합니다.

프레젠테이션 엔진과의 동기화

  1. vkQueueSubmit은 렌더링 작업이 끝나면 renderFinished 세마포어에 신호를 보냅니다.
  2. vkQueuePresentKHR은 이 세마포어를 기다렸다가, 신호가 들어오면 해당 이미지를 화면에 표시하도록 프레젠테이션 엔진 에 제출합니다.
  3. 중요한 점은, 프레젠테이션 엔진은 운영체제(WSI)의 일부이며, 이미지를 화면에 표시하는 작업이 언제 끝나는지 Vulkan 애플리케이션에 직접 알려주지 않는다는 것입니다.
  4. 애플리케이션이 특정 이미지가 더 이상 프레젠테이션에 사용되지 않음을 알 수 있는 유일한 방법은, 다음 렌더링 루프에서 vkAcquireNextImageKHR를 호출했을 때 바로 그 이미지의 인덱스(image_index)를 돌려받는 것입니다.

이러한 이유로 renderFinished 세마포어의 생명주기는 프레임이 아닌 이미지에 연결됩니다. image_index를 통해 해당 세마포어를 선택하면, 자연스럽게 프레젠테이션이 끝난 세마포어만 재사용하게 됩니다.

4. 잘못된 관리 시 발생하는 문제

만약 renderFinished 세마포어를 MAX_FRAMES_IN_FLIGHT 개수만 생성하고 current_frame 인덱스로 관리하면, 다음과 같은 레이스 컨디션이 발생할 수 있습니다.

5. 핵심 원칙 정리

결론적으로 저는 Vulkan의 동기화 설계를 다음 원칙으로 이해했습니다.

동기화 프리미티브의 생명주기는 그것이 동기화하는 ‘대상’ 의 생명주기를 따른다는 것입니다.

이 원칙에 따라 동기화 객체들의 생명주기를 관리하면, 복잡한 비동기 렌더링 과정에서 발생하는 동기화 문제를 해결할 수 있습니다.


이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.


공유하기:

다음 글
[Vulkan] 커맨드 풀 설계: 스왑체인 기반과 프레임 기반 구조 비교 분석