[vk] Drawing triangle - Drawing - Swap chain recreation

2021. 8. 11. 15:04그래픽스/vk

Swap chain recreation

1. Introduction

- 삼각형을 성공적으로 그렸지만

- 아직 제대로 처리되지 않은 몇가지 상황이 있다.

- 1) window surface 가 변경되어, 더이상 swapchain과 surface가 호환이 안될 수 있음.

- 2) window size가 변경될 경우 등등

- 이러한 이벤트들을 잡아내고, swapchain을 다시 만들어야한다.

 

 

 

 

2. Recreating the swap chain

- recreateSwapChain 이 함수는  createSwapChain 를 포함한

- swap chain 과 window size 가 관련된 객체를 생성하는 함수를 전부 호출한다.

void recreateSwapChain() {
    vkDeviceWaitIdle(device);

    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();
}

- vkDeviceWaitIdle 호출을 통해 여전히 뒤에서 돌아가는 리소스가 종료될때까지 대기한다.

- 분명히, 먼저 해야할일은 swapchain그 자체를 다시 생성하는것이다.

- 그런다음 swapchain images을 기반으로 하는 imageViews 를 재생성할 필요가 있다.

- renderpass 는 swapchain images의 format에 의존하기 때문에,  재생성할 필요가 있다.

- - - - swapchain image의 format을 변경하는것은 드물긴하지만(window resize경우)

- - - - 그래도 의존되어있으니 해주는게 좋다.

- Viewport, scissor rectangle sizegraphicsPipeline 생성 중에 지정되므로, 파이프라인 또한 다시 빌드해야함.

- - - - viewport와 sissor rectangles에 dynamic state를 사용함으로써 다시빌드하는것을 피할 수 있긴하다.

- 마지막으로 framebuffers 그리고 commandBuffers 또한 직접적으로 의존하므로 다시생성해야함.

 

- 이러한 객체들을 다시 만들기 전에 구버전을 정리할 함수를 먼저 호출해야한다.

- 따라서  recreateSwapChain  앞부분에 cleanupSwapChain 을 호출하여 정리해주자. 

void cleanupSwapChain() {

}

void recreateSwapChain() {
    vkDeviceWaitIdle(device);

    cleanupSwapChain();

    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();
}

 - 모든 관련된 cleanup 코드를 옮겨주고 cleanup에서 또한 이 함수를 호출하도록하자

void cleanupSwapChain() {
    for (size_t i = 0; i < swapChainFramebuffers.size(); i++) {
        vkDestroyFramebuffer(device, swapChainFramebuffers[i], nullptr);
    }

    vkFreeCommandBuffers(device, commandPool, static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data());

    vkDestroyPipeline(device, graphicsPipeline, nullptr);
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    vkDestroyRenderPass(device, renderPass, nullptr);

    for (size_t i = 0; i < swapChainImageViews.size(); i++) {
        vkDestroyImageView(device, swapChainImageViews[i], nullptr);
    }

    vkDestroySwapchainKHR(device, swapChain, nullptr);
}

void cleanup() {
    cleanupSwapChain();

    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
        vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
        vkDestroyFence(device, inFlightFences[i], nullptr);
    }

    vkDestroyCommandPool(device, commandPool, nullptr);

    vkDestroyDevice(device, nullptr);

    if (enableValidationLayers) {
        DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
    }

    vkDestroySurfaceKHR(instance, surface, nullptr);
    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);

    glfwTerminate();
}

- command pool을 처음부터 다시 만들 수 있지만 이는 낭비이다.

- 대신 vkFreeCommandBuffers 함수를 사용하여 기존의 command buffer를 정리하도록 함.

- 이러한 방법은 기존 pool을 재사용하여 새 command buffers를 할당할 수 있다.

 

- chooseSwapExtent (createSwapchain 함수에서 호출하는) 이 함수를 통해

- 이미 새로운 윈도우의 해상도를 얻어, swapchain image가 적절한 사이즈가 됨.

 (glfwGetFramebufferSize  를 사용하여 픽셀에서의 surface의 해상도를얻어 swapchain을 만들었다.)

 

- 이것이 스왑체인을 다시 만드는데 필요한 전부이다.

- 하지만 이러한 방법은 새 스왑체인을 만들기 전에 모든 렌더링을 중지해야한다.

- 이전 스왑체인의 이미지에 대한 명령을 그리는 동안 여전히 새로운 스왑체인을 만들 수 있는 방법이 있다.

- 이전 스왑체인을 VkSwapchainCreateInfoKHR 구조체의  oldSwapChain 필드에 전달하고, 

- 사용을 마친 즉시 이전 스왑체인을 삭제하면된다.

  (여기에선 사용하지 않음) 

 

 

 

 

3. Suboptimal or out-of-date swap chain

이제 언제 swap chain recreation이 발생하는지 알아내야한다.

-  그리고 그 때 recreateSwapChain 함수를 호출해야한다.

- 운이좋게도 Vulkan은 일반적으로 프레젠테이션 중에 swapchain이 더 이상 적절하지 않다고 알려줌.

- vkAcquireNextImageKHR 함수 와 vkQueuePresentKHR  함수는 다음과 같은 특수값을 반환한다.

  • VK_ERROR_OUT_OF_DATE_KHR: The swap chain has become incompatible with the surface and can no longer be used for rendering. Usually happens after a window resize.
  • VK_SUBOPTIMAL_KHR: The swap chain can still be used to successfully present to the surface, but the surface properties are no longer matched exactly.
VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

if (result == VK_ERROR_OUT_OF_DATE_KHR) {
    recreateSwapChain();
    return;
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
    throw std::runtime_error("failed to acquire swap chain image!");
}

- 만일 swapchain이 이미지를 가져오는 타이밍에 오래된것으로 밝혀지면, 더이상 이미지를 표시할 수 없다

- 그러므로 즉시 swapchain을 재생성하고 다음 drawFrame 호출을 시도해야한다.

 

- 만일 swapchain이 suboptimal인 상태에도 재생성하도록 결정할 수 있지만

- 이미 이미지를 획득했기 때문에 

- 이 경우에도 계속 진행하기로 결정했음.

- VK_SUCCESS VK_SUBOPTIMAL_KHR 둘다 "success" 로 간주했음.

 

 

result = vkQueuePresentKHR(presentQueue, &presentInfo);

if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
    recreateSwapChain();
} else if (result != VK_SUCCESS) {
    throw std::runtime_error("failed to present swap chain image!");
}

currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;

- vkQueuePresentKHR  는 동일하지만, 이경우 최상의 결과를 얻기위해 suboptimal일 경우에도 재생성하도록 함.

 

 

 

 

4. Handling resizes explicitly

- 비록 많은 drivers 과 platforms이 VK_ERROR_OUT_OF_DATE_KHR  이 창 크기 조정후 자동으로 트리거 되지만,

- 발생이 보장되지 않음

- 그렇기 때문에 명시적으로 크기조정을해야함

- 일단 Resize를 위한 새로운 멤버 변수 하나를 추가해야함. 

std::vector<VkFence> inFlightFences;
size_t currentFrame = 0;

bool framebufferResized = false;

- 그리고 이 플래그가 일어났을때 또한 리사이즈가 일어나도록 drawFrame 함수를 수정해야함

if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) {
    framebufferResized = false;
    recreateSwapChain();
} else if (result != VK_SUCCESS) {
    ...
}

- vkQueuePresentKHR 후에 이플레그를 검사해야한다.

- 세마포어가 일관된 상태여야하기 때문, 그렇지 않으면 signalled 상태인 세마포어가 적절히 대기하지 않을 수 있음

- 이제 확실히 resize를 감지하기위해

- GLFW 함수인 glfwSetFramebufferSizeCallback 를 사용하여 콜백을 설정할 수 있음.

void initWindow() {
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
    glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
}

static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {

}

- static 으로 함수를 만드는 이유는 GLFW가 HelloTriangleApplication 인스턴스에 대해

- 올바른 this 포인터로 멤버함수를 호출하는 방법을 모르기 때문이다. 

 

- 하지만 GLFWwindow  콜백 안에서 참조를 얻어올 수 있으며

- 그안에 임의의 포인터를 저장할 수 있는 또다른 GLFW함수가 있다.

- glfwSetWindowUserPointer 

window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
glfwSetWindowUserPointer(window, this);
glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);

- 이제 callback 함수 내에서  glfwGetWindowUserPointer 를 사용하여 멤버 변수 값을 찾아낼 수 있게되었다.

 

static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
    auto app = reinterpret_cast<HelloTriangleApplication*>(glfwGetWindowUserPointer(window));
    app->framebufferResized = true;
}

- 이제 프로그램을 실행하여 윈도우창을 조정하여

- 프레임 버퍼가 창과 함께 실제로 제대로 크기가 조정되었는지 확인합시다.

5. Handling minimization

- 창을 최소화 할때 swapchain이 데이터에서 벗어날 수 있다.

- 이 경우에는 프레임 버퍼의 크기가 0이 되어버린다.

- 이 튜토리얼에서는 recreateSwapChain 함수를 확장하여 창이 다시 foreground에 올때까지

- 일시 중지하여 이를 처리한다. 

void recreateSwapChain() {
    int width = 0, height = 0;
    glfwGetFramebufferSize(window, &width, &height);
    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device);

    ...
}

- glfwGetFramebufferSize 초기호출은 정확한 사이즈를 얻기위해서이고

- glfwWaitEvents  는 대기할 필요가 없는 경우를 처리한다. (이벤트가 생기면 루프문을 돌게함)

 

- 이제 잘 동작하는 첫번째 Vulkan 프로그램을 마쳤습니다!

- 다음 장에서는 vertex shader 에서 하드코딩한 부분을 없애고 실제 vertex buffer를 사용할것입니다.

 

 

 

 

C++ code / Vertex shader / Fragment shader

 

https://vulkan-tutorial.com/en/Drawing_a_triangle/Swap_chain_recreation