모던 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에서는 다음과 같은 단계를 거쳤습니다.
vkCreateBuffer
로 버퍼 객체를 생성vkGetBufferMemoryRequirements
로 메모리 요구사항 확인- 메모리 타입 비트마스크와 메모리 프로퍼티를 바탕으로 적절한 메모리 타입 인덱스 검색
vkAllocateMemory
로 메모리 할당vkBindBufferMemory
로 버퍼에 메모리 바인딩- 필요 시
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와 예외 처리로 간소화하는 패턴을 살펴볼 예정입니다.
'개발 이야기 > Vulkan' 카테고리의 다른 글
[모던 Vulkan (C++ 버전)] #7: 벡터 덧셈 예제 완성 (0) | 2024.12.19 |
---|---|
[모던 Vulkan (C++ 버전)] #6: Compute 파이프라인 & 디스크립터 구성 (RAII 기반 Modern C++) (0) | 2024.12.19 |
[모던 Vulkan (C++ 버전)] #4: 커맨드 버퍼, 커맨드 풀, 큐 제출 (RAII 기반) (0) | 2024.12.19 |
[모던 Vulkan (C++ 버전)] #3: 물리 디바이스 선택 및 로지컬 디바이스, 큐 확보 (RAII 적용) (0) | 2024.12.19 |
[모던 Vulkan (C++ 버전)] #2: 인스턴스 생성 (RAII와 예외 처리 활용) (0) | 2024.12.19 |