Vulkan은 애플리케이션이 메모리를 직접 제어할 수 있게 하여 성능을 극대화할 수 있지만 메모리에 대한 정확한 이해가 없이 코드를 작성하면 오히려 비효율적으로 동작할 수도 있다.

 

목차

     

     


     

     

     

     

    Vulkan Memory


     

     

    Vulkan Memory란?

     

    Vulkan Memory는 실제 메모리 공간을 대표하는 리소스다. (OpenGL에는 없다)

     

    Vulkan은 OpenGL과 달리 Memory독립된 리소스정의했다. 이러한 설계는 Memory 할당매우 무거운 작업이기 때문에 Vulkan에서는 Memory재활용을 하기 위함이다. (이러한 설계는 메모리 할당이 매우 무거운 작업이기 때문에 개발자가 직접 메모리를 관리하는것이 더 효율적일 것이라는 판단에서 비롯되었다. 흐음... 과연 그럴까?

     

     

    위의 예시) Buffer와 해당 Buffer를 위한 메모리가 있다고 가정하자. Buffer가 더 이상 필요 없어져 파괴되었을때도 메모리는 유지할수 있다. 이렇게 하면 새로운 Buffer가 필요할 때 새로운 메모리를 할당하는 대신 기존의 메모리를 재사용함으로써 속도를 높일수 있다.


     

     

    Memory의 종류

     

    컴퓨터의 사양을 확인하면 DRAMVRAM이 별도로 존재다.

    개발자는 목적에 따라 DRAM 또는 VRAM을 선택해서 사용해야 한다.

     

     

     

     

     

    어느 RAM을 사용할지는 사용 목적에 따라 달라진다.

     

    CPU가 자주 접근해야 하는 경우 DRAM이 적합하다. GPU가 자주 접근해야 하는 경우 VRAM이 적합하다.

     

     

    예를들어, Uniform Buffer는 CPU가 자주 수정하기 때문에 DRAM이 적합하다. 반면, Vertex Buffer는 GPU만 읽기 때문에 VRAM이 적합하다. 

     

     


     

     

    OpenGL의 Memory 선택

     

    OpenGL 역시 사용 목적에 따라 Memory를 선택할 수 있다.

     

    그러나 많은 OpenGL 개발자들이 이러한 선택에 대해 잘 알지 못한다. 이는 대부분의 예제에서 설명 없이 GL_STATIC_DRAW만 사용되기 때문이다.

     

    void glBufferData(GLenum target,
         GLsizeiptr size,
         const void * data,
         GLenum usage);

     

     

     

    GL_STATIC_DRAW는 데이터가 변경되지 않는다는 것을 의미하여, MemoryVRAM에 할당된다.

    GL_DYNAMIC_DRAW는 데이터가 자주 변경된다는 것을 의미하므로, MemoryDRAM 또는 VRAM에 할당된다.

     

     

    GL_STATIC_DRAW

    데이터가 변경되지 않기 때문에 CPU에서 GPU로 단 한 번만 데이터를 복사하는 경우에 적합하다. 예를 들어 Vertex 데이터나 Texture가 이에 해당된다. 초기화 이후에는 GPU만 접근하기 때문에 GPU가 가장 빠르게 접근할 수 있는 VRAM이 적합하다.

     

    GL_DYNAMIC_DRAW

    데이터가 자주 변경 되는 경우, 만약 메모리를 VRAM에 할당하면 데이터가 변경될 때마다 CPU와 GPU 간의 복사가 이뤄져야 한다. 메모리 연산은 매우 느린 연산이기 때문에 성능 저하가 발생한다. 그래서 이런 경우에는 DRAM에 메모리를 할당하는 것이 더 적합하다. 

     

    위와 같이 OpenGL에서는 상황에 따라 메모리선택이 달라진다. 하지만 개발자는 위와 같은 사실을 간과하고 프로그래밍하는 경우가 종종 발생한다. 이러한 실수를 방지하기 위해 Vulkan은 메모리를 Public API로 노출시켰다.   


     

     

    Vulkan의 Memory 속성 얻기

     

    void vkGetPhysicalDeviceMemoryProperties(
         VkPhysicalDevice physicalDevice,
         VkPhysicalDeviceMemoryProperties* pMemoryProperties);

     

    파라미터 설명 
    physicalDevice   VkPhysicalDevice
    pMemoryProperties   VkPhysicalDeviceMemoryProperties 변수의 포인터

     


     

     

    VkPhysicalDeviceMemoryProperties 구조체

     

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

     

    멤버 변수 설명 
     memoryTypeCount   VkMemoryType 개수
     memoryTypes   VkMemoryType 배열
     memoryHeapCount  
     memoryHeaps  

     


     

     

    VkMemoryType 구조체

     

    typedef struct VkMemoryType {
         VkMemoryPropertyFlags propertyFlags;
         uint32_t heapIndex;
    } VkMemoryType;

     

    멤버 변수 설명 
     propertyFlags   VkMemoryPropertyFlagBits의 조합
     heapIndex   VkMemoryHeap 배열의 인덱스

     

     

    VkMemoryPropertyFlagBits 열거형

     

    VkMemoryPropertyFlagBits 열거형은 메모리의 다양한 속성을 나타낸다.

     

    typedef enum VkMemoryPropertyFlagBits {
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT = 0x00000001,		// GPU에 존재하는 메모리(=VRAM)
         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT = 0x00000002,		// CPU에서 접근할 수 있는 메모리
         VK_MEMORY_PROPERTY_HOST_COHERENT_BIT = 0x00000004,
         VK_MEMORY_PROPERTY_HOST_CACHED_BIT = 0x00000008,
         VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT = 0x00000010,
         VK_MEMORY_PROPERTY_PROTECTED_BIT = 0x00000020,
         VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD = 0x00000040,
         VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD = 0x00000080,
         VK_MEMORY_PROPERTY_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
    } VkMemoryPropertyFlagBits;

     

    HOSTCPU

    DEVICEGPU를 의미한다.


     

     

    VkMemoryPropertyFlagBits 열거형

     

    시스템은 DRAM과 VRAM을 가지고 있으며, CPU와 GPU가 이 두 RAM을 사용하기 위해서는 각각의 가상 주소 공간에 매핑되어 있어야 한다.

     

    VkMemoryPropertyFlagBits 열거형은 어떤 메모리가 어떤 가상 주소 공간에 매핑 되는지를 의미한다. 

     

    DEVICE_LOCAL_BIT은 DEVICE만 볼 수 있는 메모리를 의미한다. VRAM을 의미하며 이 메모리는 GPU가 가장 빠르게 접근할 수 있다. 이 메모리는 HOST가 접근할 수 없기 때문에 GPU만 접근하는 Vertex buffer나 Texture와 같은 리소스에 적합하다.

     

    HOST_VISIBLE_BIT은 HOST도 볼 수 있는 메모리를 의미한다. DRAM과 VRAM 모두 HOST_VISIBLE_BIT을 가질 수 있으며 이를 위해서 같은 메모리가 두 가상 주소 공간에 매핑되어 있어야 한다. CPU, GPU 모두 접근할 수 있기 때문에 Lead pixel과 같은 작업을 하기 위한 리소스에 적합하다.

     

    HOST_CACHED_BIT은 HOST에 cache가 있는 메모리를 의미한다. HOST에 cache만 있다면 DRAM, VRAM 모두 HOST_CACHED_BIT을 가질 수 있다. 당연히 이를 위해서 같은 메모리가 두 가상 주소 공간에 맵핑되어 있어야 한다. 이 메모리는 HOST에 cache가 있기 때문에 읽는 연산에 매우 빠르다. 예를 들어, 아래 그림과 같이 VRAM이 두 가상 주소 공간에 매핑되어 있다고 가정하자.  HOST_CACHED_BIT이 없다면 VRAM으로부터 데이터를 읽어와야 한다. HOST 에서 DRAM을 바로 읽는 것보다는 느리다. 하지만 cache가 있기 때문에 빠르게 읽을 수 있다. 그러므로 이 메모리는 HOST에서 읽기 쓰기가 빈번한 Uniform buffer와 같은 리소스가 적합하다. 또한 이러한 속성들은 조합될수 있어 메모리 선택의 유연성을 더욱 향상시킨다.

     

     

     

    COHERENT_BIT은 두 메모리 간의 동기화를 자동으로 처리하는 기능을 제공한다. 이 Bit는 주로 CACHED_BIT과 같이 활성화되어 있다. HOST에 cache가 있다는 것은 HOST에서 변경한 데이터가 실제 메모리에 적용되기 전까지는 cache에만 존재하고 있다는 의미다. 이로 인해 HOST에서 데이터를 변경하고 DEVICE가 이를 읽을 때 디바이스는 cache에 남아있지 않은 이전 데이터를 읽을 수 있다. 일반적으로 동기화를 해서 개발자가 명시적으로 flash를 호출해야 하지만 COHERENT_BIT 활성화되어 있으면 운영체제가 이를 보장해준다.  

     

     


     

     

    Vulkan의 Memory 속성

     

    아래의 표는 AMD의 CPU와 GPU를 사용할 때 이용할 수 있는 메모리 타입들을 보여준다. 

     

    0번 타입은 VRAM에 위치하며 오직 GPU만 접근할 수 있다. 따라서 GPU가 이 메모리에 접근하는 속도는 매우 빠르다. 이 메모리는 DEVICE_LOCAL_BIT이 활성화되어 있을 것이다. 

     

    3번 타입은 VRAM에 위치하며 GPU와 CPU가 모두 접근할 수 있다. 이 위치 때문에 CPU에서의 접근은 빠르지만 GPU에서의 접근은 상대적으로 느립니다. 또한 이 메모리는 CPU의 cache도 존재한다. 이런 특성으로 인해 이 메모리 타입은 HOST_VISIBLE_BITHOST_CACHED_BIT이 활성화되어 있을 것이다.


     

     

    VkMemoryAllocateInfo 구조체

     

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

     

    멤버 변수 설명 
    sType   구조체의 타입
    pNext   NULL 또는 확장 기능 구조체의 포인터
    allocationSize   Memory의 바이트 크기
    memoryTypeIndex   Memory 타입 인덱스

     


     

     

    VkBuffer에 필요한 Memory 요구사항 얻기

     

    void vkGetBufferMemoryRequirements(
         VkDevice device,
         VkBuffer buffer,
         VkMemoryRequirements* pMemoryRequirements);

     

    파라미 설명 
     device   VkDevice
     buffer   VkBuffer
     pMemoryRequirement   VkMemoryRequirements 변수의 포인터

     

     

    VkMemoryRequirements 구조체

     

    함수를 호출해서 얻어온 결과는 VkMemoryRequirements 구조체 변수에 저장된다.

     

    typedef struct VkMemoryRequirements {
         VkDeviceSize size;
         VkDeviceSize alignment;
         uint32_t memoryTypeBits;
    } VkMemoryRequirements;

     

    멤버 변수 설명 
     size   최소 할당해야하는 Memory의 바이트 크기
     alignment   Memory의 바이트 얼라인먼트. (Memory Offset을 지정할 때 이 alignment에 맞춰서 Offset을 정의해야 한다.)
     memoryTypeBits   Memory가 할당될 수 있는 Memory 타입의 집합

     

     

    memoryTypeBits는 사용 가능한 메모리 타입을 알려준다.

    예를 들어, 위와 같이 1, 0, 0, 1, 1, 0, 0, 0으로 정의되어 있다면 2는 0번째, 3번째, 4번째 메모리 타입을 사용할수 있다는 의미다. 

    이 숫자들은 VkPhysicalDeviceMemoryProperties의 메모리 타입 배열에서 해당 메모리 타입의 인덱스를 나타낸다.


     

     

    필요한 Memory 타입 인덱스 찾기

     

    메모리를 할당하기 위해 먼저 얻어온 정보를 통해 사용 가능한 메모리 타입을 파악해야 된다. 그 다음 이들 중에서 우리의 요구사항을 만족하는 메모리 타입을 선택해야 된다. 아래의 코드는 이 과정을 함수로 구현하고 그 함수를 호출하는 과정이다. 

     

    mPhysicalDeviceMemoryProperties와 vertextMemoryRequirements는 Vulkan으로부터 얻어온 정보다.

    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT과 VK_MEMORY_PROPERTY_HOST_COHERENT_BIT은 우리의 요구사항이다. 이 요구사항을 만족하는 메모리 타입을 찾으면 그 메모리 타입의 인덱스가 함수의 마지막 파라미터로 반환된다.

    vkGetMemoryTypeIndex는 Vulkan에서 제공하는 함수가 아닌 직접 만든 유틸리티 함수다.

     

    메모리 타입을 순회.

    해당 메모리 타입을 사용할수 있는지 검사. 

    해당 메모리 타입이 우리가 요구하는 메모리 속성을 가지고 있는지 확인.

     

    이 모든 조건을 만족하는경우, 해당 메모리 타입을 우리가 사용할 수 있는것이라고 판단하고 그 값을 반환한다.


     

     

    VkMemoryAllocateInfo 구조체

     

     

     


     

     

    Vulkan Memory 할당

     

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

     

    파라미터 설명 
     device   VkDevice
     pAllocateInfo   VkMemoryAllocateInfo 변수의 포인터
     pAllocator   일단 NULL을 사용
     pMemory   VkDeviceMemory 변수의 포인터

     




    Vulkan Memory 해제

     

    void vkFreeMemory(
         VkDevice device,
         VkDeviceMemory memory,
         const VkAllocationCallbacks* pAllocator);

     

    파라미터 설명 
     device   VkDevice
     pAllocateInfo   VkMemoryAllocateInfo 변수의 포인터
     pAllocator   일단 NULL을 사용

     


     

     

    코드

     

    #include ...
    using namespace std;
    
    VkRenderer::VkRenderer(ANativeWindow *window) {
        // 1. VkInstance 생성
        // 2. VkPhysicalDevice 선택
        
        // ================================================================================
        // 3. VkPhysicalDeviceMemoryProperties 얻기
        // ================================================================================
        vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mPhysicalDeviceMemoryProperties);
        
        // 4. VkDevice 생성
        // 5. VkSurface 생성
        // 6. VkSwapchain 생성
        
        mSwapchainImageViews.resize(swapchainImageCount); // ImageView를 Swapchain의 개수만큼 생성
        for (auto i = 0; i != swapchainImageCount; ++i) {
            // 7. VkImageView 생성 
        }
        // 8. VkCommandPool 생성
        // 9. VkCommandBuffer 할당
        // 10. VkFence 생성
        // 11. VkSemaphore 생성
        // 12. VkRenderPass 생성
        
        mFramebuffers.resize(swapchainImageCount);
        for (auto i = 0; i != swapchainImageCount; ++i) {
            // 13. VkFramebuffer 생성
        }
       
        // 14. Vertex VkShaderModule 생성    
        // 15. Fragment VkShaderModule 생성
        // 16. VkPipelineLayout 생성
        // 17. Graphics VkPipeline 생성
        // 18. Vertex VkBuffer 생성
        
        // ================================================================================
        // 19. Vertex VkBuffer의 VkMemoryRequirements 얻기
        // ================================================================================
        VkMemoryRequirements vertexMemoryRequirements;
        vkGetBufferMemoryRequirements(mDevice, mVertexBuffer, &vertexMemoryRequirements);
    
        // ================================================================================
        // 20. Vertex VkDeviceMemory를 할당 할 수 있는 메모리 타입 인덱스 얻기
        // ================================================================================
        uint32_t vertexMemoryTypeIndex;
        VK_CHECK_ERROR(vkGetMemoryTypeIndex(mPhysicalDeviceMemoryProperties,
                                            vertexMemoryRequirements,
                                            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                                            VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                                            &vertexMemoryTypeIndex));
    
        // ================================================================================
        // 21. Vertex VkDeviceMemory 할당
        // ================================================================================
        VkMemoryAllocateInfo vertexMemoryAllocateInfo{
            .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
            .allocationSize = vertexMemoryRequirements.size,
            .memoryTypeIndex = vertexMemoryTypeIndex
        };
    
        VK_CHECK_ERROR(vkAllocateMemory(mDevice, &vertexMemoryAllocateInfo, nullptr, &mVertexMemory));
    }
    
    VkRenderer::~VkRenderer() { 
    	vkFreeMemory(mDevice, mVertexMemory, nullptr);
        ...
    }
    
    void VkRenderer::render() {
        // 1. 화면에 출력할 수 있는 VkImage 얻기
        // 2. VkFence 기다린 후 초기화
        // 3. VkCommandBuffer 초기화
        // 4. VkCommandBuffer 기록 시작
        // 5. VkRenderPass 시작
        // 6. Graphics VkPipeline 바인드
        // 7. 삼각형 그리기
        // 8. VkRenderPass 종료
        // 9. Clear 색상 갱신
        // 10. VkCommandBuffer 기록 종료
        // 11. VkCommandBuffer 제출
        // 12. VkImage 화면에 출력
    }

     

     

    전체코드

    더보기
    // MIT License
    //
    // Copyright (c) 2024 Daemyung Jang
    //
    // Permission is hereby granted, free of charge, to any person obtaining a copy
    // of this software and associated documentation files (the "Software"), to deal
    // in the Software without restriction, including without limitation the rights
    // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    // copies of the Software, and to permit persons to whom the Software is
    // furnished to do so, subject to the following conditions:
    //
    // The above copyright notice and this permission notice shall be included in all
    // copies or substantial portions of the Software.
    //
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    // SOFTWARE.
    
    #include <cassert>
    #include <array>
    #include <vector>
    #include <iomanip>
    
    #include "VkRenderer.h"
    #include "VkUtil.h"
    #include "AndroidOut.h"
    
    using namespace std;
    
    struct Vector3 {
        union {
            float x;
            float r;
        };
    
        union {
            float y;
            float g;
        };
    
        union {
            float z;
            float b;
        };
    };
    
    struct Vertex {
        Vector3 position;
        Vector3 color;
    };
    
    VkRenderer::VkRenderer(ANativeWindow *window) {
        // ================================================================================
        // 1. VkInstance 생성
        // ================================================================================
        // VkApplicationInfo 구조체 정의
        VkApplicationInfo applicationInfo{
            .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
            .pApplicationName = "Practice Vulkan",
            .applicationVersion = VK_MAKE_API_VERSION(0, 0, 1, 0),
            .apiVersion = VK_MAKE_API_VERSION(0, 1, 3, 0)
        };
    
        // 사용할 수 있는 레이어를 얻어온다.
        uint32_t instanceLayerCount;
        VK_CHECK_ERROR(vkEnumerateInstanceLayerProperties(&instanceLayerCount, nullptr));
    
        vector<VkLayerProperties> instanceLayerProperties(instanceLayerCount);
        VK_CHECK_ERROR(vkEnumerateInstanceLayerProperties(&instanceLayerCount,
                                                          instanceLayerProperties.data()));
    
        // 활성화할 레이어의 이름을 배열로 만든다.
        vector<const char*> instanceLayerNames;
        for (const auto &layerProperty : instanceLayerProperties) {
            instanceLayerNames.push_back(layerProperty.layerName);
        }
    
        uint32_t instanceExtensionCount; // 사용 가능한 InstanceExtension 개수
        VK_CHECK_ERROR(vkEnumerateInstanceExtensionProperties(nullptr,
                                                              &instanceExtensionCount,
                                                              nullptr));
    
        vector<VkExtensionProperties> instanceExtensionProperties(instanceExtensionCount);
        VK_CHECK_ERROR(vkEnumerateInstanceExtensionProperties(nullptr,
                                                              &instanceExtensionCount,
                                                              instanceExtensionProperties.data()));
    
        vector<const char *> instanceExtensionNames; // instanceExtensionName을 담는 배열
        for (const auto &properties: instanceExtensionProperties) {
            if (properties.extensionName == string("VK_KHR_surface") ||
                properties.extensionName == string("VK_KHR_android_surface")) {
                instanceExtensionNames.push_back(properties.extensionName);
            }
        }
        assert(instanceExtensionNames.size() == 2); // 반드시 2개의 이름이 필요하기 때문에 확인
    
        // sType: 구조체의 타입, pApplicationInfo: 어플리케이션의 이름
        // enabledLayerCount, ppEnableLayerNames: 사용할 레이어의 정보를 정의
        VkInstanceCreateInfo instanceCreateInfo{
            .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
            .pApplicationInfo = &applicationInfo,
            .enabledLayerCount = static_cast<uint32_t>(instanceLayerNames.size()),
            .ppEnabledLayerNames = instanceLayerNames.data(),
            .enabledExtensionCount = static_cast<uint32_t>(instanceExtensionNames.size()),
            .ppEnabledExtensionNames = instanceExtensionNames.data()
        };
    
        // vkCreateInstance로 인스턴스 생성. 생성된 인스턴스가 mInstance에 쓰여진다.
        VK_CHECK_ERROR(vkCreateInstance(&instanceCreateInfo, nullptr, &mInstance));
    
    
        // ================================================================================
        // 2. VkPhysicalDevice 선택
        // ================================================================================
        uint32_t physicalDeviceCount;
        VK_CHECK_ERROR(vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, nullptr));
    
        vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
        VK_CHECK_ERROR(vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, physicalDevices.data()));
    
        // 간단한 예제를 위해 첫 번째 VkPhysicalDevice를 사용
        mPhysicalDevice = physicalDevices[0];
    
        VkPhysicalDeviceProperties physicalDeviceProperties; // 이 구조체 안에 GPU에 필요한 모든 정보가 있다.
        vkGetPhysicalDeviceProperties(mPhysicalDevice, &physicalDeviceProperties);
    
        aout << "Selected Physical Device Information ↓" << endl;
        aout << setw(16) << left << " - Device Name: "
             << string_view(physicalDeviceProperties.deviceName) << endl;
        aout << setw(16) << left << " - Device Type: "
             << vkToString(physicalDeviceProperties.deviceType) << endl;
        aout << std::hex;
        aout << setw(16) << left << " - Device ID: " << physicalDeviceProperties.deviceID << endl;
        aout << setw(16) << left << " - Vendor ID: " << physicalDeviceProperties.vendorID << endl;
        aout << std::dec;
        aout << setw(16) << left << " - API Version: "
             << VK_API_VERSION_MAJOR(physicalDeviceProperties.apiVersion) << "."
             << VK_API_VERSION_MINOR(physicalDeviceProperties.apiVersion);
        aout << setw(16) << left << " - Driver Version: "
             << VK_API_VERSION_MAJOR(physicalDeviceProperties.driverVersion) << "."
             << VK_API_VERSION_MINOR(physicalDeviceProperties.driverVersion);
    
        // ================================================================================
        // 3. VkPhysicalDeviceMemoryProperties 얻기
        // ================================================================================
        vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mPhysicalDeviceMemoryProperties);
    
        // ================================================================================
        // 4. VkDevice 생성
        // ================================================================================
        uint32_t queueFamilyPropertiesCount;
    
        //---------------------------------------------------------------------------------
        //** queueFamily 속성을 조회
        // 사용 가능한 queueFamily의 수(=queueFamilyPropertiesCount)를 얻어온다.
        vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueFamilyPropertiesCount, nullptr);
    
        vector<VkQueueFamilyProperties> queueFamilyProperties(queueFamilyPropertiesCount);
        // 해당 queueFamily들의 속성을 배열에 얻어온다.
        vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueFamilyPropertiesCount, queueFamilyProperties.data());
        //---------------------------------------------------------------------------------
    
        // 특정 queueFamilyProperties가 VK_QUEUE_GRAPHICS_BIT를 지원하는지 확인.
        // 지원하는 queueFamilyProperties를 찾으면 break. queueFamily에 대한 정보는 mQueueFamilyIndex에 저장.
        for (mQueueFamilyIndex = 0;
             mQueueFamilyIndex != queueFamilyPropertiesCount; ++mQueueFamilyIndex) {
            if (queueFamilyProperties[mQueueFamilyIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
                break;
            }
        }
    
        // 생성할 큐를 정의
        const vector<float> queuePriorities{1.0};
        VkDeviceQueueCreateInfo deviceQueueCreateInfo{
                .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
                .queueFamilyIndex = mQueueFamilyIndex,      // queueFamilyIndex
                .queueCount = 1,                            // 생성할 큐의 개수
                .pQueuePriorities = queuePriorities.data()  // 큐의 우선순위
        };
    
        uint32_t deviceExtensionCount; // 사용 가능한 deviceExtension 개수
        VK_CHECK_ERROR(vkEnumerateDeviceExtensionProperties(mPhysicalDevice,
                                                            nullptr,
                                                            &deviceExtensionCount,
                                                            nullptr));
    
        vector<VkExtensionProperties> deviceExtensionProperties(deviceExtensionCount);
        VK_CHECK_ERROR(vkEnumerateDeviceExtensionProperties(mPhysicalDevice,
                                                            nullptr,
                                                            &deviceExtensionCount,
                                                            deviceExtensionProperties.data()));
    
        vector<const char *> deviceExtensionNames;
        for (const auto &properties: deviceExtensionProperties) {
            if (properties.extensionName == string("VK_KHR_swapchain")) {
                deviceExtensionNames.push_back(properties.extensionName);
            }
        }
        assert(deviceExtensionNames.size() == 1); // VK_KHR_swapchain이 반드시 필요하기 때문에 확인
    
        // 생성할 Device 정의
        VkDeviceCreateInfo deviceCreateInfo{
                .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
                .queueCreateInfoCount = 1,                   // 큐의 개수
                .pQueueCreateInfos = &deviceQueueCreateInfo, // 생성할 큐의 정보
                .enabledExtensionCount = static_cast<uint32_t>(deviceExtensionNames.size()),
                .ppEnabledExtensionNames = deviceExtensionNames.data() // 활성화하려는 deviceExtension들을 넘겨줌
        };
    
        // vkCreateDevice를 호출하여 Device 생성(= mDevice 생성)
        VK_CHECK_ERROR(vkCreateDevice(mPhysicalDevice, &deviceCreateInfo, nullptr, &mDevice));
        // 생성된 Device(= mDevice)로부터 큐를 vkGetDeviceQueue를 호출하여 얻어온다.
        vkGetDeviceQueue(mDevice, mQueueFamilyIndex, 0, &mQueue);
    
    
        // ================================================================================
        // 5. VkSurface 생성
        // ================================================================================
        VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo{
                .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
                .window = window
        };
    
        // surface 생성.
        VK_CHECK_ERROR(vkCreateAndroidSurfaceKHR(mInstance, &surfaceCreateInfo, nullptr, &mSurface));
    
        VkBool32 supported; // surface 지원 여부
        VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice,
                                                            mQueueFamilyIndex,
                                                            mSurface,
                                                            &supported)); // 지원 여부를 받아옴.
        assert(supported);
    
    
        // ================================================================================
        // 6. VkSwapchain 생성
        // ================================================================================
        VkSurfaceCapabilitiesKHR surfaceCapabilities;
        VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mPhysicalDevice,
                                                                 mSurface,
                                                                 &surfaceCapabilities));
    
        VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_FLAG_BITS_MAX_ENUM_KHR;
        for (auto i = 0; i <= 4; ++i) {
            if (auto flag = 0x1u << i; surfaceCapabilities.supportedCompositeAlpha & flag) {
                compositeAlpha = static_cast<VkCompositeAlphaFlagBitsKHR>(flag);
                break;
            }
        }
        assert(compositeAlpha != VK_COMPOSITE_ALPHA_FLAG_BITS_MAX_ENUM_KHR);
    
        VkImageUsageFlags swapchainImageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
        assert(surfaceCapabilities.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
    
        uint32_t surfaceFormatCount = 0;
        VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceFormatsKHR(mPhysicalDevice,
                                                            mSurface,
                                                            &surfaceFormatCount,
                                                            nullptr));
    
        vector<VkSurfaceFormatKHR> surfaceFormats(surfaceFormatCount);
        VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceFormatsKHR(mPhysicalDevice,
                                                            mSurface,
                                                            &surfaceFormatCount,
                                                            surfaceFormats.data()));
    
        uint32_t surfaceFormatIndex = VK_FORMAT_MAX_ENUM;
        for (auto i = 0; i != surfaceFormatCount; ++i) {
            if (surfaceFormats[i].format == VK_FORMAT_R8G8B8A8_UNORM) {
                surfaceFormatIndex = i;
                break;
            }
        }
        assert(surfaceFormatIndex != VK_FORMAT_MAX_ENUM);
    
        uint32_t presentModeCount;
        VK_CHECK_ERROR(vkGetPhysicalDeviceSurfacePresentModesKHR(mPhysicalDevice,
                                                                 mSurface,
                                                                 &presentModeCount,
                                                                 nullptr));
    
        vector<VkPresentModeKHR> presentModes(presentModeCount);
        VK_CHECK_ERROR(vkGetPhysicalDeviceSurfacePresentModesKHR(mPhysicalDevice,
                                                                 mSurface,
                                                                 &presentModeCount,
                                                                 presentModes.data()));
    
        uint32_t presentModeIndex = VK_PRESENT_MODE_MAX_ENUM_KHR;
        for (auto i = 0; i != presentModeCount; ++i) {
            if (presentModes[i] == VK_PRESENT_MODE_FIFO_KHR) {
                presentModeIndex = i;
                break;
            }
        }
        assert(presentModeIndex != VK_PRESENT_MODE_MAX_ENUM_KHR);
    
        VkSwapchainCreateInfoKHR swapchainCreateInfo{
                .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
                .surface = mSurface,
                .minImageCount = surfaceCapabilities.minImageCount,
                .imageFormat = surfaceFormats[surfaceFormatIndex].format,
                .imageColorSpace = surfaceFormats[surfaceFormatIndex].colorSpace,
                .imageExtent = surfaceCapabilities.currentExtent,
                .imageArrayLayers = 1,
                .imageUsage = swapchainImageUsage,
                .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
                .preTransform = surfaceCapabilities.currentTransform,
                .compositeAlpha = compositeAlpha,
                .presentMode = presentModes[presentModeIndex]
        };
    
        VK_CHECK_ERROR(vkCreateSwapchainKHR(mDevice, &swapchainCreateInfo, nullptr, &mSwapchain));
    
        uint32_t swapchainImageCount;
        VK_CHECK_ERROR(vkGetSwapchainImagesKHR(mDevice, mSwapchain, &swapchainImageCount, nullptr));
    
        mSwapchainImages.resize(swapchainImageCount);
        VK_CHECK_ERROR(vkGetSwapchainImagesKHR(mDevice,
                                               mSwapchain,
                                               &swapchainImageCount,
                                               mSwapchainImages.data()));
    
    
        mSwapchainImageViews.resize(swapchainImageCount); // ImageView를 Swapchain의 개수만큼 생성
        for (auto i = 0; i != swapchainImageCount; ++i) {
            // ================================================================================
            // 7. VkImageView 생성
            // ================================================================================
            VkImageViewCreateInfo imageViewCreateInfo{ // 생성할 ImageView를 정의
                    .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
                    .image = mSwapchainImages[i],
                    .viewType = VK_IMAGE_VIEW_TYPE_2D,
                    .format = surfaceFormats[surfaceFormatIndex].format, // Swapchain 이미지 포맷과 동일한 포맷으로 설정
                    .components = {
                            .r = VK_COMPONENT_SWIZZLE_R,
                            .g = VK_COMPONENT_SWIZZLE_G,
                            .b = VK_COMPONENT_SWIZZLE_B,
                            .a = VK_COMPONENT_SWIZZLE_A,
                    },
                    .subresourceRange = { // 모든 이미지에 대해서 이 이미지 뷰가 접근할 수 있도록 설정
                            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                            .baseMipLevel = 0,
                            .levelCount = 1,
                            .baseArrayLayer = 0,
                            .layerCount = 1
                    }
            };
    
            VK_CHECK_ERROR(vkCreateImageView(mDevice,
                                             &imageViewCreateInfo,
                                             nullptr,
                                             &mSwapchainImageViews[i])); // mSwapchainImageViews[i] 생성
        }
    
        // ================================================================================
        // 8. VkCommandPool 생성
        // ================================================================================
        VkCommandPoolCreateInfo commandPoolCreateInfo{
                .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
                .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT |           // command buffer가 자주 변경될 것임을 알려줌
                         VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, // command buffer를 개별적으로 초기화 가능하게 설정
                .queueFamilyIndex = mQueueFamilyIndex
        };
    
        VK_CHECK_ERROR(vkCreateCommandPool(mDevice, &commandPoolCreateInfo, nullptr, &mCommandPool)); // mCommandPool 생성
    
        // ================================================================================
        // 9. VkCommandBuffer 할당
        // ================================================================================
        VkCommandBufferAllocateInfo commandBufferAllocateInfo{ // 할당하려는 command buffer 정의
                .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
                .commandPool = mCommandPool,
                .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
                .commandBufferCount = 1
        };
    
        VK_CHECK_ERROR(vkAllocateCommandBuffers(mDevice, &commandBufferAllocateInfo, &mCommandBuffer));
    
    
        // ================================================================================
        // 10. VkFence 생성
        // ================================================================================
        VkFenceCreateInfo fenceCreateInfo{
            .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
        }; // 생성할 Fence의 정보를 해당 구조체에서 정의
    
        VK_CHECK_ERROR(vkCreateFence(mDevice, &fenceCreateInfo, nullptr, &mFence)); // mFence 생성. flag에 아무것도 넣어주지 않았기 때문에 생성된 Fence의 초기 상태는 Unsignal 상태다.
    
    
        // ================================================================================
        // 11. VkSemaphore 생성
        // ================================================================================
        VkSemaphoreCreateInfo semaphoreCreateInfo{
                .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
        };
    
        VK_CHECK_ERROR(vkCreateSemaphore(mDevice, &semaphoreCreateInfo, nullptr, &mSemaphore));
    
    
        // ================================================================================
        // 12. VkRenderPass 생성
        // ================================================================================
        VkAttachmentDescription attachmentDescription{
                .format = surfaceFormats[surfaceFormatIndex].format,
                .samples = VK_SAMPLE_COUNT_1_BIT,
                .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
                .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
                .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
                .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
        };
    
        VkAttachmentReference attachmentReference{
                .attachment = 0,
                .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        };
    
        VkSubpassDescription subpassDescription{
                .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
                .colorAttachmentCount = 1,
                .pColorAttachments = &attachmentReference
        };
    
        VkRenderPassCreateInfo renderPassCreateInfo{
                .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
                .attachmentCount = 1,
                .pAttachments = &attachmentDescription,
                .subpassCount = 1,
                .pSubpasses = &subpassDescription
        };
    
        VK_CHECK_ERROR(vkCreateRenderPass(mDevice, &renderPassCreateInfo, nullptr, &mRenderPass)); // mRenderPass 생성.
    
        mFramebuffers.resize(swapchainImageCount);
        for (auto i = 0; i != swapchainImageCount; ++i) {
            // ================================================================================
            // 13. VkFramebuffer 생성
            // ================================================================================
            VkFramebufferCreateInfo framebufferCreateInfo{
                    .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
                    .renderPass = mRenderPass,
                    .attachmentCount = 1,
                    .pAttachments = &mSwapchainImageViews[i], // ImageView
                    .width = mSwapchainImageExtent.width,
                    .height = mSwapchainImageExtent.height,
                    .layers = 1
            };
    
            VK_CHECK_ERROR(vkCreateFramebuffer(mDevice, &framebufferCreateInfo, nullptr, &mFramebuffers[i]));// mFramebuffers[i] 생성
        }
    
    
        // ================================================================================
        // 14. Vertex VkShaderModule 생성
        // ================================================================================
        string_view vertexShaderCode = {
                "#version 310 es                                        \n"
                "                                                       \n"
                "void main() {                                          \n"
                "    vec2 pos[3] = vec2[3](vec2(-0.5,  0.5),            \n"
                "                          vec2( 0.5,  0.5),            \n"
                "                          vec2( 0.0, -0.5));           \n"
                "                                                       \n"
                "    gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); \n"
                "}                                                      \n"
        };
    
        std::vector<uint32_t> vertexShaderBinary;
        // VKSL을 SPIR-V로 변환.
        VK_CHECK_ERROR(vkCompileShader(vertexShaderCode,
                                       VK_SHADER_TYPE_VERTEX,
                                       &vertexShaderBinary));
    
        VkShaderModuleCreateInfo vertexShaderModuleCreateInfo{
                .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
                .codeSize = vertexShaderBinary.size() * sizeof(uint32_t), // 바이트 단위.
                .pCode = vertexShaderBinary.data()
        };
    
        VK_CHECK_ERROR(vkCreateShaderModule(mDevice,
                                            &vertexShaderModuleCreateInfo,
                                            nullptr,
                                            &mVertexShaderModule)); // mVertexShaderModule 생성.
    
        // ================================================================================
        // 15. Fragment VkShaderModule 생성
        // ================================================================================
        string_view fragmentShaderCode = {
                "#version 310 es                                        \n"
                "precision mediump float;                               \n"
                "                                                       \n"
                "layout(location = 0) out vec4 fragmentColor;           \n"
                "                                                       \n"
                "void main() {                                          \n"
                "    fragmentColor = vec4(1.0, 0.0, 0.0, 1.0);          \n"
                "}                                                      \n"
        };
    
        std::vector<uint32_t> fragmentShaderBinary;
        VK_CHECK_ERROR(vkCompileShader(fragmentShaderCode,
                                       VK_SHADER_TYPE_FRAGMENT,
                                       &fragmentShaderBinary));
    
        VkShaderModuleCreateInfo fragmentShaderModuleCreateInfo{
                .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
                .codeSize = fragmentShaderBinary.size() * sizeof(uint32_t),
                .pCode = fragmentShaderBinary.data()
        };
    
        VK_CHECK_ERROR(vkCreateShaderModule(mDevice,
                                            &fragmentShaderModuleCreateInfo,
                                            nullptr,
                                            &mFragmentShaderModule));
    
        // ================================================================================
        // 16. VkPipelineLayout 생성
        // ================================================================================
        VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO
        };
    
        VK_CHECK_ERROR(vkCreatePipelineLayout(mDevice,
                                              &pipelineLayoutCreateInfo,
                                              nullptr,
                                              &mPipelineLayout));
    
        // ================================================================================
        // 17. Graphics VkPipeline 생성
        // ================================================================================
        array<VkPipelineShaderStageCreateInfo, 2> pipelineShaderStageCreateInfos{
                VkPipelineShaderStageCreateInfo{
                        .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                        .stage = VK_SHADER_STAGE_VERTEX_BIT,
                        .module = mVertexShaderModule,
                        .pName = "main"
                },
                VkPipelineShaderStageCreateInfo{
                        .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                        .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
                        .module = mFragmentShaderModule,
                        .pName = "main"
                }
        };
    
        VkPipelineVertexInputStateCreateInfo pipelineVertexInputStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
        };
    
        VkPipelineInputAssemblyStateCreateInfo pipelineInputAssemblyStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
                .topology =VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
        };
    
        VkViewport viewport{
                .width = static_cast<float>(mSwapchainImageExtent.width),
                .height = static_cast<float>(mSwapchainImageExtent.height),
                .maxDepth = 1.0f
        };
    
        VkRect2D scissor{
                .extent = mSwapchainImageExtent
        };
    
        VkPipelineViewportStateCreateInfo pipelineViewportStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
                .viewportCount = 1,
                .pViewports = &viewport,
                .scissorCount = 1,
                .pScissors = &scissor
        };
    
        VkPipelineRasterizationStateCreateInfo pipelineRasterizationStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
                .polygonMode = VK_POLYGON_MODE_FILL,
                .cullMode = VK_CULL_MODE_NONE,
                .lineWidth = 1.0f
        };
    
        VkPipelineMultisampleStateCreateInfo pipelineMultisampleStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
                .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT
        };
    
        VkPipelineDepthStencilStateCreateInfo pipelineDepthStencilStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO
        };
    
        VkPipelineColorBlendAttachmentState pipelineColorBlendAttachmentState{
                .colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
                                  VK_COLOR_COMPONENT_G_BIT |
                                  VK_COLOR_COMPONENT_B_BIT |
                                  VK_COLOR_COMPONENT_A_BIT
        };
    
        VkPipelineColorBlendStateCreateInfo pipelineColorBlendStateCreateInfo{
                .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
                .attachmentCount = 1,
                .pAttachments = &pipelineColorBlendAttachmentState
        };
    
        VkGraphicsPipelineCreateInfo graphicsPipelineCreateInfo{
                .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
                .stageCount = pipelineShaderStageCreateInfos.size(),
                .pStages = pipelineShaderStageCreateInfos.data(),
                .pVertexInputState = &pipelineVertexInputStateCreateInfo,
                .pInputAssemblyState = &pipelineInputAssemblyStateCreateInfo,
                .pViewportState = &pipelineViewportStateCreateInfo,
                .pRasterizationState = &pipelineRasterizationStateCreateInfo,
                .pMultisampleState = &pipelineMultisampleStateCreateInfo,
                .pDepthStencilState = &pipelineDepthStencilStateCreateInfo,
                .pColorBlendState = &pipelineColorBlendStateCreateInfo,
                .layout = mPipelineLayout,
                .renderPass = mRenderPass
        };
    
        VK_CHECK_ERROR(vkCreateGraphicsPipelines(mDevice,
                                                 VK_NULL_HANDLE,
                                                 1,
                                                 &graphicsPipelineCreateInfo,
                                                 nullptr,
                                                 &mPipeline));
    
        // ================================================================================
        // 18. Vertex VkBuffer 생성
        // ================================================================================
        constexpr array<Vertex, 3> vertices{
                Vertex{
                        .position{0.0, -0.5, 0.0},
                        .color{1.0, 0.0, 0.0}
                },
                Vertex{
                        .position{0.5, 0.5, 0.0},
                        .color{0.0, 1.0, 0.0}
                },
                Vertex{
                        .position{-0.5, 0.5, 0.0},
                        .color{0.0, 0.0, 1.0}
                },
        };
        constexpr VkDeviceSize verticesSize{vertices.size() * sizeof(Vertex)};
    
        VkBufferCreateInfo bufferCreateInfo{
                .sType =VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
                .size = verticesSize,
                .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
        };
    
        VK_CHECK_ERROR(vkCreateBuffer(mDevice, &bufferCreateInfo, nullptr, &mVertexBuffer));
    
        // ================================================================================
        // 19. Vertex VkBuffer의 VkMemoryRequirements 얻기
        // ================================================================================
        VkMemoryRequirements vertexMemoryRequirements;
        vkGetBufferMemoryRequirements(mDevice, mVertexBuffer, &vertexMemoryRequirements);
    
        // ================================================================================
        // 20. Vertex VkDeviceMemory를 할당 할 수 있는 메모리 타입 인덱스 얻기
        // ================================================================================
        uint32_t vertexMemoryTypeIndex;
        VK_CHECK_ERROR(vkGetMemoryTypeIndex(mPhysicalDeviceMemoryProperties,
                                            vertexMemoryRequirements,
                                            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                                            VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                                            &vertexMemoryTypeIndex));
    
        // ================================================================================
        // 21. Vertex VkDeviceMemory 할당
        // ================================================================================
        VkMemoryAllocateInfo vertexMemoryAllocateInfo{
                .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
                .allocationSize = vertexMemoryRequirements.size,
                .memoryTypeIndex = vertexMemoryTypeIndex
        };
    
        VK_CHECK_ERROR(vkAllocateMemory(mDevice, &vertexMemoryAllocateInfo, nullptr, &mVertexMemory));
    }
    
    VkRenderer::~VkRenderer() {
        vkFreeMemory(mDevice, mVertexMemory, nullptr);
        vkDestroyBuffer(mDevice, mVertexBuffer, nullptr);
        vkDestroyPipelineLayout(mDevice, mPipelineLayout, nullptr);
        vkDestroyPipeline(mDevice, mPipeline, nullptr);
        vkDestroyShaderModule(mDevice, mVertexShaderModule, nullptr);
        vkDestroyShaderModule(mDevice, mFragmentShaderModule, nullptr);
        for (auto framebuffer : mFramebuffers) {
            vkDestroyFramebuffer(mDevice, framebuffer, nullptr);
        }
        mFramebuffers.clear();
        vkDestroyRenderPass(mDevice, mRenderPass, nullptr);
        for (auto imageView : mSwapchainImageViews) {
            vkDestroyImageView(mDevice, imageView, nullptr);
        }
        mSwapchainImageViews.clear();
        vkDestroySemaphore(mDevice, mSemaphore, nullptr);
        vkDestroyFence(mDevice, mFence, nullptr);
        vkFreeCommandBuffers(mDevice, mCommandPool, 1, &mCommandBuffer);
        vkDestroyCommandPool(mDevice, mCommandPool, nullptr);
        vkDestroySwapchainKHR(mDevice, mSwapchain, nullptr);
        vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
        vkDestroyDevice(mDevice, nullptr); // Device 파괴. queue의 경우 Device를 생성하면서 생겼기 때문에 따로 파괴하는 API가 존재하지 않는다.
        vkDestroyInstance(mInstance, nullptr);
    }
    
    void VkRenderer::render() {
        // ================================================================================
        // 1. 화면에 출력할 수 있는 VkImage 얻기
        // ================================================================================
        uint32_t swapchainImageIndex;
        VK_CHECK_ERROR(vkAcquireNextImageKHR(mDevice,
                                             mSwapchain,
                                             UINT64_MAX,
                                             VK_NULL_HANDLE,
                                             mFence,                 // Fence 설정
                                             &swapchainImageIndex)); // 사용 가능한 이미지 변수에 담기
        //auto swapchainImage = mSwapchainImages[swapchainImageIndex]; // swapchainImage에 더 이상 직접 접근하지 않으므로 이제 사용X
        auto framebuffer = mFramebuffers[swapchainImageIndex];
    
        // ================================================================================
        // 2. VkFence 기다린 후 초기화
        // ================================================================================
        // mFence가 Signal 될 때까지 기다린다.
        VK_CHECK_ERROR(vkWaitForFences(mDevice, 1, &mFence, VK_TRUE, UINT64_MAX));
        // mFence가 Siganl이 되면 vkResetFences를 호출해서 Fence의 상태를 다시 초기화한다.
        // 초기화하는 이유: vkAcquireNextImageKHR을 호출할 때 이 Fence의 상태는 항상 Unsignal 상태여야 하기 때문이다.
        VK_CHECK_ERROR(vkResetFences(mDevice, 1, &mFence));
    
        // ================================================================================
        // 3. VkCommandBuffer 초기화
        // ================================================================================
        vkResetCommandBuffer(mCommandBuffer, 0);
    
        // ================================================================================
        // 4. VkCommandBuffer 기록 시작
        // ================================================================================
        VkCommandBufferBeginInfo commandBufferBeginInfo{
                .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
                .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT // 한 번만 기록되고 다시 리셋 될 것이라는 의미
        };
    
        // mCommandBuffer를 기록중인 상태로 변경.
        VK_CHECK_ERROR(vkBeginCommandBuffer(mCommandBuffer, &commandBufferBeginInfo));
    
    
        // ================================================================================
        // 5. VkRenderPass 시작
        // ================================================================================
        VkRenderPassBeginInfo renderPassBeginInfo{
                .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
                .renderPass = mRenderPass,
                .framebuffer = framebuffer,
                .renderArea{
                        .extent = mSwapchainImageExtent
                },
                .clearValueCount = 1,
                .pClearValues = &mClearValue
        };
    
        vkCmdBeginRenderPass(mCommandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
    
        // ================================================================================
        // 6. Graphics VkPipeline 바인드
        // ================================================================================
        vkCmdBindPipeline(mCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, mPipeline);
    
        // ================================================================================
        // 7. 삼각형 그리기
        // ================================================================================
        vkCmdDraw(mCommandBuffer, 3, 1, 0, 0);
    
        // ================================================================================
        // 8. VkRenderPass 종료
        // ================================================================================
        vkCmdEndRenderPass(mCommandBuffer);
    
        // ================================================================================
        // 9. Clear 색상 갱신
        // ================================================================================
        for (auto i = 0; i != 4; ++i) {
            mClearValue.color.float32[i] = fmodf(mClearValue.color.float32[i] + 0.01, 1.0);
        }
    
        // ================================================================================
        // 10. VkCommandBuffer 기록 종료
        // ================================================================================
        VK_CHECK_ERROR(vkEndCommandBuffer(mCommandBuffer)); // mCommandBuffer는 Executable 상태가 된다.
    
        // ================================================================================
        // 11. VkCommandBuffer 제출
        // ================================================================================
        VkSubmitInfo submitInfo{
                .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
                .commandBufferCount = 1,
                .pCommandBuffers = &mCommandBuffer,
                .signalSemaphoreCount = 1,
                .pSignalSemaphores = &mSemaphore
        };
    
        // submitInfo 구조체를 넘김으로써 commandBuffer 정보를 queue에 제출
        VK_CHECK_ERROR(vkQueueSubmit(mQueue, 1, &submitInfo, VK_NULL_HANDLE));
    
        // ================================================================================
        // 12. VkImage 화면에 출력
        // ================================================================================
        VkPresentInfoKHR presentInfo{
                .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
                .waitSemaphoreCount = 1,
                .pWaitSemaphores = &mSemaphore,
                .swapchainCount = 1,
                .pSwapchains = &mSwapchain,
                .pImageIndices = &swapchainImageIndex
        };
    
        VK_CHECK_ERROR(vkQueuePresentKHR(mQueue, &presentInfo)); // 화면에 출력.
        VK_CHECK_ERROR(vkQueueWaitIdle(mQueue));
    }