[vk] Drawing triangle - Presentation - Window surface

2021. 8. 6. 13:18그래픽스/vk

1. Window surface

- Vulkan은 platform agnostic API 이다.

- 직접적으로 윈도우 시스템과 인터페이스할 수 없다.

- 연결설정한 후, 결과를 화면에 표시하려면

- WSI(Window System Integration) extensions을 사용하는것이 필요하다.

 

VK_KHR_surface. 

- VkSurfaceKHR 객체를 expose함.

- 렌더링할 이미지를 surface에 나타나게해주는 추상타입.

- 프로그램의 surface 는 glfw로 이미 생성한 window에 띄워질것임.

- 이 확장은 instance lavel extension이다.

- 따라서 이미 활성화되어있다.

- glfwGetRequiredInstanceExtensions 에 포함되있기 때문

- 이 리스트는 또한 다른 WSI extensions을 포함하는데, 다음챕터에서 다룸.

 

Window surface

- instance 를 생성한 후 바로 만들어야한다.

- 왜냐하면, 확실히 physical device selection에 영향을 주기 때문.

- 이것은 render targets에 대한 부분이므로, 더 큰 주제임.

-  선택적인 구성이므로 당연히 off-screen rendering을 할 수 있다.

- hacks 없이 invisible window를 생성할 수 도 있다. (OpenGL은 hacks)

 

 

 

 

 

2. Window surface creation

- surface 변수 를 생성해야한다.

VkSurfaceKHR surface;

- 비록 VkSurfaceKHR 객체 와 그것의 사용이 platform에 agnostic하지만

- 이것의 생성이 agnostic하다는것이 아니다.

- 왜냐하면 window system details 에 의존적이기때문.

 

- windows는 HWND, HMODULE handles 를 다루는것이 필요하다.

- 그러므로  Windows에서는 VK_KHR_win32_surface 확장자가 필요하다.

- (플랫폼마다 추가해야할 확장자가 달라짐 platform-specific addition to the extension) 

- 이런것들은 자동적으로 glfwGetRequiredInstanceExtensions 에 포함됨.

 

- 이러한 플랫폼별 확장을 사용하여 surface를 생성하는것을 여기선 보여주지만

- 튜토리얼 전체에선 사용하지 않음

- GLFW 와 같은 라이브러리를 사용하면 플랫폼별 코드를 사용하는것은 의미없음.

- 실제로 glfwCreateWindowSurface 함수를 통해 플랫폼 차이를 처리할 수 있음

 

 

 

 

2.1 VkWin32SurfaceCreateInfoKHR

- native 하게 플랫폼함수들에 접근하려면 다음과 같이 헤더 부분을 추가해야함

#define VK_USE_PLATFORM_WIN32_KHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

- window surface는 Vulkan object 이므로, VkWin32SurfaceCreateInfoKHR라는

- createInfo 구조체를 작성해야함

- 이 구조체에선 hwnd and hinstance 파라미터가 중요하다.

- 이것들은 window와 process에 대한 핸들임.

VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);

- glfwGetWin32Window 함수는 raw HWND를 GLFWwindow 객체에서 가져온다.

- GetModuleHandle함수는 현재 process 의 handle인 HINSTANCE 를 반환해줌

 

 

 

 

2.2 vkCreateWin32SurfaceKHR

- vkCreateWin32SurfaceKHR 함수 호출을 통해,

- 파라미터안에 instance 와 creation info 와 , costom allocator

- 그리고 초기화할 핸들인 surface를 넣어줘야한다.

- 엄밀히 말하면 기술적으로 이것은 WSI extension 함수이다.

- 그러나 너무 일반적으로 사용되므로, 표준 Vulkan loader에 포함됨.

- 다른 extension과 다르게 주소를 사용한 명시적인 로드는 불필요

if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
    throw std::runtime_error("failed to create window surface!");
}

- 이러한 프로세스는 windows 뿐만이 아니라 Linux 같은 다른 플랫폼들 또한 유사

- 리눅스인 경우 vkCreateXcbSurfaceKHR로 XCB connection  과  X11의 생성 세부정보로 생성

glfwCreateWindowSurface함수를 사용하면, 플랫폼마다 다른 이러한 확장 기능 작업을 정확히 수행함

- 이제 프로그램에 통합할 것임.

 

 

 

2.3 glfwCreateWindowSurface

- 인스턴스 생성과 디버그 메신저를 생성한 직후 createSurface함수를 추가하여 실제 구현해보자. 

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
}

void createSurface() {

}

- GLFW 호출은 구조체를 파라미터 삼지 않고, 다음과 같이 간단하다.

void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}

- 파라미터 1 : VkInstance

- 파라미터 2 : GLFWwindow 포인터

- 파라미터 3 : costom allocators

- 파라미터 4 : 생성할 surface 핸들 변수

-> 생성이 제대로 됬는지 확인하는 절차도 잊지 말아야한다!

-  GLFW은 이 객체를 Destroying 할 특별한 함수를 제공하지 않음

- 그러나 다음과 같이 original API를 통해 가능

void cleanup() {
        ...
        vkDestroySurfaceKHR(instance, surface, nullptr);
        vkDestroyInstance(instance, nullptr);
        ...
    }

- 확실히 instance 를 제거하기전에 surface를 제거해야함.

 

 

 

 

 

3. Querying for presentation support - 지원하는 큐 찾기

- 비록 Vulkan에서 WSI(window system integration) 을 지원하도록 구현을 할 수 있지만

- 모든 device가 이 시스템을 지원한다는것은 아님

- 따라서 적합한지 검사해야함

- device가 이미지를 표시할 수 있게 보장하도록 isDeviceSuitable함수를 만들어야한다.

- 이러한 presentation 은 queue-specific feature 임.

- 때문에, 문제는 만든 surface를 지원하는 queue family를 찾아야한다는것.

 

- drawing commands를 지원하는 queue families이 presentation을 지원안할 수 있음

- 따라서 이전에 작성했던 QueueFamilyIndices구조체를 수정하여, 따로 family를 찾을 수 있게해야함

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};

- 다음으로 findQueueFamilies 또한 수정해야함

- presenting 기능을 가지는 queue family를 찾도록해야함.

- vkGetPhysicalDeviceSurfaceSupportKHR 함수를 통해 확인

- 이 함수는 물리적 장치, queue family 인덱스, surface,를 매개변수로 받음

- VK_QUEUE_GRAPHICS_BIT를 찾는 루프에 추가하면됨.

VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

- 인덱스를 저장해주는것도 잊지 말아야함.

if (presentSupport) {
    indices.presentFamily = i;
}

- 결국 동일한 queue가 선택될 가능성이 높지만

- 프로그램 전체에서 균일한 접근방식을 위해, 별도의 queue인것처럼 처리해야함

- 그럼에도 불구하고 성능향상을 위해, 똑같은 queue인것을 명시할 수 있긴하다.

 

    QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
        QueueFamilyIndices indices;

        uint32_t queueFamilyCount = 0;
        vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);

        std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
        vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());

        int i = 0;
        for (const auto& queueFamily : queueFamilies) {
            if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
                indices.graphicsFamily = i;
            }

            VkBool32 presentSupport = false;
            vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

            if (presentSupport) {
                indices.presentFamily = i;
            }

            if (indices.isComplete()) {
                break;
            }

            i++;
        }

        return indices;
    }

 

 

 

4. Creating the presentation queue

- 이제 logical device 생성 절차를 수정하여 presentation queue인 VkQueue를 생성해야한다.

- 이 핸들에 대한 멤버 변수를 다음과 같이 추가해야함.

VkQueue presentQueue;

- 두 family에서 큐를 생성하려면 다수의 VkDeviceQueueCreateInfo 구조체를 생성해야한다

- 하지만 이런걸 피할 수 있는 우아한 방법은

- 필요한 queue에 필요한 모든 고유한 queue families 의 set을 만드는것이다.

(An elegant way to do that is to create a set of all unique queue families that are necessary for the required queues)

#include <set>

...

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
}

- 그리고 VkDeviceCreateInfo를 수정하여 vector를 가리키게해야함.

createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

- 만일 queue families가 같다면, 해당 인덱스를 한번만 전달해주면된다

- 마지막으로 queue handle을 검색하는 호출을 추가해주면된다,

vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);

-  queue families가 동일한 경우, 두개의 핸들이 이제 동일한 값을 가질 가능성이 높아졌다.

    void createLogicalDevice() {
        QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

        std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
        std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

        float queuePriority = 1.0f;
        for (uint32_t queueFamily : uniqueQueueFamilies) {
            VkDeviceQueueCreateInfo queueCreateInfo{};
            queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
            queueCreateInfo.queueFamilyIndex = queueFamily;
            queueCreateInfo.queueCount = 1;
            queueCreateInfo.pQueuePriorities = &queuePriority;
            queueCreateInfos.push_back(queueCreateInfo);
        }

        VkPhysicalDeviceFeatures deviceFeatures{};

        VkDeviceCreateInfo createInfo{};
        createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

        createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
        createInfo.pQueueCreateInfos = queueCreateInfos.data();

        createInfo.pEnabledFeatures = &deviceFeatures;

        createInfo.enabledExtensionCount = 0;

        if (enableValidationLayers) {
            createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
            createInfo.ppEnabledLayerNames = validationLayers.data();
        } else {
            createInfo.enabledLayerCount = 0;
        }

        if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
            throw std::runtime_error("failed to create logical device!");
        }

        vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
        vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
    }

 

 

 

- 다음 장에선, swapchains에대해 살펴보며,

- 어떻게 이들이 이미지를 표시하는 기능을 제공하는지 살펴봄.

 

 

 

C++ code

 

https://vulkan-tutorial.com/Drawing_a_triangle/Presentation/Window_surface