⭐ Vulkan & CMake/Vulkan

[Vulkan] Vulkan Surface

Designerd 2024. 8. 4. 12:58

Vulkan은 플랫폼에 구애받지 않는 API이기 때문에 자체적으로 윈도우 시스템과 직접 인터페이스할 수 없다. 화면에 결과를 제시하기 위해 Vulkan과 VK_KHR_surface 확장을 사용해야 한다. 

 

목차

     

     



    인프런 삼각형님의 '삼각형의 실전! Vulkan 중급' 강의를 참고하였습니다. 

    😎 [삼각형의 실전! Vulkan 중급] 강의 들으러 가기!

     

     

     

    Vulkan Surface


     

     

    Vulkan Surface란?

     

    Vulkan Surface는 Window를 나타내는 객체다.

    Vulkan Surface를 통해 Window와 Window에 연결된 Display에 대한 정보를 얻을 수 있다.

     


     

     

    Android에서 Vulkan Surface 생성하기 위한 준비

     

    Vulkan Surface를 생성하기 위해서는 Window System에 맞는 API사용해야 한다.

    Android는 VK_USE_PLATFORM_ANDROID_KHR 디파인을 정의해야 한다.

     

    # 첫 번째 사용방법:

    CMakeList.txt에서 target_compile_definitions을 사용해서  VK_USE_PLATFORM_ANDROID_KHR 을 정의.

    이를 통해, Surface 생성에 필요한 API를 사용할 수 있다.

    target_compile_definitions(practicevulkan PRIVATE
         VK_USE_PLATFORM_ANDROID_KHR)

     

    # 두 번째 사용방법:

    vulkan 헤더를 인크루드하기 전에  VK_USE_PLATFORM_ANDROID_KHR 디파인 함.

    #define VK_USE_PLATFORM_ANDROID_KHR
    #include <vulkan/vulkan.h>

     

     

    # 세 번째 사용방법:

    VK_USE_PLATFORM_ANDROID_KHR 을 정의하지 않고 dynamic 로딩하는 방법.


     

     

    Vulkan Surface 생성

     

     

     

    Surface를 생성하기 위해서는 ANativeWindow가 필요함.

    vkCreateAndroidSurfaceKHR를 호출하여 VkSurfaceKHR를 생성한다.


     

     

    VkAndroidSurfaceCreateInfoKHR 구조체

     

    typedef struct VkAndroidSurfaceCreateInfoKHR {
         VkStructureType sType;
         const void* pNext;
         VkAndroidSurfaceCreateFlagsKHR flags;
         struct ANativeWindow* window;
    } VkAndroidSurfaceCreateInfoKHR;

     

    매개 변수  설명 
    sType   구조체의 타입
    pNext   NULL 또는 확장 기능 구조체의 포인터
    flags  
    window   Surface와 연결하려는 ANativeWindow

     


     

     

    Vulkan Surface 생성

     

    VkResult vkCreateAndroidSurfaceKHR(
         VkInstance instance,
         const VkAndroidSurfaceCreateInfoKHR* pCreateInfo,
         const VkAllocationCallbacks* pAllocator,
         VkSurfaceKHR* pSurface);

     

    매개 변수  설명 
    instance   VkInstance
    pCreateInfo   VkAndroidSurfaceCreateInfo 변수의 포인터
    pAllocator  
    pSurface   VkSurfaceKHR 변수의 포인터

     

    mSurface 생성


     

     

    Vulkan Physical Device가 Vulkan Surface를 지원하는지 확인

     

    모든 GPU가 Surface 출력을 지원하지 않는다. 그래서 Surface를 생성 후, physical device의 queue-family가 해당 surface를 출력할 수 있는지 확인해야 한다.

    • 디스플레이 포트가 없는 GPU도 있다. 이런 GPU들은 Surface를 출력할 수 없다.
    • vkGetPhysicalDeviceSurfaceSupportKHR 함수를 호출하여 GPU가 Surface를 호출할 수 있는지 여부를 확인할 수 있다.

     

    VkResult vkGetPhysicalDeviceSurfaceSupportKHR(
         VkPhysicalDevice physicalDevice,
         uint32_t queueFamilyIndex,
         VkSurfaceKHR surface,
         VkBool32* pSupported);

     

    매개 변수  설명 
    physicalDevice   VkPhysicalDevice
    queueFamilyIndex   Queue family index
    surface    VkSurfaceKHR
    pSupported   지원 여부를 얻어오는 변수의 포인터.
      VK_TRUE이면 지원 / VK_FALSE는 지원하지 않

     

    지원 여부에 대한 결과는 &supported에 담김.

     


     

     

    Vulkan Surface 파괴

     

    void vkDestroySurfaceKHR(
         VkInstance instance,
         VkSurfaceKHR surface,
         const VkAllocationCallbacks* pAllocator);

     

    매개 변수  설명 
    device    VkDevice
    surface   VkSurface
    pAllocator   

     


     

     

    코드

     

    CMakeList.txt

    cmake_minimum_required(VERSION 3.22.1)
    
    project("practicevulkan")
    
    find_package(game-activity REQUIRED CONFIG)
    find_package(Vulkan REQUIRED)
    
    add_library(practicevulkan SHARED
            VkRenderer.h
            VkRenderer.cpp
            VkUtil.h
            main.cpp
            AndroidOut.cpp)
    
    target_compile_definitions(practicevulkan PRIVATE
            VK_USE_PLATFORM_ANDROID_KHR)
    
    target_link_libraries(practicevulkan
            game-activity::game-activity
            android
            log
            Vulkan::Vulkan)
    • target_compile_definitions 설정함.

     

     

    VkRender.cpp

    #include ..
    using namespace std;
    
    VkRenderer::VkRenderer() {
        // 1. VkInstance 생성
        // 2. VkPhysicalDevice 선택
        // 3. VkDevice 생성
    
        // 4. 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);
    }
    
    VkRenderer::~VkRenderer() {
        vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
        vkDestroyDevice(mDevice, nullptr); // Device 파괴. queue의 경우 Device를 생성하면서 생겼기 때문에 따로 파괴하는 API가 존재하지 않는다.
        vkDestroyInstance(mInstance, nullptr);
    }

     

    전체 코드

    더보기
    // 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 "VkRenderer.h"
    #include "VkUtil.h"
    #include "AndroidOut.h"
    
    using namespace std;
    
    VkRenderer::VkRenderer() {
        // ================================================================================
        // 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. 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);
    
    
        // ================================================================================
        // 4. 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);
    }
    
    VkRenderer::~VkRenderer() {
        vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
        vkDestroyDevice(mDevice, nullptr); // Device 파괴. queue의 경우 Device를 생성하면서 생겼기 때문에 따로 파괴하는 API가 존재하지 않는다.
        vkDestroyInstance(mInstance, nullptr);
    }