본문 바로가기
Vulkan

10. Descriptor set and Descriptor set layout

by SimonLee 2024. 7. 12.

이전 챕터에서는 버텍스 버퍼를 생성하고 메모리를 생성하고 둘을 바인딩했었다.

이때 메모리 생성시 " VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT " 을 사용해서

CPU <-> GPU 공유가 가능했다.  ( R/W 빈번하게 일어남 )

 

https://graphicsimon.tistory.com/63

 

8. Vulkan Vertex Buffer & Memory

이전 챕터 [ SPIR-V ] 에서 GLSL 셰이더를 SPIR-V로 변환하고 셰이더 모듈을 만들었다. 이번 챕터 "Vulkan Buffer & Memory " 에서는셰이더 모듈에 정점 데이터를 넘겨주는 부분을 구현할 예정이다. OpenGL에

graphicsimon.tistory.com

 

유니폼 버퍼를 생성할때도 R/W 빈번하게 발생하기 때문에 

" VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT " 사용해서 유니폼 버퍼를 생성한다.

유니폼 버퍼생성이 되면 GPU로 Resource를 공유하기 위해 디스크립터를 사용한다.

 

디스크립터는 셰이더와 리소스를 연결하는 역할을 한다.

디스크립터는 내부를 볼 수 없는 불투명한 개체로 셰이더와 통신하기 위한 프로토콜을 정의하며 

위치 바인딩을 사용하여 리소스 메모리 셰이더와 연결하는 메커니즘을 제공.

디스크립터를 생성하기 위한 방법은 조금 복잡한데 아래 그림을 보자.

 

 

위 그림은 디스크립터 세트 레이아웃 구조체가 실제 리소스와 어떻게 연결되어있는지 보여준다.

 

1) 디스크립터 세트 레이아웃 객체를 생성해야 한다.

- 위 예제에서는 3개의 디스크립터 세트 레이아웃을 생성했다.

 

2) 리소스와 바인딩을 위해 디스크립터 세트 레이아웃 내부에

디스크립터 세트 레이아웃 바인딩을 리소스 개수만큼 생성해준다.

- 위 예제에서는 1번 디스크립터 세트 레이아웃 내부에 3개의 디스크립터 세트 레이아웃 바인딩이 있다.

 

3) 디스크립터 세트를 할당 한다.

- 디스크립터 세트를 할당하기 위해서는 디스크립터 풀이 필요하다.

- 디스크립터 풀을 생성할때는 필요한 리소스 만큼 생성을 해준다. 

 

4) 디스크립터 풀이 생성되면, 디스크립터 풀에서 디스크립터 세트를 할당한다.

- 디스크립터 세트 레이아웃 개수와 디스크립터 세트의 개수는 동일하다

- 디스크립터 세트 할당이 완료 되면,

 4 - 1) 디스크립터 세트 레이아웃과 디스크립터 세트가 맵핑이 완료된다.

 4 - 2) 디스크립터 세트 내부에 디스크립터가 바인딩 개수만큼 생성된다.

 

5) 다음은 유니폼을 복사할 버퍼를 생성하고 물리 메모리를 할당 후 바인딩 한다.

 

- 메모리 복사 용이성을 위하여 버퍼의 가상주소를 내부에 저장해 놓는다.

 

6) 생성된 버퍼정보와 디스크립터 세트의 정보를 사용하여  생성된 버퍼의 내용을 디스크립터 세트에 Write 한다.

 

7) Wrtie 된 내용들은 디스크립터를 통해 셰이더 Input으로 들어가게 되며 텍스처, 유니폼 버퍼 등 리소스와 바인딩 된다.

- 셰이더 INPUT 쪽에서는 layout의 binding과 set을 입력받아야 하는데

- set은 디스크립터 세트 인덱스이며,

- binding은 7) 과정에서 dstBinding 변수의 값으로 설정 되어야 한다.

 

-------------------------------------------------------------------------------------------------------------------

아래 구조로 디스크립터 세트 레이아웃을 생성해보자.

 

파이프라인 레이아웃 내부에

디스크립터 세트 레이아웃 - 0

 -- 디스크립터 세트 레이아웃 바인딩 0

디스크립터 세트 레이아웃 - 1

 -- 디스크립터 세트 레이아웃 바인딩 0

디스크립터 세트 레이아웃 - 2

 -- 디스크립터 세트 레이아웃 바인딩 0

 -- 디스크립터 세트 레이아웃 바인딩 5 --> 셰이더 유니폼 변수와 연결 예정

 

SET = 2, 

--------------------------------------------------------------------------------------------------------------------

1) 디스크립터 세트 레이아웃 생성

디스크립터 세트는 주어진 리소스 (유니폼 버퍼, 샘플 이미지, 저장된 이미지) 등을 셰이더에 연결하며

디스크립터 세트 레이아웃을 사용하여,

셰이더에 정의된 레이아웃 바인딩으로 들어오는 리소스 데이터를 읽고 해석한다.

 

디스크립터 세트 레이아웃은 0개 이상의 디스크립터 세트 레이아웃 바인딩의 모음으로,

셰이더가 지정된 위치의 리소스를 읽을 수 있게 하는 인터페이스를 제공한다.

 

디스크립터 셋 레이아웃을 생성하는 함수

// Provided by VK_VERSION_1_0
VkResult vkCreateDescriptorSetLayout(
    VkDevice                                    device,
    const VkDescriptorSetLayoutCreateInfo*      pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDescriptorSetLayout*                      pSetLayout);

- device : 논리장치

- pCreateInfo : 디스크립터 세트 레이아웃의 메타 데이터 저장 ( VkDescriptorSetLayoutCreateInfo )

- pAllocator : 호스트 메모리 할당

- pSetLayout : 생성된 디스크립터 세트 레이아웃 개체를 반환 ( VkDescriptorSetLayout )

 

디스크립터 셋 레이아웃 메타 데이터

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorSetLayoutCreateInfo {
    VkStructureType                        sType;
    const void*                            pNext;
    VkDescriptorSetLayoutCreateFlags       flags;
    uint32_t                               bindingCount;
    const VkDescriptorSetLayoutBinding*    pBindings;
} VkDescriptorSetLayoutCreateInfo;

- sType : VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO

- pNext : 확장판 지정 구조체

- flags : VkDescriptorSetLayoutCreateFlags

- bindingCount : pBindings 개수

- pBindings : VkDescriptorSetLayoutBinding 구조체 배열 포인터

 

디스크립터 셋 레이아웃 내부에는 여러개의 디스크립터 세트 레이아웃 바인딩을 가질 수 있다.

각 디스크립터 세트 레이아웃 바인딩은 디스크립터에 연결된 셰이더 스테이지를 나타낸다.

디스크립터 세트 레이아웃 바인딩 변수에 매핍되는 것은 리소스 이며,

그림을 보면 BUFFER 0, BUFFER 1 ... 으로 표현되어 있다.

 

디스크립터 세트 레이아웃 바인딩

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorSetLayoutBinding {
    uint32_t              binding;
    VkDescriptorType      descriptorType;
    uint32_t              descriptorCount;
    VkShaderStageFlags    stageFlags;
    const VkSampler*      pImmutableSamplers;
} VkDescriptorSetLayoutBinding;

- binding : 리소스 유형의 엔트리를 가리키는 바인딩 인덱스다 ( 위 그림에서 BUFFER 0, BUFFER 1 해당하는 숫자 )

이 인덱스는 바인딩 넘버와 같거나 해당 셰이더 스테이지 내에서 사용되는 인덱스와 같아야 한다

- descriptorType : 바인딩에 사용하는 디스크립터의 유형을 나타낸다. ( 연결할 버퍼의 목적 )

- descriptorCount : 셰이더에서 배열로써 존재하는 디스크립터의 개수

- stageFlags : 어떤 셰이더 스테이지가 그래픽스와 컴퓨팅 스테이트 양쪽 모두의 값에 접근할수 있는지를 지정한다.

   이 셰이더 스테이지는 VkShaderStageFlagBits의 비트 필드로 지정.

   이 값이 VK_SHADER_STAGE_ALL 이면 모든 정의된 셰이더 스테이지는 지정된 binding을 통해 리소스에 접근할 수 있다.

- pImmutableSamplers : 샘플러 핸들들의 배열을 가리키는 포인터, 해당 바인딩 으로 표현되고 디스크립트 세트 레이아웃이 사용한다. 

 

stageFlag VkShaderStageFlags 비트를 살펴보면 리소스 접근 허용되는 셰이더를 정의해야 한다.

// Provided by VK_VERSION_1_0
typedef enum VkShaderStageFlagBits {
    VK_SHADER_STAGE_VERTEX_BIT = 0x00000001,
    VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT = 0x00000002,
    VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT = 0x00000004,
    VK_SHADER_STAGE_GEOMETRY_BIT = 0x00000008,
    VK_SHADER_STAGE_FRAGMENT_BIT = 0x00000010,
    VK_SHADER_STAGE_COMPUTE_BIT = 0x00000020,
    VK_SHADER_STAGE_ALL_GRAPHICS = 0x0000001F,
    VK_SHADER_STAGE_ALL = 0x7FFFFFFF,
	...
} VkShaderStageFlagBits;

 

디스크립터 셋 레이아웃을 삭제하는 함수

// Provided by VK_VERSION_1_0
void vkDestroyDescriptorSetLayout(
    VkDevice                                    device,
    VkDescriptorSetLayout                       descriptorSetLayout,
    const VkAllocationCallbacks*                pAllocator);

- device : 논리장치

- descriptorSetLayout : 삭제할 디스크립터 셋 레이아웃

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

usage >>

디스크립터 세트 레이아웃 3개를 생성하고,

0번, 1번 디스크립터 세트 레이아웃은 디스크립터 세트 레이아웃 바인딩 1개 생성

2번 디스크립터 세트 레이아웃은 디스크립터 세트 레이아웃 바인딩 2개 생성

- 2번 디스크립터 세트는 바인딩 값을 5로 설정해서 셰이더와 연결이 잘되는지 확인해보자.

void VkRenderer::CreateDescriptorSetLayout() {
    VkDescriptorSetLayoutBinding descriptorSetLayoutBindingA {
            .binding = 0,
            .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            .descriptorCount = 1,
            .stageFlags = VK_SHADER_STAGE_VERTEX_BIT
    };
    VkDescriptorSetLayoutBinding descriptorSetLayoutBindingB {
            .binding = 0,
            .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            .descriptorCount = 1,
            .stageFlags = VK_SHADER_STAGE_VERTEX_BIT
    };

    std::vector<VkDescriptorSetLayoutBinding> bindingC {
            VkDescriptorSetLayoutBinding {
                .binding = 0,
                .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
                .descriptorCount = 1,
                .stageFlags = VK_SHADER_STAGE_VERTEX_BIT
            },
            
            VkDescriptorSetLayoutBinding {
                .binding = 5,
                .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
                .descriptorCount = 1,
                .stageFlags = VK_SHADER_STAGE_VERTEX_BIT
            }
    };

    VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfoA {
            .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
            .bindingCount = 1,
            .pBindings = &descriptorSetLayoutBindingA,
    };
    VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfoB {
            .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
            .bindingCount = 1,
            .pBindings = &descriptorSetLayoutBindingB,
    };

    VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfoC {
            .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
            .bindingCount = 2,
            .pBindings = bindingC.data()
    };


    VK_CHECK_ERROR(vkCreateDescriptorSetLayout(mDevice,
                                               &descriptorSetLayoutCreateInfoA,
                                               nullptr,
                                               &mDescriptorSetLayout[0]));

    VK_CHECK_ERROR(vkCreateDescriptorSetLayout(mDevice,
                                               &descriptorSetLayoutCreateInfoB,
                                               nullptr,
                                               &mDescriptorSetLayout[1]));

    VK_CHECK_ERROR(vkCreateDescriptorSetLayout(mDevice,
                                               &descriptorSetLayoutCreateInfoC,
                                               nullptr,
                                               &mDescriptorSetLayout[2]));
}

 

2) 디스크립터 풀을 생성해보자.

디스크립터 세트를 생성하기 위해서는 디스크립터 풀이 필요하다.

 

디스크립터 풀을 생성

// Provided by VK_VERSION_1_0
VkResult vkCreateDescriptorPool(
    VkDevice                                    device,
    const VkDescriptorPoolCreateInfo*           pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDescriptorPool*                           pDescriptorPool);

- device : 논리장치

- pCreateInfo : 디스크립터 풀 메타 데이터 ( VkDescriptorPoolCreateInfo )

- pAllocator : 호스트 메모리 할당 제어

- pDescriptorPool : 생성된 디스크립터 풀 개체

 

디스크립터 풀 메타 데이터

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorPoolCreateInfo {
    VkStructureType                sType;
    const void*                    pNext;
    VkDescriptorPoolCreateFlags    flags;
    uint32_t                       maxSets;
    uint32_t                       poolSizeCount;
    const VkDescriptorPoolSize*    pPoolSizes;
} VkDescriptorPoolCreateInfo;

- sType : VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO

- pNext : 확장 구조체 포인터

- flags :  VkDescriptorPoolCreateFlagsBits의 조합

- maxSets : 할당할수 있는 최대 디스크립터 세트의 개수 ( VkDescriptorSet )

- poolSizeCount : 디스크립터 풀의 개수 ( pPoolSizes 개수  )

- pPoolSizes : 디스크립터 풀 사이즈 구조체의 포인터 ( VkDescriptorPoolSize )

 

디스크립터 세트 생성 플래그 비트

풀에서 디스크립터 생성 방식의 대해서 정의한다.

메모리 절약을 위해 풀에서 할당 받은 디스크립터세트를 보통 해제하지 않고 재사용한다.

하지만 해제를 해야만 하는 경우에는 

 " VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT " 선택하면

디스크립터 세트를 개별적으로 해제 할수 있어서 유연하게 관리할 수 있다.

// Provided by VK_VERSION_1_0
typedef enum VkDescriptorPoolCreateFlagBits {
    VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT = 0x00000001,
  // Provided by VK_VERSION_1_2
    VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT = 0x00000002,
  // Provided by VK_EXT_mutable_descriptor_type
    VK_DESCRIPTOR_POOL_CREATE_HOST_ONLY_BIT_EXT = 0x00000004,
  // Provided by VK_NV_descriptor_pool_overallocation
    VK_DESCRIPTOR_POOL_CREATE_ALLOW_OVERALLOCATION_SETS_BIT_NV = 0x00000008,
  // Provided by VK_NV_descriptor_pool_overallocation
    VK_DESCRIPTOR_POOL_CREATE_ALLOW_OVERALLOCATION_POOLS_BIT_NV = 0x00000010,
  // Provided by VK_EXT_descriptor_indexing
    VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT,
  // Provided by VK_VALVE_mutable_descriptor_type
    VK_DESCRIPTOR_POOL_CREATE_HOST_ONLY_BIT_VALVE = VK_DESCRIPTOR_POOL_CREATE_HOST_ONLY_BIT_EXT,
} VkDescriptorPoolCreateFlagBits;

 

디스크립터 풀 사이즈

디스크립터 풀의 사이즈를 넣어준다.

디스크립터 풀에서 생성할 버퍼의 종류도 type으로 설정해야 한다.

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorPoolSize {
    VkDescriptorType    type;
    uint32_t            descriptorCount;
} VkDescriptorPoolSize;

- type : 디스크립터 타입 ( VkDescriptorType )

- descriptorCount : 디스크립터 개수

 

디스크립터 풀 Type 종류

풀에서 생성할 버퍼의 종류를 선택한다.

// Provided by VK_VERSION_1_0
typedef enum VkDescriptorType {
    VK_DESCRIPTOR_TYPE_SAMPLER = 0,
    VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 1,
    VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE = 2,
    VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3,
    VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER = 4,
    VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER = 5,
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER = 6,
    VK_DESCRIPTOR_TYPE_STORAGE_BUFFER = 7,
    VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC = 8,
    VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC = 9,
    VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT = 10,
    ...
} VkDescriptorType;

 

 

디스크립터 풀 삭제

디스크립터 풀을 삭제한다.

// Provided by VK_VERSION_1_0
void vkDestroyDescriptorPool(
    VkDevice                                    device,
    VkDescriptorPool                            descriptorPool,
    const VkAllocationCallbacks*                pAllocator);

- device : 논리장치

- VkDescriptorPool : 삭제할 디스크립터 풀

- pAllocator : 호스트 메모리 할당 제어

 

Usage >>

유니폼 버퍼를 생성하는 디스크립터 풀을 생성하자.

디스크립터 풀 생성할때 생성해야 할 리소스를 생각하자.

우리는 3개의 디스크립터 세트 레이아웃이 필요하지만

마지막 디스크립터 세트 레이아웃에는

바인딩 할 디스크립터가 2개 필요하기 때문에 4개의 버퍼의 생성이 필요하다.

VkDescriptorPoolSize descriptorPoolSize[4];
descriptorPoolSize[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorPoolSize[0].descriptorCount = 1;
descriptorPoolSize[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorPoolSize[1].descriptorCount = 1;
descriptorPoolSize[2].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorPoolSize[2].descriptorCount = 1;
descriptorPoolSize[3].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorPoolSize[3].descriptorCount = 1;


VkDescriptorPoolCreateInfo descriptorPoolCreateInfo{
        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
        .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
        .maxSets = 3,
        .poolSizeCount = 4,
        .pPoolSizes = &descriptorPoolSize[0]
};

VK_CHECK_ERROR(vkCreateDescriptorPool(mDevice,
                                      &descriptorPoolCreateInfo,
                                      nullptr,
                                      &mDescriptorPool));

 

3) 디스크립터 세트 생성 및 할당 해보자.

 

디스크립터 풀이 생성되면  디스크립트 세트를 할당 할 수 있다.

 

디스크립터 세트 할당 명령어

// Provided by VK_VERSION_1_0
VkResult vkAllocateDescriptorSets(
    VkDevice                                    device,
    const VkDescriptorSetAllocateInfo*          pAllocateInfo,
    VkDescriptorSet*                            pDescriptorSets);

- device : 논리장치

- pAllocateInfo : 디스크립터 세트 메타데이터

- pDescriptorSets : 생성된 디스크립터 세트 포인터

 

 

디스크립터 할당 메타데이터

생성시 디스크립터 풀 디스크립터 세트 레이아웃이 필요하다.

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorSetAllocateInfo {
    VkStructureType                 sType;
    const void*                     pNext;
    VkDescriptorPool                descriptorPool;
    uint32_t                        descriptorSetCount;
    const VkDescriptorSetLayout*    pSetLayouts;
} VkDescriptorSetAllocateInfo;

- sType : VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO

- pNext : 확장 구조체 포인터

- descriptorPool : 디스크립터 풀

- descriptorSetCount : 디스크립터 세트 레이아웃 개수

- pSetLayouts : 디스크립터 세트 레이아웃 포인터

 

 

디스크립터 세트 해제 명령어

// Provided by VK_VERSION_1_0
VkResult vkFreeDescriptorSets(
    VkDevice                                    device,
    VkDescriptorPool                            descriptorPool,
    uint32_t                                    descriptorSetCount,
    const VkDescriptorSet*                      pDescriptorSets);

- device : 논리장치

- descriptorPool : 디스크립터 풀

- discriptorSetCount : 디스크립터 세트 개수

- pDescriptorSets : 디스크립터 세트 포인터

 

메모리 최적화를 위해 디스크립터 세트는 재사용 된다.

디스크립터 세트를 해제하려고 하면 디스크립터 풀에서 해당 디스크립터를 생성 할때

- " VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT "

개체를 프리할수 있다는 플래그 비트를 설정하지 않으면 Validation Error가 발생한다.

 

Usage >>

VkDescriptorSetAllocateInfo descriptorSetAllocateInfo{
        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
        .descriptorPool = mDescriptorPool,
        .descriptorSetCount = 3,
        .pSetLayouts = mDescriptorSetLayout
};

VK_CHECK_ERROR(vkAllocateDescriptorSets(mDevice, &descriptorSetAllocateInfo, &mDescriptorSet[0]));

 

4) Vulkan 파이프라인 레이아웃을 생성.

1) 디스크립터 세트 레이아웃을 완료하면,

2) 파이프라인 레이아웃을 생성하고

3)  VkGraphicsPipelineCreateInfo  구조체의 layout 변수에 생성한 파이프라인 레이아웃을 넣어준다.

 

 

 

Vulkan 파이프라인 레이아웃 생성

// Provided by VK_VERSION_1_0
VkResult vkCreatePipelineLayout(
    VkDevice                                    device,
    const VkPipelineLayoutCreateInfo*           pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkPipelineLayout*                           pPipelineLayout);
)

- device : 논리장치

- pCreateInfo : vulkan 파이프라인 레이아웃 메터데이터 구조체

- pAllocator : 호스트 메모리 관리

- pPipelineLayout : 생성된 파이프라인 레이아웃 포인터

 

 

Vulkan 파이프라인 레이아웃 메타 데이터

pSetLayouts 멤버 변수에 위에서 생성한 디스크립터 세트 레이아웃을 넣어준다.

// Provided by VK_VERSION_1_0
typedef struct VkPipelineLayoutCreateInfo {
    VkStructureType                 sType;
    const void*                     pNext;
    VkPipelineLayoutCreateFlags     flags;
    uint32_t                        setLayoutCount;
    const VkDescriptorSetLayout*    pSetLayouts;
    uint32_t                        pushConstantRangeCount;
    const VkPushConstantRange*      pPushConstantRanges;
} VkPipelineLayoutCreateInfo;

- sType : VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO

- pNext : 확장 기능 구조체

- setLayoutCount : vulkan 디스크립터 세트 레이아웃 개수

- pSetLayouts : vulkan 디스크립터 세트 레이아웃 배열 포인터

- pushConstantRangeCount : 푸쉬 상수 개수

- pPushConstantRanges : 푸쉬 상수 배열 포인터

 

Vulkan 파이프라인 레이아웃 파괴

// Provided by VK_VERSION_1_0
void vkDestroyPipelineLayout(
    VkDevice                                    device,
    VkPipelineLayout                            pipelineLayout,
    const VkAllocationCallbacks*                pAllocator);

- device :논리장치

- pipelineLayout : vulkan 파이프라인 레이아웃

- pAllocator : 호스트 메모리 관리

 

Usage >>

디스크립터 레이아웃 세트는 3개

VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{
        .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
        .setLayoutCount = 3,
        .pSetLayouts = &mDescriptorSetLayout[0]
};

VK_CHECK_ERROR(vkCreatePipelineLayout(mDevice,
                                      &pipelineLayoutCreateInfo,
                                      nullptr,
                                      &mPipelineLayout));
                                      
VkGraphicsPipelineCreateInfo graphicsPipelineCreateInfo{
            .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
            .....
            .layout = mPipelineLayout,            
};

VK_CHECK_ERROR(vkCreateGraphicsPipelines(mDevice,
                                         VK_NULL_HANDLE,
                                         1,
                                         &graphicsPipelineCreateInfo,
                                         nullptr,
                                         &mPipeline));

 

------------------------------------------------------

 

5)  Uniform 버퍼를 생성 후 메모리 할당.

 

디스크립터 세트 레이아웃을 

유니폼 버퍼를 생성하는 방식은 이전에 버텍스 버퍼를 생성한것과 거의 동일하다

 

https://graphicsimon.tistory.com/63

 

8. Vulkan Vertex Buffer & Memory

이전 챕터 [ SPIR-V ] 에서 GLSL 셰이더를 SPIR-V로 변환하고 셰이더 모듈을 만들었다. 이번 챕터 "Vulkan Buffer & Memory " 에서는셰이더 모듈에 정점 데이터를 넘겨주는 부분을 구현할 예정이다. OpenGL에

graphicsimon.tistory.com

 

요약을 하면 다음과 같습니다

1. 유니폼 버퍼를 생성합니다.

  - size 설정, usage는 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT  설정.

2. 메모리 Requirements를 얻어오고, 메모리를 할당할수 있는 메모리 타입 인덱스를 얻어옵니다.

  - CPU, GPU 메모리 공유가능한 host_visible 비트를 설정하고, coherent bit을 설정하여 장치간 동기화를 합니다. 

  - 버텍스 버퍼와 동일

3. 메모리를 할당합니다. ( vkAllocateMemory )

4. 생성한 버퍼와 할당된 메모리를 바인딩 합니다. ( vkBindBufferMemory )

5. 유니폼 버퍼 데이터 초기화

   - 유니폼 버퍼 가상주소를 가져옵니다. ( vkMapMemory )

6. 해당된 메모리에 데이터를 write 합니다.

 

유니폼 블록의 경우 STD 140 메모리 레이아웃을 사용합니다.

배열 원소의 크기는 16bytes 이며,

셰이더에서 해당 원소 크기만큼 메모리를 읽어 들이기 떄문에,

STD 140 메모리 레이아웃을 사용하는 경우엔 메모리에 16bytes씩 순차적으로

메모리에 저장해야 한다.

Usage >>

// Uniform VkBuffer 생성
VkBufferCreateInfo uniformBufferCreateInfo{
        .sType =VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .size = sizeof(Uniform),
        .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
};
vkCreateBuffer(mDevice, &uniformBufferCreateInfo, nullptr, &mUniformBuffer);

// Uniform VkBuffer의 VkMemoryRequirements 얻기
VkMemoryRequirements uniformMemoryRequirements;
vkGetBufferMemoryRequirements(mDevice, mUniformBuffer, &uniformMemoryRequirements);

// Uniform VkDeviceMemory를 할당 할 수 있는 메모리 타입 인덱스 얻기
uint32_t uniformMemoryTypeIndex;
vkGetMemoryTypeIndex(mPhysicalDeviceMemoryProperties,
                        uniformMemoryRequirements,
                        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                        VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                        &uniformMemoryTypeIndex);

// Uniform VkDeviceMemory 할당
VkMemoryAllocateInfo uniformMemoryAllocateInfo{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .allocationSize = uniformMemoryRequirements.size,
        .memoryTypeIndex = uniformMemoryTypeIndex
};

vkAllocateMemory(mDevice, &uniformMemoryAllocateInfo, nullptr, &mUniformMemory);

// Uniform VkBuffer와 Vertex VkDeviceMemory 바인드
vkBindBufferMemory(mDevice, mUniformBuffer, mUniformMemory, 0);

// Uniform 데이터 초기화
vkMapMemory(mDevice, mUniformMemory, 0, VK_WHOLE_SIZE, 0, &mUniformData);
memset(mUniformData, 0, sizeof(Uniform));

 

6)  버퍼를 디스크립터 세트에 연결

생성된 리소스(버퍼)를 디스크립터 세트에 연결하려면 vkUpdateDescriptorSets() API를 사용함으로써 

리소스 정보와 연결이 가능하다.

// Provided by VK_VERSION_1_0
void vkUpdateDescriptorSets(
    VkDevice                                    device,
    uint32_t                                    descriptorWriteCount,
    const VkWriteDescriptorSet*                 pDescriptorWrites,
    uint32_t                                    descriptorCopyCount,
    const VkCopyDescriptorSet*                  pDescriptorCopies);

- device : 논리장치

- descriptorWriteCount : 쓰기 디스크립터 세트 구조체 개수

- pDescriptorWirtes : 쓰기 디스크립터 세트 구조체 포인터

- descriptorCopyCount : 복사 디스크립터 세트 구조체 개수

- pDescriptorCopies : 쓰기 디스크립터 세트 구조체 포인터

 

업데이트는( vkUpdateDescriptorSets ) 쓰기와 복사라는 두 작업이 합쳐졌다.

쓰기 작업이 먼저 실행 후 복사 작업이 수행된다.

 

쓰기 디스크립터 세트

// Provided by VK_VERSION_1_0
typedef struct VkWriteDescriptorSet {
    VkStructureType                  sType;
    const void*                      pNext;
    VkDescriptorSet                  dstSet;
    uint32_t                         dstBinding;
    uint32_t                         dstArrayElement;
    uint32_t                         descriptorCount;
    VkDescriptorType                 descriptorType;
    const VkDescriptorImageInfo*     pImageInfo;
    const VkDescriptorBufferInfo*    pBufferInfo;
    const VkBufferView*              pTexelBufferView;
} VkWriteDescriptorSet;

- sType : VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET

- pNext : 확장판 지정 구조체 포인터

- dstSet : 업데이트할 대상 디스크립터 세트를 가리킨다.

- dstBinding : 세트 내의 디스크립터 바인딩을 지정한다.

  이는 주어진 셰이더 스테이지 셰이더에서 지정된 바인딩 인덱스와 같은 값이어야 한다.

- dstArrayElement : 단일 바인딩 안에서 디스크립터 배열의 시작 요소 인덱스를 가리킨다.

- descriptorCount : pImageInfo, pBufferInfo, pTexelBufferView 유형 중에서 업데이트할 디스크립터의 개수를 나타낸다.

- descriptorType : 참여하는 각 디스크립터의 유형을 가리킨다. ( pImageInfo, pBufferInfo, pTexelBufferView )

- pImageInfo : 이미지 리소스를 나타내는 VkDescriptorImageInfo 구조체 배열, 지정하지 않으려면 VK_NULL_HANDLE

- pBufferInfo : VkDescriptorBufferInfo 구조체 배열, 지정하지 않으려면 VK_NULL_HANDLE

- pTexelBufferView : VkBufferView 핸들값을 포함하는 배열, 지정하지 않으려면 VK_NULL_HANDLE

 

 

디스크립터 버퍼 메터데이터

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorBufferInfo {
    VkBuffer        buffer;
    VkDeviceSize    offset;
    VkDeviceSize    range;
} VkDescriptorBufferInfo;

- buffer : 버퍼

- offset : 오프셋

- range : 디스크립터가 접근할수 있는 범위

 


복사 디스크립터 세트

// Provided by VK_VERSION_1_0
typedef struct VkCopyDescriptorSet {
    VkStructureType    sType;
    const void*        pNext;
    VkDescriptorSet    srcSet;
    uint32_t           srcBinding;
    uint32_t           srcArrayElement;
    VkDescriptorSet    dstSet;
    uint32_t           dstBinding;
    uint32_t           dstArrayElement;
    uint32_t           descriptorCount;
} VkCopyDescriptorSet;

- sType : VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET

- pNext : 확장판 지정 구조체 포인터

- srcSet : 복사할 소스 디스크립터 세트 지정

- srcBinding : 소스 디스크립터 세트 내의 바인딩 인덱스를 지정

- srcArrayElement : 첫 번째 업데이트 바인딩 내의 시작 배열 요소를 가리킨다.

- dstSet : 소스 디스크립터가 복사돼 들어갈 대상 디스크립터 세트를 지정한다.

- dstBinding : 대상 디스크립터 세트의 바인딩 인덱스를 지정한다.

- dstArrayElement : 단일 바인딩에서 디스크립터 배열 시작 인덱스를 가리킨다.

- descriptorCount : 소스에서 대상으로 복사될 디스크립터의 총 개수를 가리킨다.

 

Usage >>

셰이더 유니폼 블락에서 레이아웃에서 binding = 5를 사용함

dstBinding 값과 동일하게 설정.

VkDescriptorBufferInfo descriptorBufferInfo{
        .buffer = mUniformBuffer,
        .range = VK_WHOLE_SIZE
};

VkWriteDescriptorSet writeDescriptorSet{
        .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
        .dstSet = mDescriptorSet[2],
        .dstBinding = 5,
        .descriptorCount = 1,
        .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
        .pBufferInfo = &descriptorBufferInfo
};
vkUpdateDescriptorSets(mDevice, 1, &writeDescriptorSet, 0, nullptr);

 

7)  버텍스 셰이더에서 전달 받을 유니폼 버퍼 선언

셰이더 유니폼 버퍼의 레이아웃은 set = 2, binding = 5 으로 되어있다.

set 값은 디스크립터 세트 배열의 인덱스 ( mDesciptorSet ) 이고

binding 값은 2군데 수정을 동일하게 해준다.

- VkDescriptorSetLayoutBinding 내부 변수 binding

- VkWriteDescriptorSet 내부 변수 dstBinding 

 

#version 310 es                                        
                                                       
layout(location = 0) in vec3 inPosition;               
layout(location = 1) in vec3 inColor;                            
layout(location = 0) out vec3 outColor;                
                                                       
layout(set = 2, binding = 5) uniform Uniform {         
    float position[2];                                 
    float ratio;                                       
};                                                     
                                                       
void main() {                                          
     ......
    outColor = inColor;                                
}

 

8)  렌더링 과정에 다음 과정 추가. 

파이프라인에 디스크립터 세트를 바인딩 후 렌더링을 한다.

 

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

- commandBuffer : 커맨드 버퍼

- pipelineBindPoint : VK_PIPELINE_BIND_POINT_GRAPIHCS

- layout : vulkan 파이프라인 레이아웃

- firstSet : 바인딩을 시작할 세트 number

- descriptorSetCount : 디스크립트 세트 개수

- pDescriptorSets : 디스크립터 세트 배열 포인터

- dynamicOffsetCount : pDynamicOffsets 개수

- pDynamicOffsets : 추후 설명

Usage >>

descriptorSetCount : 디스크립터 세트 레이아웃 개수이므로 3

// VkDescriptorSet 바인드
vkCmdBindDescriptorSets(mCommandBuffer,
                        VK_PIPELINE_BIND_POINT_GRAPHICS,
                        mPipelineLayout,
                        0,
                        3,
                        &mDescriptorSet,
                        0,
                        nullptr);
                        
vkCmdDraw(mCommandBuffer, 3, 1, 0, 0);
vkCmdEndRenderPass(mCommandBuffer);

 

Reference

https://blog.csdn.net/u011087238/article/details/99996827

https://dvsav.ru/vulkan-4/

 

Vulkan Learning — 4. Pipeline. Descriptor Sets. Push Constants — dvsav.ru

Следующий большой объект в объектной модели Vulkan — это Pipeline. Представляет он собой некий конвейер, через который проходят данные (вершины, т

dvsav.ru

https://graphicsimon.tistory.com/64

 

9. Vulkan Pipeline & Pipeline State

Vulkan 그래픽 파이프라인은 OpenGL과 동일 합니다.차이점은 그래픽 파이프라인의 모든 단계를 개발자가 명시적으로 설정 해야합니다. 파이프라인을 생성하고 바인딩 하면 파이프라인을 사용할수

graphicsimon.tistory.com

 

https://docs.vulkan.org/guide/latest/mapping_data_to_shaders.html

https://docs.vulkan.org/guide/latest/vertex_input_data_processing.html

 

Vertex Input Data Processing :: Vulkan Documentation Project

A binding is tied to a position in the vertex buffer from which the vertex shader will start reading data out of during a vkCmdDraw* call. Changing the bindings does not require making any alterations to an app’s vertex shader source code. As an example,

docs.vulkan.org

https://web.engr.oregonstate.edu/~mjb/vulkan/Handouts/DescriptorSets.1pp.pdf

 

'Vulkan' 카테고리의 다른 글

12. Image layout and Staging Buffer  (0) 2024.07.25
11. Image  (0) 2024.07.21
9. Pipeline and Pipeline State  (0) 2024.07.08
8. Vertex Buffer and Memory  (0) 2024.07.05
7. SPIR-V  (0) 2024.07.04