모던 Vulkan (C++ 버전) 시리즈의 네 번째 글입니다. 지난 글(#3)에서 물리 디바이스 선택 및 로지컬 디바이스, 큐 확보를 C++ RAII, 예외 처리 모드로 재작성했습니다. 이제 GPU에 명령을 전달하기 위한 커맨드 버퍼(Command Buffer)와 커맨드 풀(Command Pool), 그리고 큐 제출 과정을 Modern C++ 스타일로 다시 구현해봅시다.
입문 시리즈에서는 C 스타일로 vkCreateCommandPool, vkAllocateCommandBuffers, vkQueueSubmit 등을 호출했지만, 이번에는 vk::UniqueCommandPool, vk::UniqueCommandBuffer, vk::SubmitInfo 등 Vulkan-HPP를 적극 활용하고, RAII를 통해 자원 해제를 자동화하며 에러 처리도 단순화하는 패턴을 살펴보겠습니다.
목표
- RAII를 활용한 커맨드 풀 & 커맨드 버퍼 관리
- vk::CommandPoolCreateInfo, vk::CommandBufferAllocateInfo를 Modern C++로 처리
- vk::UniqueCommandPool, vk::CommandBuffer (또는 vk::UniqueCommandBuffer) 활용
- vk::SubmitInfo를 통한 큐 제출 시 예외 처리 모드 활용으로 에러 처리 단순화
기본 개념 복습
커맨드 버퍼는 GPU에 실행할 명령을 기록하는 공간입니다. 기본 흐름은 다음과 같습니다.
- 커맨드 풀 생성 (Command Pool)
- 커맨드 풀로부터 커맨드 버퍼 할당 (Command Buffer)
- 커맨드 버퍼에 vkBeginCommandBuffer ~ vkEndCommandBuffer로 명령 기록
- vkQueueSubmit로 커맨드 버퍼를 큐에 제출, vkQueueWaitIdle로 대기
C 스타일 코드에서는 vkCreateCommandPool, vkAllocateCommandBuffers, vkBeginCommandBuffer, vkEndCommandBuffer, vkQueueSubmit 등을 호출했습니다. Vulkan-HPP를 사용하면 vk::CommandPool, vk::CommandBuffer, vk::Queue 메서드를 C++ RAII 스타일로 처리할 수 있습니다.
코드 예제
아래 예제는 단순히 커맨드 버퍼를 할당하고 빈 명령(실제 명령은 없음)을 기록한 뒤 큐에 제출하는 흐름을 보여줍니다. 나중에 Compute 파이프라인 등을 구성하면 이 커맨드 버퍼 안에 실제 GPU 명령(Dispatch, Pipeline Bind, Descriptor Bind)을 넣을 수 있습니다.
디렉토리 구조
my_vulkan_hpp_commands/
├─ CMakeLists.txt
├─ src/
│ └─ main.cpp
└─ build/
main.cpp 코드
#include <iostream>
#include <vector>
#include <vulkan/vulkan.hpp>
int main() {
// 인스턴스 생성 (이전 글 참고)
vk::ApplicationInfo appInfo("MyVulkanApp", 1, "MyEngine", 1, VK_API_VERSION_1_3);
std::vector<const char*> layers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> extensions = { "VK_EXT_debug_utils" };
vk::InstanceCreateInfo instanceInfo({}, &appInfo, (uint32_t)layers.size(), layers.data(), (uint32_t)extensions.size(), extensions.data());
vk::UniqueInstance instance = vk::createInstanceUnique(instanceInfo);
// 물리 디바이스 선택 & 로지컬 디바이스 생성 (이전 글 참고)
auto physicalDevices = instance->enumeratePhysicalDevices();
if (physicalDevices.empty()) {
std::cerr << "No Vulkan-supported GPU found!\n";
return 1;
}
vk::PhysicalDevice chosenDevice = VK_NULL_HANDLE;
uint32_t computeQueueFamilyIndex = UINT32_MAX;
for (auto& pd : physicalDevices) {
auto queueFamilies = pd.getQueueFamilyProperties();
for (uint32_t i = 0; i < queueFamilies.size(); i++) {
if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eCompute) {
chosenDevice = pd;
computeQueueFamilyIndex = i;
break;
}
}
if (chosenDevice) break;
}
if (!chosenDevice) {
std::cerr << "No suitable GPU with compute capability found!\n";
return 1;
}
float queuePriority = 1.0f;
vk::DeviceQueueCreateInfo queueCreateInfo({}, computeQueueFamilyIndex, 1, &queuePriority);
vk::DeviceCreateInfo deviceCreateInfo({}, 1, &queueCreateInfo);
vk::UniqueDevice device = chosenDevice.createDeviceUnique(deviceCreateInfo);
vk::Queue computeQueue = device->getQueue(computeQueueFamilyIndex, 0);
// 커맨드 풀 생성 (RAII)
vk::CommandPoolCreateInfo poolInfo({}, computeQueueFamilyIndex);
vk::UniqueCommandPool commandPool = device->createCommandPoolUnique(poolInfo);
// 커맨드 버퍼 할당
vk::CommandBufferAllocateInfo allocInfo(*commandPool, vk::CommandBufferLevel::ePrimary, 1);
std::vector<vk::CommandBuffer> commandBuffers = device->allocateCommandBuffers(allocInfo);
vk::CommandBuffer commandBuffer = commandBuffers[0]; // RAII는 CommandBuffer에 직접적용이 덜 명확. UniqueCommandBuffer 지원은 일부 확장 필요.
// 커맨드 버퍼 기록 시작/종료 (여기선 명령 없음)
vk::CommandBufferBeginInfo beginInfo;
commandBuffer.begin(beginInfo);
// 실제 명령 기록: 파이프라인 바인딩, Dispatch, MemoryBarrier 등 (추후)
commandBuffer.end();
// 큐 제출
vk::SubmitInfo submitInfo({}, {}, commandBuffer);
computeQueue.submit(submitInfo);
computeQueue.waitIdle();
std::cout << "Command buffer submitted successfully with Vulkan-HPP!\n";
// device, instance, commandPool 모두 RAII로 해제됨
return 0;
}
CMakeLists.txt 예제
cmake_minimum_required(VERSION 3.10)
project(vulkan_hpp_commands)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Vulkan REQUIRED)
add_executable(vulkan_hpp_commands src/main.cpp)
target_include_directories(vulkan_hpp_commands PRIVATE ${Vulkan_INCLUDE_DIRS})
target_link_libraries(vulkan_hpp_commands Vulkan::Vulkan)
빌드 및 실행
mkdir build
cd build
cmake ..
make
./vulkan_hpp_commands
“Command buffer submitted successfully with Vulkan-HPP!”가 출력되면 성공입니다.
주요 포인트
- vk::UniqueCommandPool를 사용해 커맨드 풀 자동 해제
- 커맨드 버퍼는 현재 Unique 핸들 지원이 없으나, allocate 후 std::vector로 관리 가능. 커맨드 풀 소멸 시 커맨드 버퍼 자동 해제
- submit 시 예외 처리 모드에서 에러 발생 시 예외 던짐 (여기서는 발생 가능성 낮음)
- 코드가 C 스타일 대비 훨씬 간결하고 구조적
정리 및 다음 글 예고
이번 글에서는 커맨드 버퍼, 커맨드 풀, 큐 제출 과정을 Modern C++ 스타일로 재작성했습니다. RAII를 활용해 메모리 관리를 자동화하고, 예외 모드로 에러 처리를 단순화하는 패턴을 익혔습니다.
다음 글(#5)에서는 메모리 관리와 버퍼 생성 과정을 다시 Vulkan-HPP 기반 Modern C++로 재구현해보겠습니다. Host Visible 메모리 매핑, Device Local 메모리 할당, RAII 기반 vk::UniqueBuffer 등을 통해 메모리 관리 코드를 간소화해볼 것입니다.
'개발 이야기 > Vulkan' 카테고리의 다른 글
[모던 Vulkan (C++ 버전)] #6: Compute 파이프라인 & 디스크립터 구성 (RAII 기반 Modern C++) (0) | 2024.12.19 |
---|---|
[모던 Vulkan (C++ 버전)] #5: 메모리 관리 & 버퍼 생성 (RAII 기반 Modern C++) (0) | 2024.12.19 |
[모던 Vulkan (C++ 버전)] #3: 물리 디바이스 선택 및 로지컬 디바이스, 큐 확보 (RAII 적용) (0) | 2024.12.19 |
[모던 Vulkan (C++ 버전)] #2: 인스턴스 생성 (RAII와 예외 처리 활용) (0) | 2024.12.19 |
[모던 Vulkan (C++ 버전)] #1: Vulkan-HPP와 Modern C++ 개요 (0) | 2024.12.19 |