[vk] Uniform buffers - Descriptor pool and sets

2021. 8. 16. 20:05그래픽스/vk

1. Introduction

- 이전 장의 descriptor layout은 바인딩 할 수 있는 descriptors의 타입을 설명한다. (UB)

- 이번 챕터에서는 unifom buffer descriptor로 바인딩하기위해 

- 각각의 VkBuffer 리소스에 대한 descriptor set 을 생성할것이다.

  (In this chapter we're going to create a descriptor set for each VkBuffer resource to bind it to the uniform buffer descriptor.)

 

 

 

 

2. Descriptor pool

- Descriptor sets 은 직접 만들 수 없다.

- 그것들은 command buffers과 같이 pool에서부터 할당되어진다.

- descriptor sets 을 할당해주는것은 descriptor pool이다.

- 주의할 점은 Descriptor pools은 외부적으로 동기화 되어지므로

- 멀티 쓰레드에서 동시에 같은 pool을 사용하여 할당/해제 하면 안된다.

- 새로운 함수 createDescriptorPool 를 만들어보자. 

void initVulkan() {
    ...
    createUniformBuffers();
    createDescriptorPool();
    ...
}

...

void createDescriptorPool() {

}

 

 

 

 

2.1 VkDescriptorPoolSize

- VkDescriptorPoolSize 구조체를 사용하여 descriptor 타입과, 

- 할당할 해당 타입의 descriptors의 수를 기술할 필요가있다.

  (VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT 타입인 경우, 바이트의 수)

VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());

 

- 우리는 매 프레임마다 이러한 descriptors 중 하나를 할당할것이다.

 

 

 

 

2.2 VkDescriptorPoolCreateInfo

- pool size 구조체는 VkDescriptorPoolCreateInfo 에 의해 참조될것이다.

VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;

- 사용가능한 individual descriptors의 최대 개수외에도, (descriptor count????)

- 우리는 할당할 수있는 descriptor sets의 최대 개수를 명시할 필요가있다.

  (Aside from the maximum number of individual descriptors that are available,

   we also need to specify the maximum number of descriptor sets that may be allocated:)

poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());

- 이 구조체는 또한 선택적인 flag가 있다. 

- command pools과 유사하게 개별 descriptor sets을 해제할 수 있는지의 여부를 결정하는

- 선택적인 falg가 있다 : VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT 

  (The structure has an optional flag similar to command pools that determines

    if individual descriptor sets can be freed or   not: VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT)

- 우리는 descriptor set을 생성하고 난뒤에 터치하지 않으니 flag를 설정할 필요가 없다.

- 그러므로 flags 를 기본값인 0 으로 설정한다.

VkDescriptorPool descriptorPool;

...

if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
    throw std::runtime_error("failed to create descriptor pool!");
}

- 핸들을 저장할 새로운 클래스 멤버를 추가하고  vkCreateDescriptorPool 함수를 통해 생성해야한다.

- descriptor pool은 swap chain이 재생성될때 제거해줘야한다. (이미지에 의존적이므로)

 

 

 

 

2.3 recreateSwapchain

void cleanupSwapChain() {
    ...

    for (size_t i = 0; i < swapChainImages.size(); i++) {
        vkDestroyBuffer(device, uniformBuffers[i], nullptr);
        vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
    }

    vkDestroyDescriptorPool(device, descriptorPool, nullptr);
}

- 제거와 생성을 swapchain과 함께해야한다.

void recreateSwapChain() {
    ...

    createUniformBuffers();
    createDescriptorPool();
    createCommandBuffers();
}

 

 

 

 

3. Descriptor set

- 우리는 이제 descriptor sets 자체를 할당할 수 있다.

- createDescriptorSets 함수를 추가하여 만들어보자

void initVulkan() {
    ...
    createDescriptorPool();
    createDescriptorSets();
    ...
}

void recreateSwapChain() {
    ...
    createDescriptorPool();
    createDescriptorSets();
    ...
}

...

void createDescriptorSets() {

}

 

 

 

 

3.1 Allcocate

3.1.1 DescriptorSetAllocateInfo

- A descriptor set allocation은 VkDescriptorSetAllocateInfo 구조체로 기술되어진다.

- 할당해주는 descriptor pool , 할당할 descriptor sets의 수, 기반이되는 descriptor layout 을 기술할 필요가 있다.

std::vector<VkDescriptorSetLayout> layouts(swapChainImages.size(), descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size());
allocInfo.pSetLayouts = layouts.data();

- 우리의 경우는 각각의 swapchain image에 하나의 descriptor를 생성하는것이며, layout이 모두 같다.

- 불행하게도, 우리는 layout을 모두 복사할 필요가 있다. (다음함수가 sets와 일치하는 배열을 기대하기 때문)

3.1.2 vkAllocateDescriptorSets

- descriptor set 핸들들을 저장하는 새로운 클래스 맴버를 생성하고,

- vkAllocateDescriptorSets 함수를 사용하여 할당하자

VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;

...

descriptorSets.resize(swapChainImages.size());
if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate descriptor sets!");
}

- descriptor pool이 제거되면 이것들은 자동적으로 해제되기 때문에 clean up을 명시할 필요는없다.

- vkAllocateDescriptorSets 함수를 호출하여, 하나의 uniform buffer descriptor가 있는 descriptor sets을 할당할 수 있다.

 

 

 

3.2 Update

3.2.1 VkDescriptorBufferInfo

- descriptor set들은 이제 할당되어졌다. 그러나 descriptors은 설정해야할게 더 남아있다..

- 우리는 이제 모든 descriptor를 채우는 루프문을 작성할것이다. 

for (size_t i = 0; i < swapChainImages.size(); i++) {

}

- uniform buffer descriptor처럼 buffer들을 참조하는 Descriptors 는 VkDescriptorBufferInfo구조체로 설정되어진다.

  (Descriptors that refer to buffers, like our uniform buffer descriptor, are configured with a VkDescriptorBufferInfo struct.)

- 이러한 구조체는 버퍼와 버퍼 내에서 decriptor에 대한 데이터를 포함하는 영역을 지정한다.

for (size_t i = 0; i < swapChainImages.size(); i++) {
    VkDescriptorBufferInfo bufferInfo{};
    bufferInfo.buffer = uniformBuffers[i];
    bufferInfo.offset = 0;
    bufferInfo.range = sizeof(UniformBufferObject);
}

- 이 경우와 같이 만일 전체 버퍼를 덮어쓰는경우, 그럼 VK_WHOLE_SIZE 값을 range에 사용할 수 있다.

3.2.2 VkWriteDescriptorSet

- descriptors 의 설정은 vkUpdateDescriptorSets 함수를 사용함으로써 업데이트되어진다.

- 그리고 이 함수는 VkWriteDescriptorSet구조체의 배열을 파라미터로 받는다.

VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;

- dstSet : 업데이트할 descriptor set 을 지정

- dstBinding : set 안에서 바인딩할 descriptor 지정,

- - - - 우리는 uniform buffer binding index를 0으로 지정하였음.

- dstArrayElement : descriptors 배열의 시작 요소

- - - - descriptors 는 배열이될 수 있음을 기억해야한다.

- - - - 그래서 우리는 업데이트하기를 원하는 배열의 첫번째 인덱스를 명시해야한다.

- - - - 우리는 단일 descriptor이므로 인덱스 0 을 넣은것

- - - - dstArrayElement 에서 지정한 인덱스부터 시작하는 배열에서

- - - - 다수의 descriptors를 한번에 업데이트 할 수 있음.

descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;

- descriptorType : descriptor의 타입을 다시 명시할 필요가 있음

- descriptorCount : 얼마나 많은 배열요소를 업데이트 할것인지를 지정함.

- - - - 만약 dstSet 과 dstBinding에 의해 식별된 descriptor 바인딩의 타입이

- - - - VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT 일경우, 업데이트할 바이트 수를 기입해야함

- - - - 다른 타입이면 아래중 하나의 값을 기입함.

- - - - 1) pImageInfo 의 원소 개수

- - - - 2) pBufferInfo 의 원소 개수

- - - - 3) pTexelBufferView 의 원소 개수

- - - - 4) pnext의 VkWriteDescriptorSetInlineUniformBlockEXT , VkWriteDescriptorSetAccelerationStructureKHR 

- - - -     의 dataSize 와 accelerationStructureCount 값과 매칭시켜야함

descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr; // Optional
descriptorWrite.pTexelBufferView = nullptr; // Optional

- 마지막 3 필드는 실제 descriptors을 구성하는 descriptorCount 의 크기의 구조체배열 을 참조한다. 

  (The last field references an array with descriptorCount structs that actually configure the descriptors.)

- 이것은 3개중 실제로 사용하는 descriptor type에 의존한다.

   (It depends on the type of descriptor which one of the three you actually need to use.)

- pBufferInfo 필드는 buffer data를 참조하는 descriptors에서 사용된다.

- pImageInfo 필드는 image data를 참조하는 descriptors에서 사용된다.

- pTexelBufferView 필드는 buffer views를 참조하는 descriptors에서 사용된다.

- 우리의 descriptor는 buffer 기반이므로 pBufferInfo 를 사용한다.

 

3.2.3 vkUpdateDescriptorSets

vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);

- vkUpdateDescriptorSets을 사용하여 업데이트할 수 있다.

- 파라미터로 VkWriteDescriptorSet 배열과 VkCopyDescriptorSet 배열을 받는다.

- 후자는 descriptors을 서로 복사하는데 사용되어짐.

 

 

 

 

4. Using descriptor sets

우리는 이제 createCommandBuffers 함수를 업데이트하여

- vkCmdBindDescriptorSets 함수를 사용하여 셰이더의 descriptors에

- 각 스왑체인 이미지에 대한 적절한 descriptor set을 바인딩할 필요가 있다.

- 이것은  vkCmdDrawIndexed 함수호출전에 해야하는 일이다.

// Provided by VK_VERSION_1_0
void vkCmdBindDescriptorSets(
    VkCommandBuffer                             commandBuffer,
    VkPipelineBindPoint                         pipelineBindPoint,
    VkPipelineLayout                            layout,
    uint32_t                                    firstSet,
    uint32_t                                    descriptorSetCount,
    const VkDescriptorSet*                      pDescriptorSets,
    uint32_t                                    dynamicOffsetCount,
    const uint32_t*                             pDynamicOffsets);
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[i], 0, nullptr);
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);

- 파라미터 2 : compute pipeline이나 graphics에 descriptor sets를 바인딩할지에 대한 여부를 명시할 필요가있다.

- - - - vertex 와 index 버퍼들과는 다르게, descriptor sets은 graphics pipelines에 고유하지 않기 때문.

- 파라미터 3 : descriptors의 기반이되는 layout을 지정

- 파라미터 4 : 첫번째 descriptor sets의 인덱스를 명시

- 파라미터 5 : 바인딩할 sets의 개수

- 파라미터 6 : 바인딩할 sets의 배열

- 파라미터 7~8 : dynamic descriptors에 사용되는 오프셋 배열을 지정하며, 향후 챕터에서 다룸.

- 파라미터 7 : dynamic offsets의 개수

- 파라미터 8 : dynamic offsets의 배열

 

- 만일 지금 프로그램을 실행하면 아무것도 보이지않을것이다.

- 문제는 projection 행렬에 적용한 Y-flip 때문이다.

- 정점은 이제 시계방향대신에 시계반대방향으로 그려진다(counter-clockwise order)

- 이로인해 backface culling이 시작되고, 도형은 그려지지않게된다.

- createGraphicsPipeline 함수로가서 VkPipelineRasterizationStateCreateInfo::frontFace 를 수정해줘야한다. 

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;

- 이제 프로그램을 실행해보면 다음 화면을 볼 수 있을 것이다.

- 직사각형이 사각형으로 변경된것을 볼 수 있다.

- 그 이유는 projection 행렬로 인해 종횡비가 올바르게 됬기 때문이다.

- updateUniformBuffer 는 screen resizing 에대해 적절히 처리하므로 (swap chain extent사용)

- recreateSwapChain 에서 재생성할 필요가 없다.

 

 

5. Alignment requirements

- 지금까지 얼버무린 한가지는

  (One thing we've glossed over)

- c++ 구조체의 데이터와 셰이더의 uniform 정의를 정확히 일치시키는 방법이다.

- 이것은 간단히 같은 타입을 사용하는것으로 충분해보인다. 

struct UniformBufferObject {
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

- 하지만 전부 이처럼 잘 작동되는것이아니다.

- 예를들어 다음과같은 상황이 있다.

struct UniformBufferObject {
    glm::vec2 foo;
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

layout(binding = 0) uniform UniformBufferObject {
    vec2 foo;
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

- 다시 컴파일하고, 프로그램을 실행해보면, 칼라풀한 사각형이 사라진다.

- 그 이유는 정렬 요구사항(alignment requirements) 을 고려하지 않았기 때문이다.

 

- Vulkan은 구조체 데이터를 특정 방식으로 메모리에 정렬되기를 기대한다. (the specification)

- - - - Scalars have to be aligned by N (= 4 bytes given 32 bit floats).

- - - - vec2 must be aligned by 2N (= 8 bytes)

- - - - vec3 or vec4 must be aligned by 4N (= 16 bytes)

- - - - nested structure must be aligned by the base alignment of its members rounded up to a multiple of 16.

- - - - mat4 matrix must have the same alignment as a vec4

 

 

- 우리의 origin shader는 3개의 mat4 필드로, 이미 정렬요구사항을 만족하고 있었다.

- 각각의 mat4 는 4x4x4 = 64 바이트 사이즈로 model 은 offset 0, view 는 offset 64, proj 는  offset 128 이였다.

- 이들은 모두 16의 배수였으므로, 잘 작동하였다.

 

- 하지만 8바이트의 vec2 를 추가하면 모든 offset이 맞지않게 되어버린다. (구조체의 경우 16배수여야함)

- 이러한 문제를 해결하기위해 c++11부터 도입된 alignas 지정자를 사용할것이다.

struct UniformBufferObject {
    glm::vec2 foo;
    alignas(16) glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

- 이제 컴파일해보면, shader가 올바르게 행렬값을 받아 올바르게 작동하는것을 볼 수 있다.

 

- 다행히 대부분의 경우 이러한 정렬 요구사항에 대해 생각할 필요가 없는 방법이있다.

- GLM을 포함하기전에 GLM_FORCE_DEFAULT_ALIGNED_GENTYPES 을 정의하는것이다.

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
#include <glm/glm.hpp>

- 이렇게하면 GLM 이 이미 지정된 정렬 요구사항이 있는 vec2 및 mat4 버전을 사용하게된다.

- 이것을 정의하면 이제 alignas 지정자를 프로그램에서 제거해도 된다.

 

- 불행하게도 이러한 방법은 중첩된 구조체를 사용하기 시작하면 고장날 수 있다.

- c++코드를 아래와같이 정의하고

struct Foo {
    glm::vec2 v;
};

struct UniformBufferObject {
    Foo f1;
    Foo f2;
};

 - shader 를 아래같이 정의해보자

struct Foo {
    vec2 v;
};

layout(binding = 0) uniform UniformBufferObject {
    Foo f1;
    Foo f2;
} ubo;

- 이경우 f2 는 offset 8을 가지게되어 16 의 배수가되어야하는 조건을 만족하지 못하게된다.

- 이러한 경우에는 다시 alignas 지정자를 통해 명시해줘야한다.

struct UniformBufferObject {
    Foo f1;
    alignas(16) Foo f2;
};

- 이러한 문제들(gotchas)은 항상 정렬을 명시해야하는 좋은 이유가된다.

- 항상 정렬을 명시하면, 이상한 증상에대해 당황하지 않을것이다.

   (These gotchas are a good reason to always be explicit about alignment.

    That way you won't be caught offguard by the strange symptoms of alignment errors.)

struct UniformBufferObject {
    alignas(16) glm::mat4 model;
    alignas(16) glm::mat4 view;
    alignas(16) glm::mat4 proj;
};

- 다시 재컴파일하는것을 잊지말자.

 

 

 

 

6. Multiple descriptor sets

- 몇몇 구조체들과 함수호출에서 봤듯이 다수의 descriptor sets을 동시에 바인딩하는것이 가능하다.

- 이 경우 pipeline layout을 생성할때, 각각의 descriptor set에 대한 descriptor layout을 지정해야한다.

- Shader들은 다음과 같이 특정 descriptor sets를 참조할 수 있다.

layout(set = 0, binding = 0) uniform UniformBufferObject { ... }

- 이러한 기능을 사용하여 객체별로 달라지는 descriptor들 과

- 공유되는 descriptors을 별도의 descriptor sets에 넣을 수 있다.

- 이러한 경우 draw calls에서 대부분의 descriptors을 다시 바인딩하는것을 피할 수 있다.

 

- 파이프라인과의 호환성에 관한 링크

https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#descriptorsets-compatibility

- 전체 코드

C++ code / Vertex shader / Fragment shader

https://vulkan-tutorial.com/en/Uniform_buffers/Descriptor_pool_and_sets