본문 바로가기
Vulkan

8. Vertex Buffer and Memory

by SimonLee 2024. 7. 5.

이전 챕터 [ SPIR-V ] 에서 GLSL 셰이더를 SPIR-V로 변환하고 셰이더 모듈을 만들었다.

 

이번 챕터 "Vulkan Buffer & Memory " 에서는

셰이더 모듈에 정점 데이터를 넘겨주는 부분을 구현할 예정이다.

 

OpenGL에서는 glsl 셰이더를 개발할때 정점을 VertexBuffer에 담아서 전달했다. 

이때 사용하는 함수가 " glVertexAttribPointer " 입니다.

드라이버에서 버텍스버퍼에 해당하는 size 만큼 알아서 메모리도 만들어주고 

메모리의 접근하는 방법도 설정해주었다.

 

하지만

Vulkan에서는 메모리를 할당하는 부분, 메모리 접근 하는 부분, 버퍼와 바인딩하는 부분

전부 사용자가 구현해야 한다.

임의의 데이터를 저장할수 있는 메모리 영역인 VkBuffer를 생성해보자.

 

1. 버퍼 리소스 생성

버퍼 리소스 생성에 필요한 정보 구조체 작성

// Provided by VK_VERSION_1_0
typedef struct VkBufferCreateInfo {
    VkStructureType        sType;
    const void*            pNext;
    VkBufferCreateFlags    flags;
    VkDeviceSize           size;
    VkBufferUsageFlags     usage;
    VkSharingMode          sharingMode;
    uint32_t               queueFamilyIndexCount;
    const uint32_t*        pQueueFamilyIndices;
} VkBufferCreateInfo;

- sType : VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO

- pNext : 확장판 지정 구조체 or null

- flags : VkBufferCreateFlagBits

- size : 생성할 버퍼의 총 크기 (Byte)

- usage : VkBufferUsageFlagBits, 버퍼 리소스의 용도를 정의

  버텍스 버퍼의 경우 " VK_BUFFER_USAGE_VERTEX_BUFFER_BIT  "

// Provided by VK_VERSION_1_0
typedef enum VkBufferUsageFlagBits {
    VK_BUFFER_USAGE_TRANSFER_SRC_BIT = 0x00000001,
    VK_BUFFER_USAGE_TRANSFER_DST_BIT = 0x00000002,
    VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT = 0x00000004,
    VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT = 0x00000008,
    VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT = 0x00000010,
    VK_BUFFER_USAGE_STORAGE_BUFFER_BIT = 0x00000020,
    VK_BUFFER_USAGE_INDEX_BUFFER_BIT = 0x00000040,
    VK_BUFFER_USAGE_VERTEX_BUFFER_BIT = 0x00000080,
    VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT = 0x00000100,
    ......
} VkBufferUsageFlagBits;

 

- sharingMode : 버퍼를 여러개의 큐 패밀리가 액세스 할 때 공유 모드를 정의한다.

  스왑 체인의 이미지와 마찬가지로 버퍼도 특정 큐 패밀리가 소유하거나 동시에 여러 큐 패밀리에서 공유할 수 있습니다.    버퍼는 그래픽 큐에서만 사용되므로  " VK_SHARING_MODE_EXCLUSIVE " 값을 사용한다.

- queueFamilyIndexCount : queueFamilyIndices 배열 요소의 크기를 가리킨다.

- pQueueFamilyIndices : 버퍼에 접근하는 큐 패밀리들의 배열이다.

 이때 sharingMode는 VK_SHARING_MODE_CONCURREN 여야 한다. 그렇지 않은 경우 이 값은 무시 된다.

 

버퍼 리소스를 생성한다.

// Provided by VK_VERSION_1_0
VkResult vkCreateBuffer(
    VkDevice                                    device,
    const VkBufferCreateInfo*                   pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkBuffer*                                   pBuffer);

- device : 논리 장치

- pCreateInfo : VkBufferCreateInfo

- pAllocator : 호스트 메모리 할당

- buffer : 생성된 리소스가 VkBuffer 포인터로 반환

 

버퍼 리소스를 삭제한다.

// Provided by VK_VERSION_1_0
void vkDestroyBuffer(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    const VkAllocationCallbacks*                pAllocator);

- device : 논리장치

- buffer : 삭제해야할 VkBuffer 개체

- pAllocator : 호스트 메모리 삭제 프로세스 제어

 

2. 버퍼 메모리 유형 & 요구사항 쿼리

버퍼가 생성되었지만 아직 버퍼에 메모리가 할당 되지 않았습니다.

버퍼에 메모리를 할당 하기위한 과정은 다음과 같다.

 

2.1 호환되는 메모리 유형에 대한 정보를 쿼리해야 합니다 

VkPhysicalDeviceMemoryProperties 구조체 전달 받음.

// Provided by VK_VERSION_1_0
typedef struct VkPhysicalDeviceMemoryProperties {
    uint32_t        memoryTypeCount;
    VkMemoryType    memoryTypes[VK_MAX_MEMORY_TYPES];
    uint32_t        memoryHeapCount;
    VkMemoryHeap    memoryHeaps[VK_MAX_MEMORY_HEAPS];
} VkPhysicalDeviceMemoryProperties;

// Provided by VK_VERSION_1_0
void vkGetPhysicalDeviceMemoryProperties(
    VkPhysicalDevice                            physicalDevice,
    VkPhysicalDeviceMemoryProperties*           pMemoryProperties);

- physicalDevice : 물리장치

- pMemoryProperties : 메모리 유형

 

 

2.2 사용 가능한 메모리 요구사항의 대해 쿼리해야 합니다 

VkMemoryRequirements 전달 받음.

// Provided by VK_VERSION_1_0
void vkGetBufferMemoryRequirements(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkMemoryRequirements*                       pMemoryRequirements);

- device : 논리장치

- buffer : VkBuffer

- pMemoryRequirements : 메모리 요구사항

// Provided by VK_VERSION_1_0
typedef struct VkMemoryRequirements {
    VkDeviceSize    size;
    VkDeviceSize    alignment;
    uint32_t        memoryTypeBits;
} VkMemoryRequirements;

- size : 필요한 메모리의 양

- alignment : 할당된 메모리 영역에서 버퍼가 시작되는 오프셋 바이트

- memoryTypeBits : 버퍼에 적합한 메모리 유형의 비트 필드.

 

2.3 생성 원하는 유형의 메모리의 인덱스를 얻는다.

// Provided by VK_VERSION_1_0
typedef enum VkMemoryPropertyFlagBits {
    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT = 0x00000001,
    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT = 0x00000002,
    VK_MEMORY_PROPERTY_HOST_COHERENT_BIT = 0x00000004,
    VK_MEMORY_PROPERTY_HOST_CACHED_BIT = 0x00000008,
    VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT = 0x00000010,
  // Provided by VK_VERSION_1_1
    VK_MEMORY_PROPERTY_PROTECTED_BIT = 0x00000020,
  // Provided by VK_AMD_device_coherent_memory
    VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD = 0x00000040,
  // Provided by VK_AMD_device_coherent_memory
    VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD = 0x00000080,
  // Provided by VK_NV_external_memory_rdma
    VK_MEMORY_PROPERTY_RDMA_CAPABLE_BIT_NV = 0x00000100,
} VkMemoryPropertyFlagBits;

Host ( CPU ), Device (GPU)

- VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 

: GPU 전용 메모리에 할당.

: 한 번 올리고 더 이상 CPU에 접근을 하지 않는 이미지 버퍼 생성할때 사용됨.

: GPU R/W 속도 빠름, CPU 접근 불가

 

- VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT

: 공유 메모리에 할당

: CPU의 R/W 자주 발생하는 유니폼 버퍼에서 사용.

: 쓰기 연산이 많기 때문에 즉시 flush가 되는 옵션인 HOST_COHERENT와 같이 사용됨.

: CPU R/W 속도 느림 / GPU R/W 속도 역시 느림

 

- VK_MEMORY_PROPERTY_HOST_CACHED_BIT

: 공유 메모리에 할당

: 호스트에 캐시 메모리가 있음.

: 캐쉬 메모리에 write 명령어가 끝나면 즉시 host 메모리에 업데이트하여 GPU와 CPU간 동기화

: CPU 속도 빠름 / GPU 속도 느림.

Memory Type 0 :  VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 

Memory Type 1 :  VK_MEMORY_PROEPRTY_HOST_VISIBLE_BIT 

Memory Type 2 :  VK_MEMORY_PROEPRTY_HOST_VISIBLE_BIT 

Memory Type 3 :  VK_MEMORY_PROPERTY_HOST_CACHED_BIT

 

 

 

원하는 유형의 메모리 인덱스를 얻는 함수

2가지 조건을 만족하는 메모리 인덱스를 가져온다.

1) memoryRequirements.memoryTypeBits 비트마스킹을 검사해서 해당 인덱스의 값이 1이면 지원.

2) physicalDeviceMemoryProperties.memoryTypes[i].propertyFlags 비트가 내가 원하는 유형과 일치

 

코드에선 propertyFlags (VkMemoryPropertyFlagBits)는 두가지로 설정함.

- VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT

- VK_MEMORY_PROPERTY_HOST_COHERENT_BIT

 

Usage >>

uint32_t vertexMemoryTypeIndex;
VK_CHECK_ERROR(vkGetMemoryTypeIndex(mPhysicalDeviceMemoryProperties,
                                    vertexMemoryRequirements,
                                    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                                            VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                                    &vertexMemoryTypeIndex));


inline VkResult
vkGetMemoryTypeIndex(const VkPhysicalDeviceMemoryProperties &physicalDeviceMemoryProperties,
                     const VkMemoryRequirements &memoryRequirements,
                     VkMemoryPropertyFlags memoryPropertyFlags,
                     uint32_t *memoryTypeIndex) {
    for (auto i = 0; i != physicalDeviceMemoryProperties.memoryTypeCount; ++i) {
        if (memoryRequirements.memoryTypeBits & (1 << i)) {
            if ((physicalDeviceMemoryProperties.memoryTypes[i].propertyFlags &
                 memoryPropertyFlags) == memoryPropertyFlags) {
                *memoryTypeIndex = i;
                return VK_SUCCESS;
            }
        }
    }

    *memoryTypeIndex = UINT32_MAX;
    return VK_ERROR_UNKNOWN;
}

 

 

3. 메모리 할당

위에서 구한 인덱스와 사이즈를 알면 메모리를 할당할 수 있다.

버퍼 리소스를 위한 물리적 저장 공간 할당을 하자.

메모리를 할당 메타 데이터는 VkMemoryAllocateInfo 이다.

// Provided by VK_VERSION_1_0
typedef struct VkMemoryAllocateInfo {
    VkStructureType    sType;
    const void*        pNext;
    VkDeviceSize       allocationSize;
    uint32_t           memoryTypeIndex;
} VkMemoryAllocateInfo;

- sType : VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO

- pNext : 확장 구조체 포인터

- allocationSize : 할당할 메모리 사이즈 (bytes)

- memoryTypeIndex : 위에서 구한  메모리 유형의 인덱스 ( 원하는 메모리 타입의 해당하는 배열의 인덱스 )

 

// Provided by VK_VERSION_1_0
VkResult vkAllocateMemory(
    VkDevice                                    device,
    const VkMemoryAllocateInfo*                 pAllocateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDeviceMemory*                             pMemory);

- device : 논리장치

- pAllocateInfo : 메모리 할당 메타 데이터

- pAllocator : 호스트 메모리 할당 컨트롤러

- pMemory : 생성된 메모리 핸들

 

Usage >>

// Vertex VkDeviceMemory 할당
VkMemoryAllocateInfo vertexMemoryAllocateInfo{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .allocationSize = vertexMemoryRequirements.size,
        .memoryTypeIndex = vertexMemoryTypeIndex
};

VK_CHECK_ERROR(vkAllocateMemory(mDevice,
                                &vertexMemoryAllocateInfo, nullptr, &mVertexMemory));

 

4. 버퍼 메모리 바인딩 및 데이터 복사

 

생성한 버퍼 리소스에 할당한 물리 메모리를 바인딩을 해야한다.

버퍼에 할당한 물리 메모리 바인딩을 하는 함수는 vkBindBufferMemory 이다.

VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkDeviceMemory                              memory,
    VkDeviceSize                                memoryOffset);

- device : 논리장치

- buffer : 메모리와 바인딩 할 버퍼

- memory : 버퍼와 바인딩 할 메모리

- memoryOffset : 바인딩 할 시작 오프셋

 

바인딩이 완료가 되면,

할당한 메모리에 데이터를 복사하기 위해 가상 주소로 바꾸어 주어야 한다. (CPU - > GPU)

이때 사용하는 함수가 vkMapMemory() 이다.

VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory,
    VkDeviceSize                                offset,
    VkDeviceSize                                size,
    VkMemoryMapFlags                            flags,
    void**                                      ppData);

- device : 논리장치

- memory : 가상주소로 바꿀 메모리

- offset : 시작 오프셋

- size : 복사할 데이터 사이즈

- ppData : 전달받을 가상 주소

VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory);

 

가상 주소 변환이 완료되면 GPU로 전달할 vertex buffer array를 해당 주소로 복사한다.

 

Usage >>

// Vertex VkBuffer와 Vertex VkDeviceMemory 바인드
VK_CHECK_ERROR(vkBindBufferMemory(mDevice, mVertexBuffer, mVertexMemory, 0));

// Vertex 데이터 복사
void* vertexData;
VK_CHECK_ERROR(vkMapMemory(mDevice, mVertexMemory, 0, mVertexDataSize, 0, &vertexData));
memcpy(vertexData, mVertices.data(), mVertexDataSize);
vkUnmapMemory(mDevice, mVertexMemory);

 

5. 커맨드 버퍼에 바인딩 명령을 추가한다. 

 

메모리 할당, 버퍼 생성을 완료 했고, 메모리와 버퍼를 바인딩을 완료 하였다.

이제 커맨드 버퍼에도 바인딩 명령을 큐에 추가하여 버텍스 버퍼를 바인딩하자.

 

// Provided by VK_VERSION_1_0
void vkCmdBindVertexBuffers(
    VkCommandBuffer                             commandBuffer,
    uint32_t                                    firstBinding,
    uint32_t                                    bindingCount,
    const VkBuffer*                             pBuffers,
    const VkDeviceSize*                         pOffsets);

- commandBuffer : 커맨드 버퍼

- firstBinding : 바인딩 할 첫번째 버퍼 인덱스

- bindingCount : 바인딩 할 버퍼 개수

- pBuffers : 버퍼 핸들의 어레이 포인터

- pOffsets : 버퍼 오프셋의 어레이 포인터

 

Usage >>

vkCmdBeginRenderPass(mCommandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);

//  Vertex VkBuffer 바인드
VkDeviceSize vertexBufferOffset{0};
vkCmdBindVertexBuffers(mCommandBuffer, 0, 1, &mVertexBuffer, &vertexBufferOffset);

vkCmdDraw(mCommandBuffer, 3, 1, 0, 0);
vkCmdEndRenderPass(mCommandBuffer);

 

 

Reference

https://slideplayer.com/slide/15437887/

'Vulkan' 카테고리의 다른 글

10. Descriptor set and Descriptor set layout  (0) 2024.07.12
9. Pipeline and Pipeline State  (0) 2024.07.08
7. SPIR-V  (0) 2024.07.04
6. RenderPass  (0) 2024.07.01
5. Presentation And Synchronize ( Fence, Semaphore, Event )  (0) 2024.06.27