[vk] Vertex buffers - Vertex buffer creation

2021. 8. 14. 03:19그래픽스/vk

1. Introduction

- Vulkan의 버퍼들은 그래픽 카드에의해 읽힐 수 있는 임의의 데이터를 저장하는데 사용되는 메모리 영역이다.

- 버퍼들은 vertex data를 저장하는데 사용할 수 있으며, 현재 챕터에서 다룰것이다.

- 또한 버퍼들은 많은 목적으로 앞으로의 챕터에서도 보게될것이다.

- 이때까지 다룬 Vulkan objects과는 다르게 버퍼들은 자동적으로 메모리를 할당하지 않는다.

- Vulkan API가 프로그래머에게 대부분 모든것들에 대해 통제권을 주며, 메모리 관리 또한 이 중 하나이다.

(The work from the previous chapters has shown that the Vulkan API puts the programmer in control of almost everything and memory management is one of those things.)

 

 

 

 

2. Buffer creation

- 버퍼를 만들 함수 createVertexBuffer 를 추가하고

- initVulkan 에서  createCommandBuffers  바로 이전에 호출하자.

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
    createVertexBuffer();
    createCommandBuffers();
    createSyncObjects();
}

...

void createVertexBuffer() {

}

- 먼저 VkBufferCreateInfo 구조체를 채워야 할 필요가 있다.

VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = sizeof(vertices[0]) * vertices.size();

- size : 버퍼의 사이즈를 명시한다.

- - - - sizeof 를 사용하여 vertex data의 바이트 사이즈를 명시하면된다.

bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;

- usage : 데이터를 사용하는 목적을 나타낸다.

- - - - 이것은 bitwise or 을 사용하여 여러개의 목적을 명시할 수 있다.

- - - - 현재 챕터에서는 vertexbuffer 사용 목적으로 명시하고,

- - - - 다른 사용 목적은 향후 챕터에서 다룰것이다.

bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

- sharingMode : swapchain 이미지와 같게, buffers은 또한 특정 queue family에 의해 소유될 수 있음.

    (Just like the images in the swap chain, buffers can also be owned by a specific queue family )

- - - - 또한 동시에 여러개에 의해 공유되어질 수 있다.

    (or be shared between multiple at the same time. )

- - - -  튜토리얼에서는 graphics queue에서만 사용할것이므로, exclusive 모드로 접근할것이다.

    (The buffer will only be used from the graphics queue, so we can stick to exclusive access.)

 

- flags : 희소 버퍼 메모리(sparse buffer memory) 를 설정하는데 사용하며, 지금당장은 관계없음

- - - - 그러므로 기본값 0 으로 남겨둠 

 

- 이제 이 구조체를 vkCreateBuffer 에 파라미터로 넘겨 버퍼를 생성할 수 있다.

- class member로 buffer의 핸들을 보유할 vertexBuffer 를 선언하자. 

VkBuffer vertexBuffer;

...

void createVertexBuffer() {
    VkBufferCreateInfo bufferInfo{};
    bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bufferInfo.size = sizeof(vertices[0]) * vertices.size();
    bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

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

- 이 버퍼는 rendering commands에서 사용될것이므로, 프로그램 끝까지 계속 유지해야한다.

- 그리고 이것은 swapchain에 의존하지 않으므로 cleanup 에서 제거해야한다.

void cleanup() {
    cleanupSwapChain();

    vkDestroyBuffer(device, vertexBuffer, nullptr);

    ...
}

 

 

 

 

3. Memory requirements

- 이제 버퍼는 만들어졌지만, 실제로 메모리가 할당된것은 아니다.

- 버퍼를 위한 메모리할당의 첫 스텝은 메모리 요구사항을 쿼리하는것이다.

- 적절한 이름이 붙은 vkGetBufferMemoryRequirements 함수를 사용한다.

 

 

 

 

3.1 VkMemoryRequirements

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

- VkMemoryRequirements 이 구조체는 3개의 필드가 있다.

  • size: The size of the required amount of memory in bytes, may differ from bufferInfo.size.
  • alignment: The offset in bytes where the buffer begins in the allocated region of memor, depends on bufferInfo.usage and bufferInfo.flags.
  • memoryTypeBits: 버퍼를 위해 지원되는 메모리 타입들에 대한 비트 마스크, i번째 비트는 PhysicalDeviceMemoryProperties의 i번째 메모리 타입을 의미(Bit field of the memory types that are suitable for the buffer.)

 

- memoryTypeBits : 해당 버퍼를 지원하는 메모리 타입이 physicalDeviceMemoryProperties의 메모리중 어느것인지 알려줌.

(memoryTypeBits is a bitmask and contains one bit set for every supported memory type for the resource. Bit i is set if and only if the memory type i in the VkPhysicalDeviceMemoryProperties structure for the physical device is supported for the resource.)

 

- 그래픽 카드들은 다른 타입의 메모리를 할당할 수 있다.

- 각 타입은 허용된 operations 및 performance characteristics 에 따라 다양하다.

- 우리는 사용할 적절한 타입을 찾기 위해 우리의 응용프로그램의 요구사항버퍼의 요구사항을 결합할 필요가 있다. 

- 이를 위해 새로운 함수 findMemoryType 를 만들어보자.

 

 

 

 

3.2 findMemoryType

uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {

}

 

 

 

 

3.2.1 VkPhysicalDeviceMemoryProperties

- 첫번째로, vkGetPhysicalDeviceMemoryProperties 함수를 사용하여, 

- 이용가능한 타입에 대한 정보를 쿼리할 필요가 있다.

- 이러한 정보는 VkPhysicalDeviceMemoryProperties  구조체에 저장된다.

VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

구조체 필드

memoryTypeCount : memoryTypes의 배열에서 유효한 요소의 수 

-  memoryTypes : VK_MAX_MEMORY_TYPES 크기의 배열, VkMemoryType 구조체를 내용물로 가짐

- - - - memoryHeaps에 의해 지정된 힙에서 할당된 메모리에 접근 할 수 있는 메모리 타입을 기술하는 구조체임.

- - - - 0~count 까지 유효한 요소

-  memoryHeapCount :memoryHeaps의 배열에서 유효한 요소의 수 

memoryHeaps : VK_MAX_MEMORY_HEAPS 크기의 배열, VkMemoryHeap 구조체를 내용물로가짐

- - - - 메모리를 할당할 수 있는 메모리 힙을 기술하는 구조체임

- - - - 0~count 까지 유효한 요소


- Memory heaps 는 VRAM과 같은 뚜렷한 메모리 자원이고 VRAM이 부족할때의 RAM의 스왑공간(swap space)이다. 

    (Memory heaps are distinct memory resources like dedicated VRAM and swap space in RAM for when VRAM runs out.)

- 다양한 메모리타입(The different types of memory)이 이런 힙들에 존재함.
- 지금당장 우리는 메모리의 타입에대해서만 고려할것이고, 힙에대해서는 관심이 없지만
- 힙을 사용하는것은 성능에 영향을 미치기 때문에 향후 챕터에서 다룸.

 

 

 

3.2.2 check 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 파라미터는 VkMemoryRequirments의 memoryTypeBits를 넘겨주며, 버퍼 요구사항을 만족하는 메모리를 알려준다.

- 이 코드의 의미는, 적절한 메모리타입의 인덱스를 간단한 비트연산으로 찾을 수 있음을 의미한다. 

 

 

3.3.3 check property

- 하지만 vertexbuffer에 적합한 메모리 타입에만 흥미있는것은 아니다.

- 우리는 또한 vertex data를 해당 메모리에 쓸 수 있어야함. (write)

- memoryTypes 배열은 VkMemoryType 구조체로 구성되어있다.

- 또한 이 구조체는 각각의 메모리타입의 heap 과 properties을 지정한다.

- properties은 메모리의 특별한 기능이며, 다음과 같이 표시된다.

- - - - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT : CPU에서 쓸수있도록 매핑할 수 있음. (공유 메모리)

- - - - (또는 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)

- We'll see why when we map the memory.

 

- 이제 루프를 property를 지원하는지 체크하도록 수정할 수 있다.

for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
    if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
        return i;
    }
}

- 우리는 한가지 이상의 property가 일치하기를 원하므로, AND 연산을 통해 0인지 검사하는게 아니라

- 원하는 비트필드와 일치하는지 검사해야한다.

- 만일 메모리타입이 버퍼와 적합하면 그러면 모든 property가 적합한지검사하고

- 필요한것이 맞으면 인덱스를 리턴하고 그렇지않으면 예외를 throw 한다.

VkMemoryPropertyFlags
  • VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT bit specifies that memory allocated with this type can be mapped for host access using vkMapMemory.
  • VK_MEMORY_PROPERTY_HOST_COHERENT_BIT bit specifies that the host cache management commands vkFlushMappedMemoryRanges and vkInvalidateMappedMemoryRanges are not needed to flush host writes to the device or make device writes visible to the host, respectively.
- 메모리에 관한 내용
https://lifeisforu.tistory.com/409

https://lifeisforu.tistory.com/412
https://stackoverflow.com/questions/51624650/vulkan-memoryheaps-and-their-memorytypes/51625266

 

 

 

 

4. Memory allocation

- 이제 적절한 메모리 타입을 결정할 수 있는방법이 있으므로, VkMemoryAllocateInfo  구조체를 채워넣어

- 실제 메모리를 할당할 수 있다.

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

- 메모리 할당은 간단하게 size 와 type만 명시해주면되며

- 이 두값은 vertex buffer의 요구사항과 희망하는 property에서 파생된다.

- 메모리를 핸들할 클래스맴버를 만들고, vkAllocateMemory를 통해 할당한다.

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;

...

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

- 만일 메모리할당이 성공적이면, 이제 이 메모리를 vkBindBufferMemory함수를 통해 버퍼와 연결할 수 있다.

vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);

- 첫번째부터 세번째 파라미터까지는 자명하다(self-explantory)

- 파라미터 4는 메모리 영역에서의 offset을 의미한다.(버퍼에 바운딩된)

- - - - memoryOffset 바이트 부터 시작하여 메모리의  VkMemoryRequirements::size 바이트 수 만큼 바인딩됨

- - - - 이 메모리는 vertex buffer위해 할당되었기 때문에 offset은 간단하게 0으로 설정하였다.

- - - - 만일 offset이 0가 아니라면, memRequirements.alignment 로 나눌 수 있어야할 필요가 있다.(정렬되어있어야함)

 

- c++의 동적 메모리 할당처럼, 메모리 또한 어느 지점에서 해제해줘야한다.

- 메모리는 buffer object에 바인딩된 메모리는 버퍼가 사용되지 않을때 해제할 수 있으므로

- buffer를 먼저 제거한후 해제해야한다.

void cleanup() {
    cleanupSwapChain();

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

 

 

 

 

5. Filling the vertex buffer

- 이제 vertex data를 buffer에 옮겨야한다.

- 일단 vkMapMemory를 사용하여 버퍼메모리를 CPU에 접근 가능한 메모리에 매핑해야한다.

    ( mapping the buffer memory)

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

 

- 이 함수는 offset 과 size에 의해 정의된 특정한 메모리 리소스의 영역에 접근하는것을 허락해준다.

- 이 offset과 size는 여기에서 각각 0bufferInfo.size 이다.

- 이것은 또한 특별한값 VK_WHOLE_SIZE 로 명시할 수 잇는데, 모든 메모리를 매핑한다는것이다.

- 파라미터 5 는 특정한 flags를 지정하는데 사용할 수 있지만, 현재 API에서는 이용불가능하므로 0

  (향후 버전을위해 예약된것)

- 파라미터 6 는 매핑된 메모리에 대한 포인터의 출력을 지정한다.

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

- 이제 간단하게 memcpy 를 사용하여 vertex data를 mapped memory로 옮길 수 있고

- 다시  vkUnmapMemory.함수를 사용하여 unmap 할 수있다.

- 불행하게도, 드라이버는 아마 즉시 데이터를 버퍼 메모리로 복사하지 못한다. (for example because of caching.)

- 이것은 또한 쓸 버퍼가 아직 mapped memory에 보이지 않을 수 있다.

- 이것은 두가지 방법으로 대처할 수 있다.

- 우리는 첫번째 접근법을 따를것이다.

- 이것은 mapped memory가 항상 할당된 메모리의 내용물과 일치하도록 보장해준다.

- 명시적인 flushing보다는 성능이 좋지못하다는것을 명심해야하며, 왜 그런지는 다음 챕터에서 알게될것이다.

 

- Flushing memory range 또는 coherent memory heap 을 사용하는것의 

- 드라이버가 버퍼에 대한 우리의 쓰기를 인지할것이라는것을 의미하지만

- 그러나 아직 GPU에서 실제로 visible할 수 있다는 의미는 아니다.

- GPU로의 데이터 전송은 백그라운드에서 일어나는 연산이며

- 명세서에는 간단히 vkQueueSubmit 다음 호출을 기점으로 완료가 보장됨을 말해준다.

 

 

 

 

6. Binding the vertex buffer

- 남은것은 vertex buffer를 rendering operations 동안에 바인딩 시키는것이다.

createCommandBuffers 함수를 다음과 같이 확장시켜야한다.

vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

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

vkCmdDraw(commandBuffers[i], static_cast<uint32_t>(vertices.size()), 1, 0, 0);

- vkCmdBindVertexBuffers 함수는 이전장에서 설정한것과 같이 vertexbuffer 바인딩하기 위해 사용된다.

  (The vkCmdBindVertexBuffers function is used to bind vertex buffers to bindings, like the one we set up in the previous chapter.)

- 파라미터 2, 3 은 바인딩할 vertexBuffers의 배열의 첫번째 인덱스와 바인딩할 개수를 지정한다.

- 파라미터 4, 5 는 바인딩할 vertexBuffers의 배열과 vertex data를 읽을 시작점인 byte offset이다/

-  vkCmdDraw 함수 또한 버퍼 안에서의 vertices 개수로 수정해야한다 (하드코딩한 숫자 3 대신에)

 

이제 프로그램을 실행시키면 다음과 같은 삼각형이 다시 나타날것이다.

 

아래와 같이 정점 윗부분을 흰색으로 바꿔보자

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}}
};

- 다음 챕터에선 다른방식으로 vertex data를 vertex buffer로 복사할것이다.

- 그 결과 성능은 더 좋아질것이지만, 더 많은 작업을해야한다.

 

C++ code / Vertex shader / Fragment shader

 

    void createVertexBuffer() {
        VkBufferCreateInfo bufferInfo{};
        bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
        bufferInfo.size = sizeof(vertices[0]) * vertices.size();
        bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
        bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

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

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

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

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

        vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0);

        void* data;
        vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data);
            memcpy(data, vertices.data(), (size_t) bufferInfo.size);
        vkUnmapMemory(device, vertexBufferMemory);
    }
    uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
        VkPhysicalDeviceMemoryProperties memProperties;
        vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);

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

 

https://vulkan-tutorial.com/Vertex_buffers/Vertex_buffer_creation