본문 바로가기

Vulkan

Drawing - Rendering and presentation


이전 글 : Drawing - Command buffers
다음 글 : Swap chain recreation



Rendering and presentation
렌더링과 프리젠테이션

Setup


이번에는 앞에서 설명한 모든 것이 함께 조립될 것 입니다.. 삼각형을 화면에 올리기 위해 메인 루프에서 호출 할 drawFrame 함수를 작성합니다. 함수를 만들고 mainLoop에서 호출하십시오.


void mainLoop() {
   while (!glfwWindowShouldClose(window)) {
       glfwPollEvents();
       drawFrame();
   }
}

...

void drawFrame() {

}





Synchronization

동기화


drawFrame 함수는 다음 작업을 수행합니다.


  • 스왑 체인에서 이미지를 가져옵니다.

  • 해당 이미지가 있는 명령 버퍼를 프레임 버퍼의 어테치먼트로 실행하십시오.

  • 프레젠테이션을 위해 이미지를 스왑 체인으로 반환합니다.


이러한 각 이벤트는 단일 함수 호출을 사용하여 동작하도록 설정 되지만 비동기 적으로 실행됩니다. 함수 호출은 작업이 실제로 완료되기 전에 반환되며 실행 순서도 정의되지 않습니다. 그것은 각각의 작업이 이전 작업에 의존하기 때문에 어쩔수 없는 일이기도 합니다.


스왑 체인 이벤트를 동기화하는 방법에는 펜스와 세마포어가 있습니다. 이들은 하나의 연산 신호를 가지고 연산을 조정하며 펜스나 세마포어가 unsignaled 상태에서 signaled 상태로 갈 때까지 대기하는 데 사용할 수 있는 두 객체입니다.


차이점은 펜스의 상태는 vkWaitForFences 와 같은 호출을 사용하여 프로그램에서 액세스 할 수 있으며 세마포어는 사용할 수 없다는 점입니다. 펜스는 주로 응용 프로그램 자체를 렌더링 작업과 동기화하는 반면 세마포어는 명령 대기열 내에서 또는 작업 대기열간에 작업을 동기화하는 데 사용됩니다. 우리는 세마포어를 가장 적합하게 만드는 그리기 명령 및 프리젠 테이션의 대기열 작업을 동기화하려고 합니다.



Semaphores

세마포어


이미지를 가져 와서 렌더링 할 준비가 되었음을 알리기 위해 하나의 세마포가 필요하며 렌더링이 완료되고 프리젠 테이션이 발생할 수 있음을 알리는 또 다른 신호가 필요합니다. 이러한 세마포어 객체를 저장할 두 개의 클래스 멤버를 만듭니다.


VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;


세마포어를 만들기 위해 이 튜토리얼의 마지막 부분인 createSemaphores 를 추가합니다.


void initVulkan() {
   createInstance();
   setupDebugCallback();
   createSurface();
   pickPhysicalDevice();
   createLogicalDevice();
   createSwapChain();
   createImageViews();
   createRenderPass();
   createGraphicsPipeline();
   createFramebuffers();
   createCommandPool();
   createCommandBuffers();
   createSemaphores();
}

...

void createSemaphores() {

}


세마포를 생성하려면 VkSemaphoreCreateInfo 를 준비해야 하지만, 현재 버전의 API에서는 실제로 sType 외에 필수 필드가 없습니다.



void createSemaphores() {
   VkSemaphoreCreateInfo semaphoreInfo = {};
   semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
}


Vulkan API 또는 확장 기능의 향후 버전은 다른 구조와 마찬가지로 플래그 및 pNext 매개 변수에 대한 기능을 추가 할 수 있습니다. 세마포를 생성하는 것은 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);





Acquiring an image from the swap chain

스왑 체인에서 이미지 가져 오기


앞에서 언급했듯이 drawFrame 함수에서 가장 먼저해야 할 일은 스왑 체인에서 이미지를 얻는 것입니다. 스왑 체인은 확장기능 이기 때문에 vk * KHR 네이밍 규칙이 있는 함수를 사용해야 합니다.


void drawFrame() {
   uint32_t imageIndex;
   vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}


vkAcquireNextImageKHR 의 처음 두 매개 변수는 이미지를 가져 오려는 논리 장치와 스왑 체인 입니다. 세 번째 매개 변수는 이미지가 사용 가능하게 되는 시간을 나노초 단위로 지정합니다. 64 비트 부호없는 정수의 최대 값을 사용하면 시간 초과가 비활성화됩니다.


다음 두 매개 변수는 프레젠테이션 엔진이 이미지 사용을 마치면 신호를 보낼 동기화 개체를 지정합니다. 그것이 우리가 그걸 그리기 시작할 수 있는 시점입니다. 세마포어, 펜스 또는 둘 다 지정할 수 있습니다. 우리는 여기서 imageAvailableSemaphore 를 사용하려고 합니다.


마지막 매개 변수는 사용 가능하게 된 스왑 체인 이미지의 인덱스를 출력하는 변수를 지정합니다. 인덱스는 swapChainImages 배열의 VkImage 를 참조합니다. 이 인덱스를 사용하여 올바른 명령 버퍼를 선택합니다.





Submitting the command buffer

명령 버퍼 제출


큐 제출 및 동기화는 VkSubmitInfo 구조의 매개 변수를 통해 구성됩니다.


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;


처음 세 매개 변수는 실행이 시작되기 전에 대기 할 파이프 라인의 단계 (들)에서 기다릴 세마포어를 지정합니다. 색상을 사용할 때까지 이미지에 색상을 쓰고 싶어하므로 color attchement 에 쓰는 그래픽 파이프 라인의 단계를 지정하고 있습니다. 이는 이론적으로 이미지 구현이 아직 완료되지 않은 상태에서 구현이 이미 버텍스 쉐이더 등을 실행할 수 있음을 의미합니다. waitStages 배열의 각 항목은 pWaitSemaphores 에서 동일한 색인을 가진 세마포어에 해당 합니다.


submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];


다음 두 매개 변수는 실행을 위해 실제로 제출할 명령 버퍼를 지정합니다. 앞서 언급했듯이, 방금 가져온 스왑 체인 이미지를 색상 첨부로 바인딩하는 명령 버퍼를 제출해야합니다.


VkSemaphore signalSemaphores[] = {renderFinishedSemaphore};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;


signalSemaphoreCount 및 pSignalSemaphores 매개 변수는 명령 버퍼가 실행을 완료하면 신호를 보낼 세마포어를 지정합니다. 이 경우 renderFinishedSemaphore를 사용합니다.


if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
   throw std::runtime_error("failed to submit draw command buffer!");
}


이제 vkQueueSubmit 을 사용하여 명령 버퍼를 그래픽 대기열에 제출할 수 있습니다. 이 함수는 작업 부하가 훨씬 클 때 VkSubmitInfo 구조체 배열을 효율성을 위한 인수로 사용합니다. 마지막 매개 변수는 명령 버퍼가 실행을 완료 할 때 신호를 보낼 선택적 펜스를 참조합니다. 동기화를 위해 세마포어를 사용하기 때문에 VK_NULL_HANDLE을 전달합니다.




Subpass dependencies

하위 패스 종속성


렌더 패스의 하위 패스는 이미지 레이아웃 전환을 자동으로 처리합니다. 이러한 전환은 서브 패스 간의 메모리 및 실행 종속성을 지정하는 서브 패스 종속성에 의해 제어됩니다. 지금은 하나의 서브 패스 만 있지만이 서브 패스 전후의 작업은 암시적 "서브 패스"로 계산됩니다.


렌더링 패스의 시작과 렌더링 패스의 끝에서 전환을 처리하는 두 가지 기본 종속성이 있지만, 적시에 발생하지는 않습니다. 그것은 전환이 파이프 라인의 시작에서 발생한다고 가정하지만 그 시점에서 아직 이미지를 얻지 못했습니다! 이 문제를 처리하는 데는 두 가지 방법이 있습니다. imageAvailableSemaphore의 waitStages를 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT 로 변경하여 이미지를 사용할 수있을 때까지 렌더링 패스가 시작되지 않도록 하거나 VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 단계까지 렌더 패스를 대기시킬 수 있습니다. Subpass 종속성을 살펴보고 작동하는 방법을 알아 보는 것이 좋을듯 하여  두 번째 옵션을 사용하기로 합니다.


하위 패스 종속성은 VkSubpassDependency 구조체에 지정됩니다. createRenderPass 함수로 이동하여 다음을 추가하십시오.


VkSubpassDependency dependency = {};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;


처음 두 필드는 종속성과 종속 서브 패스의 색인을 지정합니다. 특수 값 VK_SUBPASS_EXTERNAL은 srcSubpass 또는 dstSubpass에 지정되었는지 여부에 따라 렌더 패스 전후의 암시적 서브 패스를 참조합니다. 인덱스 0은 처음이자 유일한 하나 인 우리의 서브 패스를 참조합니다. dstSubpass는 종속성 그래프에서 순환을 방지하기 위해 항상 srcSubpass보다 커야합니다.


dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;


다음 두 필드는 대기 할 작업과 이러한 작업이 수행되는 단계를 지정합니다. 스왑 체인이 이미지에 접근하기 전에 스왑 체인이 이미지 읽기를 끝내기를 기다려야합니다. 이 작업은 색상 첨부 출력 단계 자체를 기다려 수행 할 수 있습니다.


dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;


이 작업을 기다려야 하는 작업은 컬러 어테치먼트 단계에 있으며 컬러 어테치먼트를 읽고 쓰는 작업이 필요합니다. 이러한 설정은 실제로 필요하고 (그리고 허용 될 때까지) 일어나는 변화를 막을 것입니다 : 우리가 그것에 색을 쓰기 시작할 때.


renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;


VkRenderPassCreateInfo 구조체에는 종속 배열을 지정하는 두 개의 필드가 있습니다.



Presentation

프리젠테이션


프레임 그리기의 마지막 단계는 결과를 스왑 체인에 다시 제출하여 결국 화면에 표시되도록 하는 것입니다. 프리젠테이션은 drawFrame 함수의 끝에 VkPresentInfoKHR 구조를 통해 구성됩니다.


VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;

presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;


처음 두 매개 변수는 VkSubmitInfo 처럼 프리젠테이션이 발생하기 전에 대기 할 세마포어를 지정합니다.


VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;


다음 두 매개 변수는 이미지를 표시 할 스왑 체인과 각 스왑 체인의 이미지 인덱스를 지정합니다. 이것은 거의 항상 하나입니다.


presentInfo.pResults = nullptr; // Optional


pResults라는 마지막 선택적 매개 변수가 하나 있습니다. 프리젠테이션이 성공한 경우 VkResult 값의 배열을 지정하여 모든 개별 스왑 체인을 검사 할 수 있습니다. 현재 함수의 반환 값을 사용할 수 있기 때문에 단일 스왑 체인 만 사용하는 경우에는 필요하지 않습니다.


vkQueuePresentKHR(presentQueue, &presentInfo);


vkQueuePresentKHR 함수는 스왑 체인에 이미지를 표시하라는 요청을 제출합니다. 다음 장에서 vkAcquireNextImageKHR 및 vkQueuePresentKHR 모두에 대한 오류 처리를 추가 할 것입니다. 왜냐하면 그들의 실패가 반드시 우리가 지금까지 보아온 함수와는 달리 프로그램이 종료되어야 한다는 것을 의미하지는 않기 때문입니다.


지금까지 모든 것을 올바르게 작성 했다면 프로그램을 실행할 때 다음과 비슷한 내용을 보게됩니다.




예! 불행히도 유효성 검사 레이어가 활성화되면 프로그램을 닫을때 충돌 오류가 발생합니다. debugCallback에서 터미널에 인쇄 된 메시지는 다음과 같은 이유를 나타냅니다.



drawFrame의 모든 작업은 비동기입니다. 즉, mainLoop에서 루프를 종료해도 그리기 및 프레젠테이션 작업이 계속 진행될 수 있습니다. 그런 일이 일어나고있는 동안 자원을 정리하는 것은 문제가 있습니다.


이 문제를 해결하려면 mainLoop을 종료하고 창을 삭제하기 전에 논리 장치가 작업을 마칠 때까지 기다려야합니다.


void mainLoop() {
   while (!glfwWindowShouldClose(window)) {
       glfwPollEvents();
       drawFrame();
   }

   vkDeviceWaitIdle(device);
}


vkQueueWaitIdle 을 사용하여 특정 명령 대기열의 작업이 완료 될 때까지 기다릴 수도 있습니다. 이 기능들은 동기화를 수행하는 매우 기초적인 방법으로 사용될 수 있습니다. 이제  창을 닫을 때 문제없이 프로그램이 종료됩니다.



Memory leak

메모리 누수


유효성 검사 레이어가 활성화 된 상태에서 응용 프로그램을 실행하고 응용 프로그램의 메모리 사용량을 모니터링하면 응용 프로그램이 느리게 증가하는 것을 알 수 있습니다. 그 이유는 유효성 검사 레이어 구현이 응용 프로그램이 GPU와 명시 적으로 동기화되기를 기대하기 때문입니다. 기술적으로 필수는 아니지만 한 번 프레임을 수행하는 것이 성능에 큰 영향을 미치지는 않습니다.


다음 프레임 그리기를 시작하기 전에 프레젠테이션이 끝나기를 명시 적으로 기다림으로써 이를 해결 할 수 있습니다.


void drawFrame() {
   ...

   vkQueuePresentKHR(presentQueue, &presentInfo);

   vkQueueWaitIdle(presentQueue);
}


응용 프로그램의 상태는 많은 응용 프로그램에서 모든 프레임마다 업데이트됩니다. 이 경우 프레임을 이렇게 그리는 것이 가장 효율적입니다.


void drawFrame() {
   updateAppState();

   vkQueueWaitIdle(presentQueue);

   vkAcquireNextImageKHR(...)

   submitDrawCommands();

   vkQueuePresentKHR(presentQueue, &presentInfo);
}


이 방법을 사용하면 이전 프레임이 렌더링되는 동안 게임에서 AI 루틴을 실행하는 등 응용 프로그램의 상태를 업데이트 할 수 있습니다. 그렇게하면 GPU와 CPU가 항상 바쁘게 유지됩니다.



Conclusion

결론


화면에 뭔가를 보여주기 위하여 우리는 약 800라인의 프로그램 코드가 필요합니다.

Vulkan이 설명을 통해 엄청난 양의 제어권을 제공 한다는 소식이 있습니다. 코드를 다시 읽고 프로그램의 모든 Vulkan 개체의 목적에 대한 정신 모델을 작성하고 서로 관련시키는 방법을 생각해보십시오. 우리는이 지식을 바탕으로 프로그램의 기능을 이 시점부터 확장 할 것입니다.


다음 장에서는 잘 동작하는 벌칸 (Vulkan) 프로그램에 필요한 작은 것을 하나 더 다루 겠습니다.


C++ code / Vertex shader / Fragment shader


이전 글 : Drawing - Command buffers
다음 글 : Swap chain recreation



'Vulkan' 카테고리의 다른 글

Vertex input description  (0) 2018.01.25
Swap chain recreation  (0) 2018.01.25
Drawing - Command buffers  (1) 2018.01.25
Drawing - Framebuffers  (1) 2018.01.24
Graphics pipeline basics- Conclusion  (0) 2018.01.24