이 글은 Vulkan을 학습하는 개발자의 관점에서, 커맨드 풀 설계를 위한 두 가지 접근 방식(스왑체인 이미지 기반과 프레임 인 플라이트 기반)을 비교하고 정리한 기록입니다.
[Vulkan] 커맨드 풀 설계: 스왑체인 기반과 프레임 기반 구조 비교 분석
Vulkan 튜토리얼을 따라 애플리케이션을 만들다 보면 자연스럽게 특정 구조를 사용하게 됩니다. 저 또한 스왑체인 이미지 개수에 맞추어 커맨드 버퍼를 생성하는 일반적인 방식으로 학습을 시작했습니다. 하지만 이 설계가 더 복잡한 상황에서는 비효율적이라는 점을 배우게 되어, 이를 개선하는 프레임 인 플라이트(Frame-in-Flight) 기반 설계와 비교하며 학습한 내용을 정리하고자 합니다.
2026-07-19 정정: 커맨드 풀, 커맨드 버퍼, submit fence,
imageAvailable은 frame slot 기준으로 관리할 수 있지만, present가 기다리는renderFinished는 swapchain image 기준으로 관리해야 안전합니다. 자세한 이유는 renderFinished 세마포어는 왜 프레임이 아니라 이미지에 묶어야 하는가에서 설명합니다.
1. 일반적인 학습 구조: 스왑체인 이미지 기반
대부분의 튜토리얼은 아래와 같은 리소스 구성을 기반으로 렌더링 파이프라인을 구축합니다.
| 항목 | 개수 | 설명 |
|---|---|---|
| 스왑체인 이미지 | 3개 | 화면에 표시하기 위한 이미지 버퍼 |
| 프레임 인 플라이트 (FIF) | 2개 | CPU가 GPU의 작업 완료를 기다리지 않고 미리 준비할 수 있는 프레임의 최대 개수 |
| 그래픽스 큐 패밀리 | 1개 | 모든 렌더링 명령을 제출하는 통로 |
| 커맨드 풀 | 1개 | 그래픽스 큐 패밀리에 종속된 커맨드 버퍼 할당자 |
| 커맨드 버퍼 | 3개 | 스왑체인 이미지 개수와 동일하게 생성 |
이 구조에서는 vkAcquireNextImageKHR을 통해 얻은 스왑체인 이미지의 인덱스를 사용하여, 해당 인덱스에 맞는 커맨드 버퍼에 렌더링 명령을 기록합니다.
2. 스왑체인 이미지 기반 구조의 특징
장점
- 단순한 구현: 스왑체인 이미지 인덱스와 커맨드 버퍼 인덱스가 1:1로 매칭되어 코드를 이해하고 작성하기가 직관적입니다.
- 낮은 진입 장벽: 검증된 튜토리얼 패턴을 그대로 따르므로, Vulkan의 기본 동기화 객체(
Fence,Semaphore)의 역할을 이해하는 데 집중할 수 있습니다.
단점
- 리소스 소유 기준 혼합: frame slot과 swapchain image는 서로 다른 생명 주기를 가집니다. 모든 렌더링 리소스를 한 인덱스 기준에 묶으면 각 리소스의 안전한 재사용 시점을 별도로 추적하기 어려워집니다.
- 복잡한 동기화:
vkAcquireNextImageKHR가 반환하는imageIndex는 순차적이지 않습니다. 이 때문에 현재 CPU가 작업 중인 프레임(currentFrame)과imageIndex사이의 관계를 관리하기 위한 추가 로직이 필요하며, 이는 구조를 복잡하게 만듭니다. - 리소스 개수 결합:
MAX_FRAMES_IN_FLIGHT값과 무관하게 커맨드 버퍼 수가 스왑체인 이미지 개수에 결합됩니다. - 낮은 확장성: 렌더 타겟이 스왑체인 외부(예: G-Buffer, 섀도우맵)로 확장될 경우, 이미지 인덱스에 종속된 현재 구조는 큰 변경을 필요로 합니다.
3. 대안 구조: 프레임 인 플라이트 기반
커맨드 기록과 submit 완료를 frame slot 단위로 재사용하기 위해, 각 프레임 인 플라이트(Frame-in-Flight)가 커맨드 풀, 커맨드 버퍼, fence와 imageAvailable을 소유하는 구조를 적용할 수 있습니다. 반면 present가 기다리는 renderFinished는 swapchain image 단위로 분리합니다.
struct FrameResources {
VkCommandPool commandPool;
VkCommandBuffer commandBuffer;
VkFence renderFence;
VkSemaphore imageAvailable;
};
// 커맨드 기록과 submit 완료에 묶이는 리소스는 frame slot별로 생성
FrameResources frames[MAX_FRAMES_IN_FLIGHT]; // 보통 2개
// present가 사용하는 세마포어는 swapchain image별로 생성
std::vector<VkSemaphore> renderFinishedByImage(swapchainImageCount);
이 구조에서는 스왑체인 이미지 인덱스를 커맨드 버퍼 선택에 사용하지 않습니다. 현재 CPU가 작업할 프레임(currentFrame)의 FrameResources를 사용하고, renderFinished를 선택할 때만 acquire가 반환한 imageIndex를 사용합니다.
4. 프레임 인 플라이트 기반 구조의 장점
- 메모리 효율성:
MAX_FRAMES_IN_FLIGHT개수만큼의 커맨드 풀과 커맨드 버퍼만 생성하므로 메모리 사용량을 예측하고 관리하기 용이합니다. - 단순한 재사용: 특정 프레임에 대한 렌더링 명령 기록을 다시 시작할 때, 개별 커맨드 버퍼가 아닌 풀 전체를
vkResetCommandPool()으로 초기화합니다. 이는 드라이버에 더 효율적인 최적화 힌트를 제공합니다. - 명확한 재사용 기준: 커맨드 풀, 커맨드 버퍼, fence와
imageAvailable은 frame slot의 fence로 재사용 시점을 확인합니다.renderFinished는 같은 swapchain image가 다시 acquire된 시점을 기준으로 재사용합니다. - 멀티스레드 확장성: 각 프레임이 독립적인 커맨드 풀을 가지므로, 추후 멀티스레드 렌더링으로 확장하기 위한 기반이 됩니다. 예를 들어, 여러 스레드가 각자의 커맨드 풀에서
secondary command buffer를 생성하여 병렬로 명령을 기록하고, 메인 스레드에서 이를 취합합니다.
5. 동기화 과정의 이해
프레임 인 플라이트 기반 구조의 주된 동기화 흐름은 다음과 같습니다.
vkWaitForFences(): 현재 frame slot을 사용했던 이전 submit이 끝났는지 확인합니다.vkAcquireNextImageKHR():imageAvailable[currentFrame]과 함께 렌더링할imageIndex를 얻습니다.vkResetCommandPool(): fence로 안전을 확인한 frame slot의 커맨드 풀을 리셋하고 새로운 명령을 기록합니다.vkQueueSubmit():imageAvailable[currentFrame]을 기다리고renderFinishedByImage[imageIndex]를 signal하며, submit 완료를renderFence로 추적합니다.vkQueuePresentKHR(): 같은renderFinishedByImage[imageIndex]를 기다려 해당 이미지를 present합니다.
MAX_FRAMES_IN_FLIGHT = 2, 스왑체인 이미지 개수 = 3 구성은 흔히 사용하는 예시입니다. 중요한 것은 모든 리소스를 한 인덱스에 맞추는 것이 아니라, 각 리소스를 실제 재사용 보장에 맞는 기준으로 관리하는 것입니다.
6. 정리하며
스왑체인 이미지 기반 구조는 Vulkan의 기본 개념을 익히는 데 좋은 시작점입니다. 프레임 인 플라이트 기반 커맨드 기록은 CPU 작업 단위와 submit 완료 추적을 명확하게 만드는 유용한 대안이지만, 모든 리소스가 frame slot 소유가 되는 것은 아닙니다. frame slot과 swapchain image의 서로 다른 생명 주기를 구분하는 것이 동기화 설계의 핵심입니다.
이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.