2021. 8. 11. 15:02ㆍ그래픽스/vk
Rendering and presentation
-모든것이 합쳐지는 챕터
1. Setup
- mainLoop 에서 호출되어 삼각형을 표시하는 함수를 작성할것임
- drawFrame 함수를 만들것임
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
drawFrame();
}
}
...
void drawFrame() {
}
2. Synchronization
- drawFrame이 함수는 다음과 같은 작업을 수행하도록 작성할것임.
- Acquire an image from the swap chain
- Execute the command buffer with that image as attachment in the framebuffer
- Return the image to the swap chain for presentation
- 이러한 각 이벤트는 단일 함수호출을 사용하여 모션으로 설정되지만, 비동기적으로 실행됨
- 함수호출은 작업이 실제로 완료되기전에 반환되며, 실행 순서도 정의되지 않음
- 각 작업은 이전 작업에 의존하기 때문에 일어난 불행한일
2.1 fence, semaphore
- syncronizing swap chain events : fence , semaphores.
- 공통점
- - - - operation signal 를 가짐으로써 연산을 조정하는데 사용. (하나의 연산 신호를 가짐)
- - - - unsignaled 상태에서 signaled 상태로 갈때까지 대기하도록하여 연산을 조정하는데 사용할 수 있음
- 차이점
- - - - fence : vkWaitForFences 와 같은 호출을 사용하여 프로그램에 접근할 수 있음
- - - - semaphore : 그러지 못함
- - - - fence : 응용 프로그램 자체를 렌더링 연산과의 synchronize를 위해 설계됨.
- - - - semaphore : command queue 안 또는 전체에서 연산을 synchronize하는데 사용됨
- 튜토리얼에서는 queue operations of draw commands 과 presentation을 동기화하기를 원하기 때문에
- semaphore를 사용하는게 적절하다.
3. Semaphores
- 두개의 semaphore가 필요
- image를 얻어오고 렌더링 할 준비가 끝났음을 알리기 위한것
- 렌더링이 완료되고 presentation이 발생할 수 있음을 알리기 위한것
- 이 두개의 semaphore object를 class members로 만들어주자.
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;
- semaphore을 만들었으니 직접 생성하는 함수createSemaphores 를 만들어야한다.
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
createFramebuffers();
createCommandPool();
createCommandBuffers();
createSemaphores();
}
...
void createSemaphores() {
}
- 객체를 생성하기 위해 createinfo 구조체를 작성해야한다.
3.1 CreateInfo
// Provided by VK_VERSION_1_0
typedef struct VkSemaphoreCreateInfo {
VkStructureType sType;
const void* pNext;
VkSemaphoreCreateFlags flags;
} VkSemaphoreCreateInfo;
void createSemaphores() {
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
}
- Vulkan API 또는 확장 기능의 향후 버전은 flag에 추가적 기능이 추가될 수 있다
(현재버전의 flag는 향후 사용을 위해 예약되어있는것)
- vkCreateSemaphore 함수를 통해 생성할 수 있음.
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS) {
throw std::runtime_error("failed to create semaphores!");
}
- 프로그램 끝에서 정리해줘야함.
void cleanup() {
vkDestroySemaphore(device, renderFinishedSemaphore, nullptr);
vkDestroySemaphore(device, imageAvailableSemaphore, nullptr);
...
pNext를 사용하여 timeline semaphore로 확장가능하다. https://www.khronos.org/blog/vulkan-timeline-semaphores/cuid=4237875 |
4. Acquiring an image from the swap chain
- drawFrame 함수에서 가장 먼저 해야할 일은 swapchain에서 이미지를 얻어오는것이다.
- swapchain은 확장기능(extension feature)이기 때문에, vk*KHR 네이밍 규칙의 함수를 사용해야함,
void drawFrame() {
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}
- vkAcquireNextImageKHR
- 파라미터 1,2 : 이미지를 가져오려고하는 logical device , swapchain
- 파라미터 3 : 이미지를 사용할 수 있는 시간을 나노초 단위로 지정
(a timeout in nanoseconds for an image to become available)
- - - - 64비트 unsigned int 값의 최대값을 사용하면 timeout이 비활성화됨
- 파라미터 4, 5 : presentation engine이 이미지 사용을 마치면, 신호를 보낼 synchronization object를 명시
- - - - 이 시점에서 이미지를 그릴 수 있다.
- - - - 여기에서 fence나 semaphore 또는 둘다 지정하는게 가능하다. (파라미터 4 : semaphore / 파라미터 5 : fence)
- - - - 여기서 imageAvailableSemaphore 객체를 사용할것임
- 파라미터 6 : 사용가능하게 된 swapchain image의 index를 저장할 변수를 지정
- - - - 이 index는 swapChainImages 배열의 VkImage 를 참조하게됨.
- - - - 이 인덱스를 사용하여 적절한 command buffer를 사용하게됨
presentation engine - definition - What exactly is the "presentation engine" in Vulkan jargon? - Stack Overflow |
5. Submitting the command buffer
- Queue submission및 synchronization은 VkSubmitInfo 구조체의 매개변수를 통해 구성됨
5.1 Image Available Semaphore
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphore};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
- 파라미터 1~3 : 실행이 시작되기 전에 대기할 파이프 라인의 단계들에서 기다릴 세마포어를 지정
- 색상을 이미지에 쓰기위해(writing colors to image) 사용할 수 있을 때까지 기다림.
- 그래서 color attachment를 사용하는(write) graphics pipeline의 단계를 명시해야함.
- 아직 이미지를 사용하지 못하는 상태에서 vertex shader 등을 실행할 수 있음을 의미.
(That means that theoretically the implementation can already start executing our vertex shader and such while the image is not yet available)
- waitStages 배열의 각 항목은 pWaitSemaphores 에서 동일한 index를 가진 세마포어에 해당함.
(Each entry in the waitStages array corresponds to the semaphore with the same index in pWaitSemaphores)
- 이런 스테이지는 VK_PIPELINE_STAGE 열거형을 사용한다. (이 열거형은 커맨드가 실행되는 순서와 같은것은 아님, 일부 스테이지는 병합, 소실될 수 있음) - 이 스테이지들은 커맨드가 통과할 파이프라인 스테이지들이다.
- 이것들은 다중의 스테이지들을 합치거나 특별한 접근을 제어하는데 사용됨
출처: https://lifeisforu.tistory.com/417 [그냥 그런 블로그] |
5.2 Commandbuffer
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
- 실제 실행을 위한 command buffers을 명시한다
- color attachment로 방금 획득한 swapchain image를 바인딩하는 command buffer를 제출해야함.
(As mentioned earlier, we should submit the command buffer that binds the swap chain image we just acquired as color attachment.)
5.3 RenderFinished Semaphore
VkSemaphore signalSemaphores[] = {renderFinishedSemaphore};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
- signalSemaphoreCount, pSignalSemaphores : commandbuffer 가 실행을 완료하면 신호를 보낼 세마포어를 지정
- renderFinishedSemaphore 를 지정하여 목적에 맞게사용.
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
- vkQueueSubmit : graphics queue에 commandbuffer를 제출
- VkSubmitInfo : 이 구조체 배열을 인수로 받는것은 작업부하(workload)가 더 커졌을 때 효율성을 높이기 위한것
- 마지막 파라미터는 선택적인 fence를 참조함, 동기화를 위해 semaphore를 사용했으므로 VK_NULL_HANDLE 로 설정
- - - - 이를 통해 command buffer가 실행을 완료할 때 신호를 보내게 할 수 있음.
6. Subpass dependencies
- renderpass의 subpasses은 자동으로 image layout을 전환한다.
- 이러한 전환은 subpass dependencies 에의해 제어됨.
- - - - subpass dependencies 는 subpass 사이의 memory 및 execuation dependencies를 정의함
- 우리는 지금 오직 하나의 subpass만 있지만
- subpass 전후의 작업은 암시적으로 "subpasses"로 계산됨. (복수임을 가정하고 연산을함)
- 두가지 종속성 : render pass의 시작과 끝에서 전환을 처리.
- 하지만 전자는(former , render pass 의 시작) 적시에 일어나지 않음
- - - - 전자는, 전환이 pipeline 시작부분 에서 일어난다고 가정하지만
- - - - 그 시점에는 아직 이미지를 얻지 못했음
- 이문제를 처리할 수 있는 방법은 두가지가 있음
- imageAvailableSemaphore 에 대한 waitStages 를 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT 로 변경하는 방법
- - - - image를 이용가능할때까지 render passes를 시작되지 않도록함
- VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT stage 까지 render pass를 기다리게 하는 방법
- 여기서는 두번째 방법을 사용함. (subpass 종속성을 살펴보고 작동하는 방법을 알 수 있음)
6.1 VkSubpassDependency - createRendeerPass
- subpass 종속성은 VkSubpassDependency 구조체에 기술할 수 있음
- createRenderPass 함수에 다음 코드를 추가할것임.
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
- 위 필드들은 종속성과, 종속 서브패스의 indices를 지정한다.
- 특수값 VK_SUBPASS_EXTERNAL 는 srcSubpass 또는 dstSubpass 값으로 올 수 있으며,
- renderpass 전후의 암시적 subpass를 참조함. ?????
- 튜토리얼에선 인덱스 0 을 사용하여 처음이자 유일한 하나인 subpass를 참조하도록함
- dstSubpass 는 종속성 그래프에서 순환을 방지하기위해 항상 srcSubpass 보다 높이있어야함.
(서브패스중 하나가 VK_SUBPASS_EXTERNAL이 아닌 경우)
The first two fields specify the indices of the dependency and the dependent subpass. The special value VK_SUBPASS_EXTERNAL refers to the implicit subpass before or after the render pass depending on whether it is specified in srcSubpass or dstSubpass. The index 0 refers to our subpass, which is the first and only one. The dstSubpass must always be higher than srcSubpass to prevent cycles in the dependency graph (unless one of the subpasses is VK_SUBPASS_EXTERNAL). |
If srcSubpass is equal to VK_SUBPASS_EXTERNAL, the first synchronization scope includes commands that occur earlier in submission order than the vkCmdBeginRenderPass used to begin the render pass instance Otherwise, the first set of commands includes all commands submitted as part of the subpass instance identified by srcSubpass and any load, store or multisample resolve operations on attachments used in srcSubpass. In either case, the first synchronization scope is limited to operations on the pipeline stages determined by the source stage mask specified by srcStageMask. |
The synchronization scopes define which other operations a synchronization command is able to create execution dependencies with. Any type of operation that is not in a synchronization command’s synchronization scopes will not be included in the resulting dependency. |
6.1.1 stage, access mask
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
- 대기할 연산 과 이러한 연산이 일어나는 스테이지를 지정
- 이미지에 접근하기전에 swapchain이 이미지 읽기를 먼저 끝마치도록 해야하므로.
- color attachment output 단계까지 기다리고 수행되어지도록 설정한것.
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- 작업은 color attachment stage 이후이며 "writing" 과 관련됨을 명시한것
- 이러한 세팅은 실제로 필요하고 허용될때까지 전환이 발생하지 않도록 방지함.
(when we want to start writing colors to it.)
- 암시적으로 subpass는 전환을 수행함 VK_IMAGE_LAYOUT_UNDEFINED -> VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL - 그리고 우리는 이 전환이 일어날때 이미지를 이미 습득한 상태여야만함 - pWaitDstStageMask 를 통해 VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 스테이지전에 image를 얻어오는것을 보장했지만 - 정확히 언제 인지는 모름 - 그러므로 external dependency를 틍해 subpass 전환을 VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 후에 하도록 설정해줘야함 (그렇지 않으면 GPU는 image를 presentation engine으로 부터 얻기전에 전환을 수행할것임) srcAccessMask = 0 - 이것은 전환이 발생하기 전의 접근 범위를 나타냄 - 렌더링 파이프라인은 전환 전에 이미지를 사용하지 않음(읽기 또는 쓰기가 없음) - 따라서 무시하는 차원에서 0으로 설정 https://github.com/ARM-software/vulkan-sdk/issues/14 |
6.1.2 dependency
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
- VkRenderPassCreateInfo 구조체는 종속 배열을 지정하는 두개의 필드를 가지고 있음
7. Presentation
- 프레임을 그리는 마지막 단계는 결과를 swapchain에 다시 제출하여 결국 화면에 표시되도록하는것
- presentation은 VkPresentInfoKHR 구조체를 통해 구성됨. (drawFrame 함수 끝에서)
7.1 presentInfo
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
- waitSemaphoreCount, pWaitSemaphore : presentation이 일어나기전에 대기해야하는 세마포어
(signalSemaphore = renderFinishedSemaphore)
7.2 swapchain
VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
- 이미지를 표시할 swap chains을 명시
- 그리고 각 swapchain에 대한 image 인덱스를 지정
- 이것들은 거의 항상 하나.
7.3 pResult
presentInfo.pResults = nullptr; // Optional
- 마지막 파라미터 pResults
- VkResult 의 배열을 명시할 수 있음
- 모든 individual swap chain을 성공적으로 presentation했는지 검사한다.
- 이것은 single swapchain을 사용할 경우 필요가 없음
(오직 present function이 리턴하는 하나의 값만 검사하면되므로)
7.4 PresentKHR
vkQueuePresentKHR(presentQueue, &presentInfo);
- vkQueuePresentKHR 함수는 swapchain에 image를 표시하라는 요청을 제출한다.
- vkAcquireNextImageKHR and vkQueuePresentKHR 에 대한 오류 처리를 다음 챕터에서 다룰 꺼임
(리사이즈와 관련이 있다.)
만일 올바르게 작성했다면, 다음과 비슷한 내용이 실행될 것이다.

This colored triangle may look a bit different from the one you're used to seeing in graphics tutorials. That's because this tutorial lets the shader interpolate in linear color space and converts to sRGB color space afterwards. See this blog post for a discussion of the difference. |
7.5 DeviceWaitIdle
- 불행하게도, 유효성 검사가 활성화된 상태면, 이 프로그램이 종료될때 프로그램은 충돌이 일어남
- debugCallback 는 오류메시지를 터미널에 표시해줄것이다.

- drawFrame 이 비동기적으로 모든 작업을 수행하기 때문이다.
- 즉, mainLoop 에서 루프를 종료할 때 그리기 및 프레젠테이션 작업이 계속 진행 중일 수 있음
- 그런 일이 일어나는동안 리소스를 정리한다는것은 좋지 못한 아이디어임.
- 이러한 문제를 해결하려면, .mainLoop 의 종료와 윈도우 제거전에 logical device가 연산이 끝날때까지 기다려야한다.
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
drawFrame();
}
vkDeviceWaitIdle(device);
}
- vkQueueWaitIdle함수를 사용하여, 특정 command queue의 작업이 끝날때 까지 대기하도록 할 수 있다.
- 이런 함수들은 synchronization을 수행하는 가장 기초적인 방법으로 사용할 수 있다.
- 이제 문제없이 창이 닫힐것이다.
8. Frames in flight
- 유효성 검사를 활성화 시킨상태에서 애플리케이션을 실행하면,
- 오류가 발생하거나 메모리 사용량이 천천히 증가하는것을 볼 수 있다.
- 그 이유는 응용 프로그램이 drawFrame 함수에서 작업을 빠르게 제출하지만
- 실제로 완료된 작업이 있는지 확인하지 않기 때문
- CPU가 작업을 GPU가 따라갈 수 있는것보다 빠르게 제출하면,
- queue에서 작업이 천천히 채워지게됨.
- 더 나쁜것은, 동시에 여러 프레임에 대해 command buffers과 함께
- imageAvailableSemaphore및renderFinishedSemaphore 를 재사용하고있다는것
8.1 vkQueueWaitIdle 를 추가하는 방식 - 비효율적
- 이문제를 해결할 쉬운 방법은 아래와 같이 vkQueueWaitIdle. 함수를 통해
- 제출한 직후 작업이 완료될 때 까지 기다리는것이다.
void drawFrame() {
...
vkQueuePresentKHR(presentQueue, &presentInfo);
vkQueueWaitIdle(presentQueue);
}
- 그러나 현재로서는 GPU를 최적으로 사용하지 않지 않을 가능성이 높다
- 전체 graphics pipeline이 한번에 한 프레임에만 사용되기 때문이다.
- 현재 프레임이 이미 진행한 단계는 idle 상태이며, 다음 프레임에서 미리 사용할 수 있음
- 이제 우리의 응용프로그램을 multiple frames에서 쌓이는 작업의 양을 제한하면서 진행할 수 있도록 확장할 것이다.
8.2 multiple semaphore , multiple fence 방식
8.2.1 GPU-GPU synchronization - using semaphore
- 프로그램 상단에 동시에 처리하고싶은 프레임의 수를 정의한다.
const int MAX_FRAMES_IN_FLIGHT = 2;
- 각각의 프레임은 자신의 semaphore들을 가지고 있어야한다.
std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
- createSemaphores 함수는 다음과 같이 변경해야한다.
void createSemaphores() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create semaphores for a frame!");
}
}
- Similarly, they should also all be cleaned up:
void cleanup() {
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
}
...
}
- 매번 올바른 세마포어를 사용하기위해 현재 프레임을 추적해야함
- 이를 위해 프레임 인덱스를 사용함.
- 이를 통해 적절한 세마포어를 사용하도록 drawFrame 함수를 수정 할 수 있음.
- 이때 매번 다음 프레임으로 넘어가는것을 잊지 말아야함.
size_t currentFrame = 0;
void drawFrame() {
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
...
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
...
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
...
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
- 모듈로(%)연산을 사용함으로 써, frame index 가
- MAX_FRAMES_IN_FLIGHT 개의 프레임의 작업을 queue에 넣고
- 다시 반복하도록 보장해줘야함
- 현재, 여러 프레임을 동시에 처리할 수 있도록 필요한 객체를 설정했지만.
- 실제로 MAX_FRAMES_IN_FLIGHT 개수보다 더 많이 제출하는것을 방지하지는 않음
- 현재로서는 GPU-GPU 동기화만 하고 있고, 작업상황을 추적하기 위한
- CPU-GPU 동기화는 진행되지 않았음.
- 프레임 #0이 아직 진행중인 동안 프레임 #0 객체를 사용할 수 있다.
(아직 대기하는등 제한을 걸지않음)
8.2.2 CPU-GPU synchronization - using fence
- CPU-GPU 동기화를 수행하기 위해서
- Vulkan은 synchronization primitive 의 두번째 타입인 fence 제공한다.
- Fences은 신호를 받고 기다릴 수 있다는 점에서 Semaphores와 비슷하지만
- 실제 자신의 코드에서 기다린다.
- 먼저 각 프레임에 대해 fence를 만든다
std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> inFlightFences;
size_t currentFrame = 0;
- createSemaphores 함수를 createSyncObjects 로 이름을 바꾸고
- fence도 같이 초기화 하도록함.
void createSyncObjects() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS ||
vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create synchronization objects for a frame!");
}
}
}
- VkFence의 생성은 세마포어 생성과 비슷하며, Destroy 또한 마지막에 해줘야함.
void cleanup() {
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);
}
...
}
- 이제 drawFrame 의 동기화에 fence를 사용하도록 변경해야한다.
- vkQueueSubmit 의 파라미터로 fence를 건네주어, command buffer의 실행이 끝날 경우 신호를 주도록 할 수 있음
- 이것은 프레임이 완료되었다는것을 알리는것과 동일하다.
void drawFrame() {
...
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
...
}
- 이제 남은것은 drawFrame 프레임이 끝날 때 까지 기다리도록 시작부분을 변경해야함.
void drawFrame() {
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFences[currentFrame]);
...
}
- vkWaitForFences 함수는 fence 배열을 파라미터로 받고, 이 배열의 일부 또는 전부가 신호를 받을 때 까지 기다림.
- 여기서 VK_TRUE 를 파라미터로 설정한것은 모든 fence를 기다리기를 원하는것
(하지만, 단일 fence인 경우 명백히 중요하지 않음)
- vkAcquireNextImageKHR 함수와 마찬가지로 timeout 설정도 해줘야함.
- 세마포어와 달리 fence를 vkResetFences호출로 재설정하여 수동으로 신호없는(unsignaled) 상태로 복원해줘야함.
- 지금 프로그램을 실행하면, 더이상 아무것도 렌더링하지 않으며, 유효성 검사레이어는 아래와 같은 메시지를 표시한다.

- 이는 제출되지않은 fence를 기다리고 있는것을 의미한다.
- 기본적으로 fence는 unsignaled 상태로 생성되기 때문이다.
- 따라서 vkWaitForFences 함수에서 영원히 대기하게됨.
- 그러므로 아래와 같이 fencs를 생성할때 signaled 상태로 초기화 하도록해야함.
void createSyncObjects() {
...
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
...
}
- 이제 메모리 누수가 사라졌지만, 이 프로그램은 아직 제대로 작동하지 않음
- MAX_FRAMES_IN_FLIGHT 보다 더 높은 swap chain images의 인덱스가 들어올때
- vkAcquireNextImageKHR 가 images를 out-of-order인것을 반환할때
If MAX_FRAMES_IN_FLIGHT is higher than the number of swap chain images or vkAcquireNextImageKHR returns images out-of-order then it's possible that we may start rendering to a swap chain image that is already in flight. |
- 이러한것들을 피하기 위해서는, 각각의 swapchain image를 추적할 필요가 있음
(현재 진행중인 프레임에서 스왑체인 이미지를 사용하고 있는 경우)
To avoid this, we need to track for each swap chain image if a frame in flight is currently using it. This mapping will refer to frames in flight by their fences so we'll immediately have a synchronization object to wait on before a new frame can use that image. |
8.2.3 ImagesInFlight
- swapchain image를 추적할 fence 배열을 생성해야함.
std::vector<VkFence> inFlightFences;
std::vector<VkFence> imagesInFlight;
size_t currentFrame = 0;
- 마찬가지로 createSyncObjects 에서 초기화
- 그리고 첫 프레임에서는 이미지를 사용한적이 없으므로,
- 명시적으로 fence를 VK_NULL_HANDLE 로 초기화헤야함
void createSyncObjects() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE);
...
}
- 이제 drawFrame 함수를 수정해야함.
- 새프레임에 대해 방금 할당받은 이미지를 사용하는 이전 프레임을 기다리도록 수정해야함.
void drawFrame() {
...
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
// Check if a previous frame is using this image (i.e. there is its fence to wait on)
if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
}
// Mark the image as now being in use by this frame
imagesInFlight[imageIndex] = inFlightFences[currentFrame];
...
}
- vkWaitForFences 함수가 하나더 늘어났으니,
- vkResetFences 함수 또한 위치를 변경해줘야함
- 간단하게 fence를 사용하기 직전에 리셋해주었다.
void drawFrame() {
...
vkResetFences(device, 1, &inFlightFences[currentFrame]);
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
...
}
- 이제 synchronization를 보장하도록 모든 코드를 수정하였음
- 이제 두개의 작업프레임을 가지고, 실수로 동일한 이미지를 사용하지 않음.
- 최종 정리와 같은 코드는 vkDeviceWaitIdle 와 같은 더 거친 방법을 의존하는게 좋음.
- 이러한 접근방법은 성능요구사항에따라 결정해야함.
- 더 광범위한 synchronization에 대해 배우고 싶다면
- this extensive overview by Khronos를 보면된다.
8.23 요약 - inflightFence가 signal 됬다는것은 연결된 swapchain의 제출이 끝났다는것. - 따라서 함수 처음에 이 fence의 신호를 기다리고 이 자원을 사용할 수 있으면 다음 코드로 넘어감 - 하지만 이때 만약 이미지를 습득 했어도 이 이미지가 여전히 제출된상태일 수 있음 - (MAX_FLIGHT와 image 의 개수가 다를경우 항상 똑같은 세마포어가 똑같은 swapchain image에 사용되지 않음) - 또한 이런 fence는 전부 "포인터" 타입임. (VK_DEFINE_NON_DISPATCHABLE_HANDLE 로써 정의된 타입은 전부 구조체 포인터) ( https://lifeisforu.tistory.com/395 ) - imageInFlightFence는 포인터로 역할 직접적으로 생성하지 않음. 전부 null handle로 생성 - 그리고 이것은 inFlightFence를 가리키는 역할임. - 그러므로 해당 inFlightFence가 아직 signal 상태가 아니라면, 이는 아직 in flight 상태라는것 - 따라서 이를 검사하여, 해당 이미지가 제출이 완료되어야 - 현재 inFlightFence를 사용하여 이 이미지를 다시 in flight 상태로 만들 수 있음 - 따라서 한번더 vkWaitForFence 함수를 사용하여 대기해야하는것. |
9. Conclusion
- 900줄이 넘는 코드로 마침내 삼각형을 스크린에 띄웠다.
- Vulkan 프로그램을 Bootstrapping 하는것은 확실히 많은 작업이지만
- 중요한것은 명시적으로 엄청난 양을 제어할 수 있다는것이다.
- 이제 복습을 통해 Vulkan의 구조를 익히고,
- 객체들이 서로 어떻게 관련되는지 mental model을 구축하는것이 좋음
- 이제부터는 이 프로그램의 기능을 확장하기위해, 이 지식을 바탕으로 구축할것임.
- 다음장에선 잘 동작하는 vulkan 프로그램을 위해 처리해야하는 작은것 하나를 더 다룰것임.
void createSyncObjects() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS ||
vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create synchronization objects for a frame!");
}
}
}
void drawFrame() {
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
}
imagesInFlight[imageIndex] = inFlightFences[currentFrame];
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
vkResetFences(device, 1, &inFlightFences[currentFrame]);
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
vkQueuePresentKHR(presentQueue, &presentInfo);
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
C++ code / Vertex shader / Fragment shader
https://vulkan-tutorial.com/Drawing_a_triangle/Drawing/Rendering_and_presentation
'그래픽스 > vk' 카테고리의 다른 글
[vk] Vertex buffers - Vertex input description (0) | 2021.08.14 |
---|---|
[vk] Drawing triangle - Drawing - Swap chain recreation (0) | 2021.08.11 |
[vk] Drawing triangle - Drawing - Command buffers (0) | 2021.08.11 |
[vk] Drawing triangle - Drawing - Framebuffers (0) | 2021.08.11 |
[vk] Drawing triangle - graphics pipeline basics - Conclusion (0) | 2021.08.09 |