[모던 Vulkan (C++ 버전)] #4: 커맨드 버퍼, 커맨드 풀, 큐 제출 (RAII 기반)

모던 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에 실행할 명령을 기록하는 공간입니다. 기본 흐름은 다음과 같습니다.

  1. 커맨드 풀 생성 (Command Pool)
  2. 커맨드 풀로부터 커맨드 버퍼 할당 (Command Buffer)
  3. 커맨드 버퍼에 vkBeginCommandBuffer ~ vkEndCommandBuffer로 명령 기록
  4. 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 등을 통해 메모리 관리 코드를 간소화해볼 것입니다.

반응형