본문 바로가기
Vulkan

12. Image layout and Staging Buffer

by SimonLee 2024. 7. 25.

이미지 리소스를 마저 생성하고

최적 타일링으로 이미지 리소스를 생성하는 방법도 알아봅니다.

그리고 최적화에 필요한 스테이징 버퍼에 대해서도 알아보자.

 

이미지 리소스 생성하는 과정은 다음과 같습니다.

1. 이미지 개체를 생성한다.

2. 이미지 데이터 할당 및 내용 채우기

3. 이미지 샘플러 생성

4. 이미지 뷰 생성

5. 적절한 레이아웃으로 설정

6. 셰이더 수정과 업데이트 디스크립터

5. 적절한 이미지 레이아웃으로 설정

이미지 레이아웃이란 이미지 메모리에 이미지를 저장하는 방식이다.

위에서 메모리 타일 종류인 최적 타일과 리니어 타일에 대해서 설명했었다.

읽기 / 쓰기를 목적으로 최적 레이아웃의 메모리에 직접 액세스를 할수가 없다.

최적 레이아웃의 불투명한 성질로 인하여 한 유형의 레이아웃을 다른 유형의 레이아웃으로 변경이 필요하다.

 

GPU 하드웨어가 최적 레이아웃을 지원하면

레이아웃 전환을 통해 선형 또는 최적 레이아웃으로 데이터를 저장할 수 있다.

레이아웃 전환 과정은 메모리 장벽( Memory Barrier )을 사용하여 진행할 수 있다.

 

메모리 장벽은 지정된 이전 레이아웃과 새 이미지 레이아웃을 검사하고

레이아웃 전환을 실행한다.

 

메모리 장벽은 데이터 읽기와 쓰기를 동기화 하는데 도움이 된다.

메모리 장벽을 삽입하면 메모리 지시를 실행하기에 앞서 

메모리 실행이 실행되기 전 메모리 작동이 완료되었는지를 확인한다.

 

메모리의 장벽 3가지가 있는데 

전역 메모리 장벽, 버퍼 메모리 장벽, 이미지 메모리 장벽

 

우리는 이미지 메모리 장벽을 살펴본다.

이미지 메모리 장벽은 VkImageMemoryBarrier 인스턴스로 표현하며, 

지정된 이미지의 개체 특정 하위 리소스 범위를 통해 다른 메모리 액세스 유형에 적용할 수 있다.

 

할당된 이미지 메모리는 용도에 따라 배치돼야 하는데,

이미지 레이아웃은 주어진 용도 특성에 맞춰서 구현에 지정한 방식으로 메모리 내용을 액세스 할수 있게 한다.

 

Vulkan에서 이미지 레이아웃은 VkImageLayout으로 표현하며, 

열거형의 관련된 필드는 다음과 같다.

// Provided by VK_VERSION_1_0
typedef enum VkImageLayout {
    VK_IMAGE_LAYOUT_UNDEFINED = 0,
    VK_IMAGE_LAYOUT_GENERAL = 1,
    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL = 2,
    VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL = 3,
    VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL = 4,
    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL = 5,
    VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL = 6,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL = 7,
    VK_IMAGE_LAYOUT_PREINITIALIZED = 8,
    ...
} VkImageLayout;

- VK_IMAGE_LAYOUT_UNDEFINED : 이 레이아웃은 장치 액세스를 지원하지 않는다. 이미지 전환에서 initialLayout 또는 oldLayout에 가장 적합하다. 이 레이아웃을 변경해도 갖고 있는 메모리 데이터는 보존 할수는 없다.

- VK_IMAGE_LAYOUT_GENERAL : 이 레이아웃은 모든 유형의 장치 액세스를 지원한다.

 

- VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : 이 레이아웃은 색상 이미지에 매우 적합하다. 따라서 색사 이미지와 VkFrameBuffer의 채택된 첨부에만 사용해야 한다. 이 레이아웃을 사용하려면 이 미지의 사용비트가 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT으로 설정 되어 있어야 한다.

 

- VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : 프레임 버퍼의 깊이/스텐실 첨부에만 사용가능, 프레임 버퍼의 색상 읽기와 드로잉 명령을 사용한 쓰기접근 할수 있다.

- VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : 이 레이아웃은 이미지를 읽기 전용 셰이더 리소스로 사용하낟. 샘플링 이미지 디스크립터, 연결된 이미지 샘플러 디스크립터 또는 읽기 전용의 스토리지 이미지 디스크립터로 사용한다.

- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : 다음 이미지 전송 명령의 소스(SRC)로만 사용할 수 있다.

- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL : 다음 이미지 전송 명령의 목적지(DST)로만 사용할 수 있다.

- VK_IMAGE_LAYOUT_PREINITIALIZED : 호스트에서 내용을 쓰는 이미지의 초기 레이아웃으로 사용하도록 의도된 것이므로, 레이아웃 전환을 먼저 실행하지 않고도 데이터를 메모리에 즉시 쓸 수 있습니다.

이 레이아웃은 VkImageCreateInfo의 initialLayout 멤버로 사용할 수 있습니다. 

현재 VK_IMAGE_LAYOUT_PREINITIALIZED는 VK_IMAGE_TILING_OPTIMAL 이미지에 대해 정의된 표준 레이아웃이 없기 때문에 선형 이미지에서만 유용합니다.

 

메모리 장벽 ( VkImageMemoryBarrier )을 생성하자.

// Provided by VK_VERSION_1_0
typedef struct VkImageMemoryBarrier {
    VkStructureType            sType;
    const void*                pNext;
    VkAccessFlags              srcAccessMask;
    VkAccessFlags              dstAccessMask;
    VkImageLayout              oldLayout;
    VkImageLayout              newLayout;
    uint32_t                   srcQueueFamilyIndex;
    uint32_t                   dstQueueFamilyIndex;
    VkImage                    image;
    VkImageSubresourceRange    subresourceRange;
} VkImageMemoryBarrier;

- sType : VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER

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

- srcAccessMask : 소스에 대한 접근권한

- dstAccessMAsk : 목적지의 대한 접근권한

- oldLayout : 변경 전 레이아웃

- newLayout : 변경 후 레이아웃

- srcQueueFamilyIndex : 소스의 대한 큐 패밀리 인덱스

- dstQueueFamilyIndex : 목적지의 대한 큐 패밀리 인덱스

- image : barrier에 의해 영향 받을 이미지 핸들

- subresourceRange : barrier에 의해 영향받을 이미지 내부 서브 이미지 범위 

 

서브리소스 레인지

// Provided by VK_VERSION_1_0
typedef struct VkImageSubresourceRange {
    VkImageAspectFlags    aspectMask;
    uint32_t              baseMipLevel;
    uint32_t              levelCount;
    uint32_t              baseArrayLayer;
    uint32_t              layerCount;
} VkImageSubresourceRange;

- aspectMask :  이미지 뷰 내부에 포함된 VkImageAspectFlags 비트마스크 

- baseMipLevel : 뷰에 접근 가능한 첫번째 밉 레벨

- levelCount : 뷰에 접근가능한 밉 레벨 개수

- baseArrayLayer : 뷰에 접근 가능한 어레이 레이어

- layerCount : 뷰에 접근 가능한 레이어 개수

 

 

접근 용도를 나타내는 플래그 비트

// Provided by VK_VERSION_1_0
typedef enum VkAccessFlagBits {
    VK_ACCESS_INDIRECT_COMMAND_READ_BIT = 0x00000001,
    VK_ACCESS_INDEX_READ_BIT = 0x00000002,
    VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT = 0x00000004,
    VK_ACCESS_UNIFORM_READ_BIT = 0x00000008,
    VK_ACCESS_INPUT_ATTACHMENT_READ_BIT = 0x00000010,
    VK_ACCESS_SHADER_READ_BIT = 0x00000020,
    VK_ACCESS_SHADER_WRITE_BIT = 0x00000040,
    VK_ACCESS_COLOR_ATTACHMENT_READ_BIT = 0x00000080,
    VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT = 0x00000100,
    VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT = 0x00000200,
    VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT = 0x00000400,
    VK_ACCESS_TRANSFER_READ_BIT = 0x00000800,
    VK_ACCESS_TRANSFER_WRITE_BIT = 0x00001000,
    VK_ACCESS_HOST_READ_BIT = 0x00002000,
    VK_ACCESS_HOST_WRITE_BIT = 0x00004000,
    VK_ACCESS_MEMORY_READ_BIT = 0x00008000,
    VK_ACCESS_MEMORY_WRITE_BIT = 0x00010000,
    ....
} VkAccessFlagBits;

 

생성한 메모리 장벽은 vkCmdPipelineBarrier 함수를 사용해 삽입한다.

// Provided by VK_VERSION_1_0
void vkCmdPipelineBarrier(
    VkCommandBuffer                             commandBuffer,
    VkPipelineStageFlags                        srcStageMask,
    VkPipelineStageFlags                        dstStageMask,
    VkDependencyFlags                           dependencyFlags,
    uint32_t                                    memoryBarrierCount,
    const VkMemoryBarrier*                      pMemoryBarriers,
    uint32_t                                    bufferMemoryBarrierCount,
    const VkBufferMemoryBarrier*                pBufferMemoryBarriers,
    uint32_t                                    imageMemoryBarrierCount,
    const VkImageMemoryBarrier*                 pImageMemoryBarriers);

- commandBuffer : 메모리 장벽이 정의된 커맨드 버퍼

- srcStageMask : 이 비트 마스크 필드는 장벽 구현 전에 수행이 완료돼야 하는 파이프라인 스테이지를 정의

- dstStageMask : 이 비트 마스크 필드는 장벽 이전의 명령을 모두 수행되기 전까지는 시작하면 안되는 파이프라인 스테이지 정의.

- dependencyFlags : 이 필드는 VkDependencyFlagBits 값을 참조한다. 이 값은 장벽이 스크린 공간 지역성이 있는지를 알려준다.

- memoryBarrierCount : 메모리 장벽의 개수

- pMemoryBarriers : VkMemoryBarrier 개체의 배열로, memoryBarrierCount와 개수가 같다.

- bufferMemoryBarrierCount : 버퍼 메모리 장벽의 개수다.

- pBufferMemoryBarrier : VkBufferMemoryBarrier 개체의 배열로, 개수는 BufferMemoryBarrierCount

- imageMemoryBarrierCount : 이미지 유형의 메모리 장벽 개수

- pImageMemoryBarriers : VkImageMemoryBarrier 개체의 배열로, 개수는 imageMemoryBarrierCount 와 같다.

 

 

Usage >>

vkBeginComandBuffer ~ vkEndCommandBuffer 사이에 이미지 레이아웃 변환 코드를 보자.

이미지 메모리 장벽 파라메터를 보면..

type은 VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER

목적지 액세스 비트는 VK_ACCESS_SHADER_READ_BIT

이전 레이아웃은 VK_IMAGE_LAYOUT_PREINITIALIZED

이후 레이아웃은 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,

 

파이프라인 삽입 명령어 파라메터를 보면

수행 완료되어야 하는 파이프라인 스테이지는

VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT

이전의 명령을 모두 수행되기 전까지는 시작하면 안되는 파이프라인 스테이지는           VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT

VkCommandBufferBeginInfo commandBufferBeginInfo{
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
};

VK_CHECK_ERROR(vkBeginCommandBuffer(mCommandBuffer, &commandBufferBeginInfo));

// VkImageLayout 변환
VkImageMemoryBarrier imageMemoryBarrier{
        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .srcAccessMask = VK_ACCESS_NONE,
        .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
        .oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED,
        .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image = mImage,
        .subresourceRange = {
                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                .baseMipLevel = 0,
                .levelCount = 1,
                .baseArrayLayer = 0,
                .layerCount = 1
        }
};

vkCmdPipelineBarrier(mCommandBuffer,
                     VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
                     VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                     0,
                     0,
                     nullptr,
                     0,
                     nullptr,
                     1,
                     &imageMemoryBarrier);

// VkCommandBuffer 기록 종료
VK_CHECK_ERROR(vkEndCommandBuffer(mCommandBuffer));

// VkCommandBuffer 제출
VkSubmitInfo submitInfo{
        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
        .pCommandBuffers = &mCommandBuffer
};

VK_CHECK_ERROR(vkQueueSubmit(mGraphicQueue, 1, &submitInfo, VK_NULL_HANDLE));
VK_CHECK_ERROR(vkQueueWaitIdle(mGraphicQueue));

 

6. 셰이더 수정과 디스크립터 수정.

6.1 버텍스 셰이더에서 텍스처 좌표를 전달하자.

텍스처 좌표를 버텍스 셰이더로 전달 하기 위해  버텍스 셰이더에 " inUv "변수를 추가한다.

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

 

버텍스 셰이더로 텍스처 좌표를 어트리뷰트를 사용하여 전달하자.

파이프라인의 버텍스 입력 스테이트 ( VkPipelineVertexInputStateCreateInfo )의

내부변수 pVertexInputState --> pVertexAttributeDescription 배열에

VkVertexInputAttributeDescription 인스턴스를 추가하면 된다.

VkVertexInputAttributeDescription의 location = 2 값이 ==> 셰이더 location 값과 동일해야 한다.

 

- 버텍스 입력 스테이트 관련해서는 Reference 9. 벌칸 파이프라인, 벌칸 스테이트를 참조하자

VkVertexInputBindingDescription vertexInputBindingDescription{
        .binding = 0,
        .stride = sizeof(Vertex),
        .inputRate = VK_VERTEX_INPUT_RATE_VERTEX
};

array<VkVertexInputAttributeDescription, 3> vertexInputAttributeDescriptions{
        ....
        VkVertexInputAttributeDescription{
                .location = 2,
                .binding = 0,
                .format = VK_FORMAT_R32G32_SFLOAT,
                .offset = offsetof(Vertex, uv)
        }
};

VkPipelineVertexInputStateCreateInfo pipelineVertexInputStateCreateInfo {
        .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
        .vertexBindingDescriptionCount = 1,
        .pVertexBindingDescriptions = &vertexInputBindingDescription,
        .vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexInputAttributeDescriptions.size()),
        .pVertexAttributeDescriptions = vertexInputAttributeDescriptions.data()
};

VkGraphicsPipelineCreateInfo graphicsPipelineCreateInfo{
        .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
        .....
        .pVertexInputState = &pipelineVertexInputStateCreateInfo,
        .....
        .layout = mPipelineLayout,        
};
VK_CHECK_ERROR(vkCreateGraphicsPipelines(mDevice,
                                         VK_NULL_HANDLE,
                                         1,
                                         &graphicsPipelineCreateInfo,
                                         nullptr,
                                         &mPipeline));

 

6.2 프래그먼트 셰이더에서 샘플러를 전달하자.

이미지를 샘플러 형태로 프래그 먼트 셰이더로 전달해본다.

 

프래그 먼트에서 샘플러로 전달 받기 위해 이미지 뷰가 필요하다.

 

레이아웃 설정은 set = 2, binding = 1로 한다.

어트리뷰트를 사용하여 프래그먼트 셰이더로 전달하기 위해  " outUv " 변수도 추가한다.

 

프래그먼트 셰이더

#version 310 es                                                         
precision mediump float;                                                
                                                                        
layout(location = 0) in vec3 inColor;                                   
layout(location = 1) in vec2 inUv;                                      
                                                                        
layout(location = 0) out vec4 outColor;                                 
                                                                        
layout(set = 2, binding = 1) uniform sampler2D combinedImageSampler;    
                                                                        
void main() {                                                           
    vec4 texColor = texture(combinedImageSampler, inUv);                
    outColor = vec4(inColor * texColor.rgb, 1.0);                       
}

 

이제 uniform sampler2D에 데이터를 전달할 디스크립터 세트를 생성하기 위해

아래와 같은 절차를 진행합니다.

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

2. 디스크립터 풀 생성 

3. 디스크립터 세트 생성 

4. 디스크립터 업데이트

아래 내용도 디스크립터 이전 챕터를 참조하자.

 

6.3 디스크립터 세트 레이아웃 생성.

std::vector<VkDescriptorSetLayoutBinding> bindingC {
        // uniform binding set : 2, binding : 1 for FARG_SHADER
        VkDescriptorSetLayoutBinding {
                .binding = 1,
                .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                .descriptorCount = 1,
                .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT
        },
};
VkDescriptorSetLayoutCreateInfo infoC {
        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
        .bindingCount = 1,
        .pBindings = bindingC.data()
};
VK_CHECK_ERROR(vkCreateDescriptorSetLayout(mDevice,
                                           &infoC,
                                           nullptr,
                                           &mDescriptorSetLayout[2]));

 

6.4 디스크립터 풀 생성 및 디스크립터 세트 생성

VkDescriptorPoolSize descriptorPoolSize[4];
...
descriptorPoolSize[2].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorPoolSize[2].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));

VkDescriptorSetAllocateInfo descriptorSetAllocateInfo {
        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
        .descriptorPool = mDescriptorPool,
        .descriptorSetCount = 3,
        .pSetLayouts = mDescriptorSetLayout
};
VK_CHECK_ERROR(vkAllocateDescriptorSets(mDevice, &descriptorSetAllocateInfo, &mDescriptorSet[0]));

 

6.5 디스크립터 세트 업데이트

생성한 이미지뷰와 샘플러와 디스크립터 세트를 연결하기 위해 디스크립터 업데이트를 합니다.

- 현재 이미지 뷰 내부에 이미지가 있고 이미지와 이미지 버퍼와 연결이 되어있는 상태이다.

 

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

- device : 디스크립터 세트를 업데이트할 논리장치

- desriptorWriteCount : pDescriptorWrites 배열의 개수

- descriptorWrites : 디스크립터 세트를 wrtie 하는 것의 대한 정보 배열

- descriptorCopyCount : pDescriptorCopies 배열의 개수

- pDescriptorCopies : 디스크립터 세트를 copy 하는 것의 대한 정보 배열

 

디스크립터 이미지 인포 메타데이터

imageLayout 은 셰이더에서 읽기 전용을 사용하기 때문에,

VK_IMAGE_LAYOUT_SHADER_ONLY_OPTIMAL 을 사용한다.

// Provided by VK_VERSION_1_0
typedef struct VkDescriptorImageInfo {
    VkSampler        sampler;
    VkImageView      imageView;
    VkImageLayout    imageLayout;
} VkDescriptorImageInfo;

- sampler : 이미지 뷰와 연결할 샘플러

- imageView : 샘플러와 연결한 이미지뷰

- imageLayout : 이미지 레이아웃 정보

 

VkWriteDescriptorSet는 리소스당 1:1 매핑되어야 함.

// 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_DESCRIPTOR_SET_ALLOCATE_INFO

- pNext : 확장판 구조체

- dstSet : 할당된 디스크립터 세트

- dstBinding : 셰이더에서 바인딩된 값

- descriptorCount : 디스크립터 개수

- descriptorType : 디스크립터 타입

- pImageInfo : 디스크립터 이미지 인포 메타데이터 포인터

 

 

Usage >>

VkDescriptorImageInfo descriptorImageInfo{
        .sampler = mSampler,
        .imageView = mImageView,
        .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
};

array<VkWriteDescriptorSet, 2> writeDescriptorSets {
        ......
        VkWriteDescriptorSet {
            .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
            .dstSet = mDescriptorSet[2],
            .dstBinding = 1,
            .descriptorCount = 1,
            .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            .pImageInfo = &descriptorImageInfo
        }
};

vkUpdateDescriptorSets(mDevice, writeDescriptorSets.size(), writeDescriptorSets.data(), 0, nullptr);

 

렌더링 하기전에 커맨드 버퍼에 디스크립터 세트를 바인딩한 후 렌더링하자.

vkCmdBindDescriptorSets(mCommandBuffer,
                        VK_PIPELINE_BIND_POINT_GRAPHICS,
                        mPipelineLayout,
                        0,
                        3,
                        mDescriptorSet,
                        0,
                        nullptr);


Staging Buffer

CPU와 GPU 메모리를 공유하는 방식 " VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT  " 을 사용하면

GPU 에서 메모리를 읽는 속도가 느립니다.

 

버텍스 버퍼의 경우 유니폼 데이터와는 달리 한번 데이터를 메모리에 복사 하고 뒤로 변경이 없기 때문에,

상대적으로 느린 방식 ( " VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT  ")을 사용할 필요가 없습니다.

 

GPU에서만 메모리에 접근 가능한 메모리인

" VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT " 으로 바꾸어 줘야 하는데 

한 가지 문제점은 해당 메모리는 CPU에서 접근이 되지 않아

초기 버텍스 데이터를 메모리에 복사가 불가능 합니다.

 

그래서 초기 데이터를 복사하기 위해 CPU 접근 가능한 버퍼 및 메모리를 생성해야 합니다.

이를 "스테이징 버퍼" 라고 합니다.

스테이징 버퍼 생성 후, 스테이징 버퍼의 내용을 채워 주고

GPU만 접근가능한 버퍼를 만든 뒤, 스테이징 버퍼로 부터 복사를 해야 합니다.

 

스테이징 버퍼를 해제하는 시점은

복사가 도중 하면 안되기 떄문에, 데이터가 전송이 끝난 시점에 해야 합니다.

그 시점은 커맨드 큐에서 처리가 다 끝난 시점이어야 합니다.

 

GPU 에서만 접근 가능한 버텍스 버퍼 생성

 

버퍼를 생성할때 스테이징 버퍼로 부터 데이터를 받기 위해 usage 플래그에

" VK_BUFFER_USAGE_TRANSFER_DST_BIT " 설정을 한다.

버퍼 메모리 할당할때는 " VK MEMORY_PROPERTY_DEVICE_LOCAL_BIT " 비트가 설정된 메모리를 할당받는다.

 

VkBufferCreateInfo bufferCreateInfo{
        .sType =VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .size = mVertexDataSize,
        .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
};

vkCreateBuffer(mDevice, &bufferCreateInfo, nullptr, &mVertexBuffer);

// 사용할수 있는 메모리 타입 알아오기
vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mPhysicalDeviceMemoryProperties);

// 어떤 메모리 타입, 얼마나 할당해야하는지
VkMemoryRequirements vertexMemoryRequirements;
vkGetBufferMemoryRequirements(mDevice, mVertexBuffer, &vertexMemoryRequirements);
aout << "memory size : " << vertexMemoryRequirements.size << std::endl;

// Vertex VkDeviceMemory를 할당 할 수 있는 메모리 타입 인덱스 얻기
uint32_t vertexMemoryTypeIndex;
vkGetMemoryTypeIndex(mPhysicalDeviceMemoryProperties,
                                    vertexMemoryRequirements,
                                    // 스테이징 버퍼를 사용하기 때문에, 버텍스 버퍼는 오직 GPU R/W 가능하도록 만듬.
                                    // VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 사용
                                    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                                    &vertexMemoryTypeIndex);

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

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

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

 

 

CPU, GPU 접근 가능한 스테이징 버퍼 생성 코드

 

스테이징 버퍼는 생성할때 usage를 " VK_BUFFER_USAGE_TRANSFER_SRC_BIT "로 설정한다.

메모리 할당은  CPU, GPU 공유가 가능해야 하므로

" VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT " 비트로 설정한다. 

버퍼가 생성 및 바인딩이 완료되면, 스테이징 버퍼에 넘겨줄 데이터를 채워준다.

 

vkCmdCopyBuffer를 통해 스테이징 버퍼에서 위에서 생성한 버텍스 버퍼로 복사를 해준다.

VkBufferCopy 설정할때, 전부를 복사하려면 offset을 지정하지 않아도 된다.

// Provided by VK_VERSION_1_0
void vkCmdCopyBuffer(
    VkCommandBuffer                             commandBuffer,
    VkBuffer                                    srcBuffer,
    VkBuffer                                    dstBuffer,
    uint32_t                                    regionCount,
    const VkBufferCopy*                         pRegions);

- commandBuffer : 커맨드를 넣어줄 커맨드 버퍼

- srcBuffer : source buffer

- dstBuffer : destination buffer

- regionCount : pRegions의 개수

- pRegions : VkBufferCopy 포인터

typedef struct VkBufferCopy {
    VkDeviceSize    srcOffset;
    VkDeviceSize    dstOffset;
    VkDeviceSize    size;
} VkBufferCopy;

 

 

스테이징 버퍼의 해제 시점은 커맨드 버퍼의 커맨드가 실행 완료가 된 후 이다.

vkQueueWaitIdle(mGraphicQueue);

// Staging VkBuffer 생성
VkBufferCreateInfo stagingBufferCreateInfo{
        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .size = mVertexDataSize,
        .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT
};

VkBuffer stagingBuffer;
vkCreateBuffer(mDevice, &stagingBufferCreateInfo, nullptr, &stagingBuffer);

// 사용할수 있는 메모리 타입 알아오기
vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mPhysicalDeviceMemoryProperties);

// Staging VkBuffer의 VkMemoryRequirements 얻기
VkMemoryRequirements stagingMemoryRequirements;
vkGetBufferMemoryRequirements(mDevice, stagingBuffer, &stagingMemoryRequirements);

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

// Staging VkDeviceMemory 할당
VkMemoryAllocateInfo stagingMemoryAllocateInfo{
        .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .allocationSize = stagingMemoryRequirements.size,
        .memoryTypeIndex = stagingMemoryTypeIndex
};

VkDeviceMemory stagingMemory;
vkAllocateMemory(mDevice, &stagingMemoryAllocateInfo, nullptr, &stagingMemory);

// Staging VkBuffer와 Staging VkDeviceMemory 바인드
VK_CHECK_ERROR(vkBindBufferMemory(mDevice, stagingBuffer, stagingMemory, 0));

// 스테이징 버퍼에 데이터 채우기
void *stagingData;
VK_CHECK_ERROR(vkMapMemory(mDevice, stagingMemory, 0, mVertexDataSize, 0, &stagingData));
memcpy(stagingData, mVertices.data(), mVertexDataSize);
vkUnmapMemory(mDevice, stagingMemory);

// 버텍스 버퍼에 복사
VkCommandBufferBeginInfo commandBufferBeginInfo{
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
};

vkBeginCommandBuffer(commandBuffer, &commandBufferBeginInfo);

VkBufferCopy bufferCopy{
        .size = mVertexDataSize
};
vkCmdCopyBuffer(commandBuffer, stagingBuffer, mVertexBuffer, 1, &bufferCopy);

// VkCommandBuffer 기록 종료
VK_CHECK_ERROR(vkEndCommandBuffer(commandBuffer));

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

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

vkFreeMemory(mDevice, stagingMemory, nullptr);
vkDestroyBuffer(mDevice, stagingBuffer, nullptr);

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

Reference

https://graphicsimon.tistory.com/64

 

9. Vulkan Pipeline & Pipeline State

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

graphicsimon.tistory.com

https://graphicsimon.tistory.com/63

 

8. Vertex Buffer and Memory

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

graphicsimon.tistory.com

 

 

 

 

'Vulkan' 카테고리의 다른 글

10 - 1. Descriptor 추가 이해  (0) 2024.08.05
11. Image  (0) 2024.07.21
10. Descriptor set and Descriptor set layout  (0) 2024.07.12
9. Pipeline and Pipeline State  (0) 2024.07.08
8. Vertex Buffer and Memory  (0) 2024.07.05