[모던 Vulkan (C++ 버전)] #3: 물리 디바이스 선택 및 로지컬 디바이스, 큐 확보 (RAII 적용)

모던 Vulkan (C++ 버전) 시리즈의 세 번째 글입니다. 지난 글(#2)에서는 Vulkan-HPP와 Modern C++ 스타일을 적용해 인스턴스를 생성하고 Validation Layer와 익스텐션 설정 과정을 간소화했습니다. 이제 인스턴스가 준비되었으니, 시스템에 장착된 GPU(물리 디바이스)를 탐색하고, Compute 또는 Graphics에 적합한 큐 패밀리를 선택한 뒤 로지컬 디바이스를 생성하고 큐를 확보하는 과정을 C++ RAII 패턴을 활용해 다시 구현해보겠습니다.

입문 시리즈에서 C 스타일 코드로 했던 과정을 이제 Vulkan-HPP와 RAII(Unique*), 예외 처리 등을 통해 더 깔끔하고 유지보수하기 쉬운 형태로 재작성합니다.

목표

  • 인스턴스가 준비되었다고 가정하고, 물리 디바이스(Physical Device) 리스트를 얻어 GPU를 하나 선택
  • computeQueueFamilyIndex나 graphicsQueueFamilyIndex 등 원하는 큐 패밀리를 탐색
  • vk::DeviceCreateInfo로 로지컬 디바이스 생성
  • vk::UniqueDevice로 디바이스를 RAII 관리
  • vk::Device::getQueue로 큐 핸들 얻기
  • 예외 처리 모드 활용 시 디바이스 생성 실패 시 예외 처리

기본 개념 복습

이전에는 다음과 같은 흐름으로 진행했습니다.

  1. vkEnumeratePhysicalDevices로 GPU 리스트 얻기
  2. 반복문 돌며 조건(예: Compute 기능 지원)을 만족하는 디바이스 선택
  3. 큐 패밀리 열람 후 적절한 인덱스 찾기
  4. vkCreateDevice로 로지컬 디바이스 생성, vkGetDeviceQueue로 큐 획득

이제 Vulkan-HPP에서는 instance.enumeratePhysicalDevices(), physicalDevice.getQueueFamilyProperties() 등을 활용해 std::vector와 C++ 스타일로 처리합니다.

코드 예제

디렉토리 구조

my_vulkan_hpp_device/
 ├─ 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;
    }

    // Compute 가능한 큐 패밀리를 지원하는 디바이스를 하나 선택
    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); 
    // 확장이나 Layer를 추가하고 싶다면 deviceCreateInfo에 추가 가능

    try {
        vk::UniqueDevice device = chosenDevice.createDeviceUnique(deviceCreateInfo);

        // 큐 획득
        vk::Queue computeQueue = device->getQueue(computeQueueFamilyIndex, 0);

        std::cout << "Device and compute queue successfully created with Vulkan-HPP!\n";
        // device, instance가 범위를 벗어나면 자동으로 vkDestroyDevice, vkDestroyInstance 호출
    } catch (const vk::SystemError& err) {
        std::cerr << "Failed to create device: " << err.what() << "\n";
        return 1;
    }

    return 0;
}

CMakeLists.txt 예제

cmake_minimum_required(VERSION 3.10)
project(vulkan_hpp_device)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Vulkan REQUIRED)

add_executable(vulkan_hpp_device src/main.cpp)
target_include_directories(vulkan_hpp_device PRIVATE ${Vulkan_INCLUDE_DIRS})
target_link_libraries(vulkan_hpp_device Vulkan::Vulkan)

빌드 및 실행

mkdir build
cd build
cmake ..
make
./vulkan_hpp_device

정상적으로 GPU와 큐를 선택했다면 “Device and compute queue successfully created with Vulkan-HPP!”가 출력됩니다.

주요 포인트

  • std::vector와 Vulkan-HPP 메서드(.enumeratePhysicalDevices(), .getQueueFamilyProperties()) 조합으로 자원 탐색 코드 간결화
  • vk::UniqueDevice로 디바이스 RAII 관리 → 디바이스 파괴 코드 필요 없음
  • 예외 던지는 모드에서 디바이스 생성 실패 시 try/catch로 에러 처리 단순화
  • C 스타일 대비 코드 라인 수와 복잡성 감소

정리 및 다음 글 예고

이번 글에서는 물리 디바이스 선택, 로지컬 디바이스 생성, 큐 획득 과정을 C++ RAII, 예외 처리, Vulkan-HPP를 활용해 재작성했습니다. 코드가 한결 깔끔해지고, 메모리 관리 및 에러 처리가 단순화되는 것을 확인할 수 있습니다.

다음 글(#4)에서는 커맨드 풀, 커맨드 버퍼, 큐 제출 과정을 다시 Modern C++ 스타일로 재구현해보겠습니다. RAII를 통해 커맨드 버퍼와 커맨드 풀을 자동 정리하고, 에러 처리 역시 예외로 단순화하는 패턴을 살펴볼 예정입니다.

반응형