[모던 Vulkan (C++ 버전)] #5: 메모리 관리 & 버퍼 생성 (RAII 기반 Modern C++)

모던 Vulkan (C++ 버전) 시리즈의 다섯 번째 글입니다. 지난 글(#4)에서 커맨드 버퍼, 커맨드 풀, 큐 제출 과정을 Modern C++ 스타일로 재작성했다면, 이번에는 메모리 관리와 버퍼 생성 과정을 Vulkan-HPP와 RAII를 활용해 더 단순화해보겠습니다. 이전 입문 시리즈에서는 C 스타일로 메모리 타입을 찾고, vkAllocateMemory, vkMapMemory, vkBindBufferMemory 등을 호출하는 방식을 썼습니다. 이제는 Vulkan-HPP를 통한 강타입 enum, RAII 객체, 예외 처리로 메모리 관리 로직을 개선하고, 코드 가독성을 높일 것입니다.

목표

  • Vulkan-HPP의 vk::BufferCreateInfo, vk::UniqueBuffer를 활용해 RAII 버퍼 객체 생성
  • 호스트 메모리 매핑 시 std::vector나 std::array와 같은 STL 컨테이너 데이터 복사 간소화
  • 디바이스 메모리 할당 시 vk::MemoryAllocateInfo, vk::UniqueDeviceMemory 사용
  • 메모리 타입 선택 로직을 C++ STL과 enum class 활용해 명확하고 짧은 코드로 구현
  • 메모리 바인딩, vkMapMemory, vkUnmapMemory를 RAII나 예외 처리와 연계해 안전하게 처리

기본 개념 복습

C 스타일 Vulkan에서는 다음과 같은 단계를 거쳤습니다.

  1. vkCreateBuffer로 버퍼 객체를 생성
  2. vkGetBufferMemoryRequirements로 메모리 요구사항 확인
  3. 메모리 타입 비트마스크와 메모리 프로퍼티를 바탕으로 적절한 메모리 타입 인덱스 검색
  4. vkAllocateMemory로 메모리 할당
  5. vkBindBufferMemory로 버퍼에 메모리 바인딩
  6. 필요 시 vkMapMemory로 호스트에서 데이터 쓰기/읽기

Vulkan-HPP와 Modern C++을 사용하면 다음이 달라집니다.

  • vk::UniqueBuffer를 사용해 버퍼 RAII 관리
  • 메모리 타입 인덱스 찾는 함수를 C++ STL로 깔끔하게 구현
  • vk::UniqueDeviceMemory로 디바이스 메모리를 RAII 관리
  • 예외 발생 시 try/catch로 간단히 에러 처리

코드 예제

아래 예제에서는 float 배열을 GPU 버퍼로 올린 뒤 다시 읽어오는 과정을 보여줍니다. 실제 Compute 파이프라인이나 커맨드 버퍼 제출은 다음 글에서 더 연계할 수 있습니다.

디렉토리 구조

my_vulkan_hpp_memory/
 ├─ CMakeLists.txt
 ├─ src/
 │   └─ main.cpp
 └─ build/

메모리 타입 찾기 유틸 함수

메모리 타입 인덱스를 찾는 로직을 간단한 함수로 구현할 수 있습니다.

uint32_t findMemoryTypeIndex(const vk::PhysicalDeviceMemoryProperties& memProperties,
                             uint32_t typeFilter,
                             vk::MemoryPropertyFlags properties) {
    for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
        if ((typeFilter & (1 << i)) && 
            (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
            return i;
        }
    }
    throw std::runtime_error("Failed to find suitable memory type!");
}

main.cpp 코드

#include <iostream>
#include <vector>
#include <vulkan/vulkan.hpp>
#include <cstring> // for memcpy

uint32_t findMemoryTypeIndex(const vk::PhysicalDeviceMemoryProperties& memProperties,
                             uint32_t typeFilter,
                             vk::MemoryPropertyFlags properties);

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::PhysicalDeviceMemoryProperties memProperties = chosenDevice.getMemoryProperties();

    // 버퍼 생성
    vk::BufferCreateInfo bufferInfo({}, sizeof(float)*1024, vk::BufferUsageFlagBits::eStorageBuffer, vk::SharingMode::eExclusive);
    vk::UniqueBuffer buffer = device->createBufferUnique(bufferInfo);

    // 메모리 요구사항 조회
    vk::MemoryRequirements memReq = device->getBufferMemoryRequirements(*buffer);

    // Host Visible 메모리 타입 인덱스 찾기 (Host에서 데이터 쓰기 가능)
    uint32_t memTypeIndex = findMemoryTypeIndex(memProperties, memReq.memoryTypeBits, 
                                                vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);

    vk::MemoryAllocateInfo allocInfo(memReq.size, memTypeIndex);
    vk::UniqueDeviceMemory bufferMemory = device->allocateMemoryUnique(allocInfo);

    // 버퍼에 메모리 바인딩
    device->bindBufferMemory(*buffer, *bufferMemory, 0);

    // Host에서 데이터 쓰기
    void* dataPtr = device->mapMemory(*bufferMemory, 0, memReq.size);
    float* floatData = reinterpret_cast<float*>(dataPtr);
    for (int i = 0; i < 1024; i++) {
        floatData[i] = static_cast<float>(i);
    }
    device->unmapMemory(*bufferMemory);

    // 다시 읽어와서 확인
    dataPtr = device->mapMemory(*bufferMemory, 0, memReq.size);
    floatData = reinterpret_cast<float*>(dataPtr);
    std::cout << "C[0] = " << floatData[0] << ", C[100] = " << floatData[100] << "\n";
    device->unmapMemory(*bufferMemory);

    std::cout << "Buffer created and data transferred using Vulkan-HPP!\n";
    return 0;
}

uint32_t findMemoryTypeIndex(const vk::PhysicalDeviceMemoryProperties& memProperties,
                             uint32_t typeFilter,
                             vk::MemoryPropertyFlags properties) {
    for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
        if ((typeFilter & (1 << i)) && 
            (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
            return i;
        }
    }
    throw std::runtime_error("Failed to find suitable memory type!");
}

CMakeLists.txt 예제

cmake_minimum_required(VERSION 3.10)
project(vulkan_hpp_memory)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Vulkan REQUIRED)

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

빌드 및 실행

mkdir build
cd build
cmake ..
make
./vulkan_hpp_memory

"C[0] = 0, C[100] = 100"과 같은 출력과 "Buffer created and data transferred using Vulkan-HPP!"가 나오면 성공입니다.

주요 포인트

  • vk::UniqueBuffer로 버퍼 자동 해제 처리
  • vk::UniqueDeviceMemory로 디바이스 메모리 할당 자동 관리
  • 예외 발생 시 std::runtime_error로 던져 에러 처리 단순화
  • C 스타일 대비 코드 라인 수와 복잡성 감소, 메모리 타입 찾기 로직도 명확해짐
  • RAII, 예외 처리, STL 컨테이너 활용으로 코드 품질 개선

정리 및 다음 글 예고

이번 글에서는 메모리 관리와 버퍼 생성 과정을 Vulkan-HPP 기반 RAII 패턴과 예외 처리를 적용해 재구현했습니다. 이제 데이터 업로드/다운로드, 호스트 매핑 과정도 더 직관적으로 처리할 수 있습니다.

다음 글(#6)에서는 Compute 파이프라인 및 디스크립터 셋 구성 과정을 다시 Modern C++ 스타일로 재작성해보겠습니다. SPIR-V 셰이더 모듈, 파이프라인 레이아웃, 디스크립터 풀, 디스크립터 셋 할당 등을 RAII와 예외 처리로 간소화하는 패턴을 살펴볼 예정입니다.

반응형