본문 바로가기

ComputerGraphics/Vulkan

[Vulkan] Vertex Buffer

728x90
반응형

Vertex Buffer ~ find memory type 날라감

 

 

버퍼까지는 잘 만들었지만, 아직 메모리는 할당되지 않았습니다. 

vkGetBufferMemoryRequirements함수는 메모리 요구사항을 query(질의)하는 함수입니다.

(createVertexBuffer에 작성하면 됩니다. ) 

VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements);

VkMemoryRequirements는

size, aligment, memoryTypeBits 3가지 필드를 가지고 있습니다. 

 

 

버퍼의 요구사항, application의 요구사항을 조합해서 올바른 memory type을 선택해야됩니다. 이를 위해서 findMemoryType이라는 함수를 생성해봅시다.

uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
    //...
}

 

 

첫 번째로

메모리 type에 대한 정보를 가져옵시다. (여기서 부터는 다시 findMemoryType에 ) 

VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

 

VkPhysicalDeviceMemoryProperties

  • memoryHeaps: VRAM or RAM(VRAM이 부족할 경우) 과 같은 물리적으로 구분된 메모리 자원을 의미합니다. 
  • memoryTypes : Heap 안에 존재하는 메모리 type을 나타냅니다.

 

Buffer에 적합한 메모리 타입을 찾아봅시다.

for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
    if (typeFilter & (1 << i)) {
        return i;
    }
}
throw std::runtime_error("failed to find suitable memory type!");

 

typeFilter에 되어있는 bit mask를 통해서 올바른 메모리 type을 찾을 수 있습니다. 

 

 

하지만 buffer에만 맞는 메모리 type이 아니라 CPU에서도 해당 메모리에 접근할 수 있어야합니다.

그렇기때문에

VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT  

VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 속성을 포함하는 것도 추가해야됩니다.

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!");

 

 

Memory Allocation 

이제 메모리 type을 찾을 수 있으니, 메모리를 실제로 할당해봅시다.

VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(
    memRequirements.memoryTypeBits,
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
);

 

메모리 할당은 크기와 타입만 지정해주면 되므로 간단합니다.

이 두 값은 위에서 만들어준 finMemoryType과 memoryRequirement에서 유도됩니다. 

 

 

이제 handle을 만들고, 메모리를 할당해봅시다.

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;

...

if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate vertex buffer memory!");
}

 

메모리 할당이 성공했다면, 이제 이 메모리를 buffer에 연결(bind)할 수 있습니다.

vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);

 

네 번째 파라미터는 메모리 영역 내의 오프셋입니다.

이 메모리는 vertex buffer 전용으로 할당된 것이므로, 오프셋은 0입니다.
(만약 오프셋이 0이 아니라면, memRequirements.alignment의 배수여야 합니다.)

 

이 handle 또한 cleanup에 추가해줘야합니다.

void cleanup() {
    cleanupSwapChain();

    vkDestroyBuffer(device, vertexBuffer, nullptr);
    vkFreeMemory(device, vertexBufferMemory, nullptr);
}

 

Filling the Vertex Buffer

buffer를 만들어주고 binding까지 해줬으니, vertex data를 buffer에 복사해봅시다.

 

vkMapMemory를 통해서 buffer메모리에 cpu가 접근 가능한 영역으로 맵핑합니다. 

void* data;
vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);

 

이제 memcpy를 사용해 vertex data를 매핑된 메모리로 복사하고, vkUnmapMemory로 매핑을 해제하면 됩니다.

void* data;
vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);
memcpy(data, vertices.data(), (size_t)bufferInfo.size);
vkUnmapMemory(device, vertexBufferMemory);

 

 

드라이버는 데이터를 즉시 실제 buffer 메모리에 복사하지 않을 수 있습니다. 

이 문제를 해결하기 위해서 두 가지 방법이 존재합니다. 우리는 첫번째 방법을 사용합니다. 

  • VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 사용
  • flush를 사용

 

메모리를 플러시하거나 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 를 사용하는 것은 드라이버가 buffer에 쓴 내용을 인지하도록 만드는 것입니다.

그러나 그 내용이 GPU에 즉시 보이는 것을 보장하지는 않습니다. 하지만 다음 vkQueueSubmit 호출 시점까지는 전송이 완료되는 것은 보장합니다. 

 

 

Binding the vertex buffer

 

마지막 단계를 렌더링 중에 vertex buffer를 binding 하는 것입니다. ( graphics pipeline에 우리의 vertex buffer를 붙혀주는 느낌) 

이를 위해 recordCommandBuffer 함수를 확장해봅시다. 

vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);

vkCmdDraw(commandBuffer, static_cast<uint32_t>(vertices.size()), 1, 0, 0);


vkCmdBindVertexBuffers 함수는 버텍스 버퍼를 바인딩 슬롯(binding) 에 연결합니다.  

첫 번째, 두 번째 매개변수는 바인딩 시작 인덱스바인딩 count를 지정합니다.

마지막 매개변수는 vertex data를 읽기 시작할 byte 단위 오프셋 배열입니다.

 

vkCmdDraw 호출도 수정해줍시다.

 

 이제 실행시켜보면 익숙한 삼각형이 보일 것입니다.

 

 

우리가 vertex data를 잘 주고 있는건지 확인하기 위해서 색상을 바꿔봅시다

const std::vector<Vertex> vertices = {
    {{ 0.0f, -0.5f }, {1.0f, 1.0f, 1.0f}},  // 아래쪽 중앙 - 흰색
    {{ 0.5f,  0.5f }, {0.0f, 1.0f, 0.0f}},  // 오른쪽 위 - 초록색
    {{-0.5f,  0.5f }, {0.0f, 0.0f, 1.0f}},  // 왼쪽 위 - 파란색
};

 

 

좋습니다~

 


Staging Buffer

지금 우리가 사용 중인 버텍스 버퍼는 정상적으로 작동하긴 하지만,
CPU가 접근할 수 있는 메모리 타입은 GPU가 읽기엔 최적이 아닐 수 있습니다.

 

그래픽 카드 입장에서 최적인 메모리는 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 속성을 가진 메모리이며,
전용 GPU에서는 CPU가 이 메모리에 직접 접근할 수 없습니다.

 

이번 챕터에서는 두 개의 버텍스 버퍼를 만들 것입니다

 

Staging Buffer: cpu에서 접근 가능한 memory

여기에 vertex array의 data를 먼저 업로드합니다.

 

Final Vertex Buffer: (GPU에 최적화된) device local memory

stating buffer의 내용을 복사합니다. 

 

Transfer queue 

buffer 복사 명령은 전송 작업(transfer operations) 을 지원하는 큐 패밀리가 필요합니다.
해당 queue family는 VK_QUEUE_TRANSFER_BIT 플래그로 표시됩니다.

 

다행히 대부분의 경우

VK_QUEUE_GRAPHICS_BIT 또는 VK_QUEUE_COMPUTE_BIT 기능을 가진 모든 큐 패밀리암묵적으로 VK_QUEUE_TRANSFER_BIT도 지원합니다.

 

우리는 따로 transfer-only-queue(전송 전용 큐)를 찾지 않아도 됩니다.

 

Abstracting buffer creation

이번 챕터에서는 여러 종류의 buffer를 만들 예정이므로, 버퍼 생성 코드를 별도 함수로 분리하는 것이 좋습니다.
이름은 createBuffer()로 하고, 기존 createVertexBuffer() 함수의 버퍼 생성 관련 코드를 이 함수로 옮깁니다:

void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
                  VkMemoryPropertyFlags properties,
                  VkBuffer& buffer, VkDeviceMemory& bufferMemory) {
    VkBufferCreateInfo bufferInfo{};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = size;
    bufferInfo.usage = usage;
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
        throw std::runtime_error("failed to create buffer!");
    }

    VkMemoryRequirements memRequirements;
    vkGetBufferMemoryRequirements(device, buffer, &memRequirements);

    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex =
        findMemoryType(memRequirements.memoryTypeBits, properties);

    if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
        throw std::runtime_error("failed to allocate buffer memory!");
    }

    vkBindBufferMemory(device, buffer, bufferMemory, 0);
}

 

 

이렇게 createBuffer 함수를 만들었다면, createVertexBuffer함수도 간소화 할 수 있을 것 입니다. 

void createVertexBuffer() {
    VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

    createBuffer(bufferSize,
                 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 vertexBuffer,
                 vertexBufferMemory);

    void* data;
    vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, vertices.data(), (size_t)bufferSize);
    vkUnmapMemory(device, vertexBufferMemory);
}

 

Using a Staging Buffer

 

이제 createVertexBuffer 함수를 수정하여, CPU에서 접근 가능한 버퍼를 임시로만 사용하고,
실제 vertex buffer는 GPU 전용 디바이스 local memory에 생성하도록 바꿀 것입니다.

void createVertexBuffer() {
    VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    createBuffer(bufferSize,
                 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 stagingBuffer,
                 stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, vertices.data(), (size_t)bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    createBuffer(bufferSize,
                 VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
                 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                 vertexBuffer,
                 vertexBufferMemory);
}

 

이제 stagingBuffer와 stagingBufferMemory는 vertex data를 임시로 담기 위한 buffer입니다.

 

GPU만 접근 가능한 버퍼 플래그는 다음과 같습니다:

VK_BUFFER_USAGE_TRANSFER_SRC_BIT: 해당 buffer가 source로 사용될 수 있음.

VK_BUFFER_USAGE_TRANSFER_DST_BIT: 해당 buffer가 destination으로 사용될 수 있음.

 

vertexBuffer는 이제 device local memory에서 할당됩니다.
이 메모리는 보통 vkMapMemory를 사용할 수 없습니다.

 

하지만 stagingBuffer에서 vkCmdCopyBuffer를 통해 복사할 수는 있습니다.

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

 

바로 command buffer에 기록합시다.

    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    vkBeginCommandBuffer(commandBuffer, &beginInfo);

VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT: 해당 command buffer가 한번만 사용될 것임을 드라이버에 알려줘 최적화에 도움을 줍니다. 

 

 

vkCmdCopyBuffer 함수를 통해서 복사를 해줍시다. 

    VkBufferCopy copyRegion{};
    copyRegion.srcOffset = 0; // 생략 가능
    copyRegion.dstOffset = 0; // 생략 가능
    copyRegion.size = size;

    vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);
    vkEndCommandBuffer(commandBuffer);

 

 

이 command buffer는 복사를 위한 command이기 때문에, record를 바로 멈추고, 실행이 완료되길 기다립니다.

    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
    vkQueueWaitIdle(graphicsQueue);

 

 

여기서는 단순히 전송(transfer)만 완료되기를 기다리면 됩니다.

vkQueueWaitIdle를 통해서 복사 작업이 끝나는 것을 기다립니다.

( fence를 사용해서 여러 작업을 비동기적으로 관리할 수 있습니다.) 

 

    vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
    }


복사작업에서 사용된 임시 command buffer를 해제합니다. 

 

 

이제 createVertexBuffer()에서 copyBuffer()를 호출하고, staging buffer를 없애주시면 됩니다. 

createBuffer(bufferSize,
             VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
             VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
             vertexBuffer,
             vertexBufferMemory);

copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingBufferMemory, nullptr);

 

이제 실행시켜보면 역시나 잘 동작하는 것을 볼 수 있을 것입니다!! 

 

하지만 vkAllocateMemory를 매번 호출하는 것은 좋은 방식은 아닙니다

vkAllocateMemory를 통한 동시 메모리 할당의 최대 수가 정해져있고, 고성능 GPU도 ( 4096개 정도가 최대입니다 ) 

 

많은 객체에 대해 메모리를 한꺼번에 할당하려면,
하나의 메모리 블록을 여러 객체에 나눠서 사용하는 custom allocator 를 구현하는 것이 올바른 방식입니다.
이때는 여러 Vulkan 함수에서 등장한 offset 파라미터를 활용하여 메모리 내에서 구간을 나누게 됩니다.

 ( 물론 튜토리얼에서는 상관없음 )

 

이거를 사용하다고 하네요 

https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator

 

GitHub - GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator: Easy to integrate Vulkan memory allocation library

Easy to integrate Vulkan memory allocation library - GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator

github.com

 

 

Index Buffer

 

인덱스 버퍼는 정점 버퍼를 참조하는 인덱스 배열입니다.

(잘 아시겠지만, 중복 없이 하나의 정점을 여러 삼각형에서 재사용할 수 있게 해주는 친구입니다.)

 

사각형을 그려봅시다.

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, // 아래 왼쪽: 빨간색
    {{ 0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}}, // 아래 오른쪽: 초록색
    {{ 0.5f,  0.5f}, {0.0f, 0.0f, 1.0f}}, // 위 오른쪽: 파란색
    {{-0.5f,  0.5f}, {1.0f, 1.0f, 1.0f}}  // 위 왼쪽: 흰색
};
const std::vector<uint16_t> indices = {
    0, 1, 2,  // 첫 번째 삼각형
    2, 3, 0   // 두 번째 삼각형
};

정점 수가 65535개 미만이면 uint16_t로 충분합니다.

 

 

항상 하던 것과 같이, 인덱스 버퍼와 메모리를 멤버 변수로 선언합니다.

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

 

createVertexBuffer()와 거의 동일하게 동작하지만,
usage 플래그 + 인덱스 데이터를 처리한다는 점이 다릅니다.

void createIndexBuffer() {
    VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size();

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;

    // CPU에서 접근 가능한 임시 버퍼 생성
    createBuffer(bufferSize, 
                 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 stagingBuffer, stagingBufferMemory);

    // 인덱스 데이터 복사
    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, indices.data(), (size_t)bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    // GPU 전용 인덱스 버퍼 생성
    createBuffer(bufferSize,
                 VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
                 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                 indexBuffer, indexBufferMemory);

    // 스테이징 버퍼에서 실제 인덱스 버퍼로 복사
    copyBuffer(stagingBuffer, indexBuffer, bufferSize);

    // 스테이징 버퍼 제거
    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}

 

vertex buffer 정리할 때 같이 정리해줍시다. 

void cleanup() {
    cleanupSwapChain();

    vkDestroyBuffer(device, indexBuffer, nullptr);
    vkFreeMemory(device, indexBufferMemory, nullptr);

    vkDestroyBuffer(device, vertexBuffer, nullptr);
    vkFreeMemory(device, vertexBufferMemory, nullptr);

    ...
}

 

 

Using an index buffer

 

인덱스 버퍼를 사용하여 렌더링하려면, recordCommandBuffer 함수에 두 가지 변경이 필요합니다.

 

1. index buffer binding

vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);

vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16);

2. chaing drawing command

vkCmdDrawIndexed(commandBuffer,
    static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);

 

따란 드디어 indexing을 사용해서 사각형을 그렸다!! 

728x90
반응형

'ComputerGraphics > Vulkan' 카테고리의 다른 글

[Vulkan] Depth Buffer  (0) 2025.05.15
[Vulkan] Texture Mapping  (0) 2025.05.14
[Vulkan] Uniform Buffer  (0) 2025.05.14
[Vulkan]Vulkan_삼각형그리기_Presentation(2/5)  (1) 2025.04.26
[Vulkan] Vulkan_삼각형그리기_Setup(1/5)  (0) 2025.04.25