[譯]Vulkan教程(19)渲染和呈現


[譯]Vulkan教程(19)渲染和呈現

Rendering and presentation 渲染和呈現

Setup 設置

This is the chapter where everything is going to come together. We're going to write the drawFrame function that will be called from the main loop to put the triangle on the screen. Create the function and call it from mainLoop:

這是整合一切的章節。我們要寫drawFrame 函數that從主循環調用to將三角形放到屏幕上。創建此函數,從mainLoop調用它。

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

 

Synchronization 同步

The drawFrame function will perform the following operations:

  • 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

drawFrame 函數會實施下述操作:

  • 從交換鏈請求一個image。
  • 執行命令buffer,以此image作為幀緩存的附件。
  • 返回image到交換鏈for呈現。

Each of these events is set in motion using a single function call, but they are executed asynchronously. The function calls will return before the operations are actually finished and the order of execution is also undefined. That is unfortunate, because each of the operations depends on the previous one finishing.

這些事件都通過一個函數調用來驅動,但是它們是異步執行的。這些函數調用會在操作實際完成前就返回,執行的順序是未定義的。這很不幸,因為每個操作都依賴於前一個完成。

There are two ways of synchronizing swap chain events: fences and semaphores. They're both objects that can be used for coordinating operations by having one operation signal and another operation wait for a fence or semaphore to go from the unsignaled to signaled state.

同步交換鏈事件的方式有2種:fence和semaphore。它們都是可以用於協調操作的對象by讓一個操作有信號and另一個操作等待fence或semaphore從無信號到有信號的狀態。

The difference is that the state of fences can be accessed from your program using calls like vkWaitForFencesand semaphores cannot be. Fences are mainly designed to synchronize your application itself with rendering operation, whereas semaphores are used to synchronize operations within or across command queues. We want to synchronize the queue operations of draw commands and presentation, which makes semaphores the best fit.

兩者的區別是,fence的狀態可以在你的程序中查詢-使用vkWaitForFencesand之類的調用,semaphore的狀態則不能。Fence主要用於同步你的app的渲染操作,而semaphore主要用於同步命令隊列內或跨隊列的操作。我們想同步繪制命令的隊列操作和呈現,which讓semaphore成為最合適的選項。

Semaphores 信號

We'll need one semaphore to signal that an image has been acquired and is ready for rendering, and another one to signal that rendering has finished and presentation can happen. Create two class members to store these semaphore objects:

我們需要一個semaphore來提醒image已經被請求了,准備好for渲染,另一個semaphore來提醒渲染已經完成了,呈現可以開始。創建2個類成員來記錄這2個semaphore對象:

VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;

 

To create the semaphores, we'll add the last create function for this part of the tutorial: createSemaphores:

為創建semaphore,我們添加本教程這部分的最后一個create 函數createSemaphores

 1 void initVulkan() {
 2     createInstance();
 3     setupDebugCallback();
 4     createSurface();
 5     pickPhysicalDevice();
 6     createLogicalDevice();
 7     createSwapChain();
 8     createImageViews();
 9     createRenderPass();
10     createGraphicsPipeline();
11     createFramebuffers();
12     createCommandPool();
13     createCommandBuffers();
14     createSemaphores();
15 }
16  
17 ...
18  
19 void createSemaphores() {
20  
21 }

 

Creating semaphores requires filling in the VkSemaphoreCreateInfo, but in the current version of the API it doesn't actually have any required fields besides sType:

創建semaphore要求填入VkSemaphoreCreateInfo,但是API的當前版本里,它沒有任何要填的字段-除了sType

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

 

Future versions of the Vulkan API or extensions may add functionality for the flags and pNext parameters like it does for the other structures. Creating the semaphores follows the familiar pattern with vkCreateSemaphore:

將來的Vulkan API版本或擴展可能添加功能for flags 和pNext 參數-像其他結構體那樣。創建semaphore遵循類似的模式withvkCreateSemaphore

if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS ||
    vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS) {
 
    throw std::runtime_error("failed to create semaphores!");
}

 

The semaphores should be cleaned up at the end of the program, when all commands have finished and no more synchronization is necessary:

Semaphore應當在程序結束時被清理when所有的命令已經完成and不再需要同步:

void cleanup() {
    vkDestroySemaphore(device, renderFinishedSemaphore, nullptr);
    vkDestroySemaphore(device, imageAvailableSemaphore, nullptr);

 

Acquiring an image from the swap chain 從交換鏈請求image

As mentioned before, the first thing we need to do in the drawFrame function is acquire an image from the swap chain. Recall that the swap chain is an extension feature, so we must use a function with the vk*KHR naming convention:

如前所述,函數drawFrame 首先要做的是從交換鏈請求一個image。回憶that交換鏈是個擴展特性,所以我們必須用vk*KHR 命名方式的函數:

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

 

The first two parameters of vkAcquireNextImageKHR are the logical device and the swap chain from which we wish to acquire an image. The third parameter specifies a timeout in nanoseconds for an image to become available. Using the maximum value of a 64 bit unsigned integer disables the timeout.

vkAcquireNextImageKHR 的前2個參數是邏輯設備和交換鏈from which我們要請求image。第3個參數指定image可用的超時時間in納秒。使用64位的最大值則會禁用此超時。

The next two parameters specify synchronization objects that are to be signaled when the presentation engine is finished using the image. That's the point in time where we can start drawing to it. It is possible to specify a semaphore, fence or both. We're going to use our imageAvailableSemaphore for that purpose here.

接下來的2個參數指定同步對象that被信號的when呈現引擎用完了image。這是我們可以開始在其上繪制的時間。可以指定一個semaphore、fence或兩者都指定。我們這里要用我們的vkAcquireNextImageKHR  for我們的目的。

The last parameter specifies a variable to output the index of the swap chain image that has become available. The index refers to the VkImage in our swapChainImages array. We're going to use that index to pick the right command buffer.

Submitting the command buffer 提交命令buffer

Queue submission and synchronization is configured through parameters in the VkSubmitInfo structure.

隊列提交和同步是通過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;

 

The first three parameters specify which semaphores to wait on before execution begins and in which stage(s) of the pipeline to wait. We want to wait with writing colors to the image until it's available, so we're specifying the stage of the graphics pipeline that writes to the color attachment. That means that theoretically the implementation can already start executing our vertex shader and such while the image is not yet available. Each entry in the waitStages array corresponds to the semaphore with the same index in pWaitSemaphores.

前3個參數指定,執行開始前要等待哪個semaphore,管道在哪個階段等待。我們想等image准備好了再寫入顏色,所以我們指定圖形管道的寫入顏色附件的階段。這意味着理論上實現可以已經開始執行我們的頂點shader,同時image還沒有准備好。waitStages 數組的每個元素對應着pWaitSemaphores數組中相同索引的semaphore。

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

 

The next two parameters specify which command buffers to actually submit for execution. As mentioned earlier, we should submit the command buffer that binds the swap chain image we just acquired as color attachment.

接下來2個參數指定,提交哪個命令buffer去執行。如前所述,我們應當提交命令buffer that綁定交換鏈image that我們請求到用作顏色附件的那個。

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

 

The signalSemaphoreCount and pSignalSemaphores parameters specify which semaphores to signal once the command buffer(s) have finished execution. In our case we're using the renderFinishedSemaphore for that purpose.

signalSemaphoreCount 和pSignalSemaphores 參數指定,一旦命令buffer執行完畢,讓哪個semaphore發信號。在我們的案例中,我們要使用renderFinishedSemaphore  for那個目的。

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

 

We can now submit the command buffer to the graphics queue using vkQueueSubmit. The function takes an array of VkSubmitInfo structures as argument for efficiency when the workload is much larger. The last parameter references an optional fence that will be signaled when the command buffers finish execution. We're using semaphores for synchronization, so we'll just pass a VK_NULL_HANDLE.

我們現在可以提交命令buffer到圖形queue-用vkQueueSubmit。此函數接收VkSubmitInfo 結構體數組為參數for高效地大量工作負載。最后一個參數引用一個可選的fence,其在命令buffer執行完成后發信號。我們要用semaphore同步,所以傳入VK_NULL_HANDLE即可。

Subpass dependencies subpass依賴

Remember that the subpasses in a render pass automatically take care of image layout transitions. These transitions are controlled by subpass dependencies, which specify memory and execution dependencies between subpasses. We have only a single subpass right now, but the operations right before and right after this subpass also count as implicit "subpasses".

回憶到在一個render pass中的subpass們自動地關心image布局轉移。這些轉移通過subpass 依賴進行控制,which指定subpass之間的內存和執行依賴。我們現在只有1個subpass,但是subpass之前和之后的操作也隱式地算作“subpass”。

There are two built-in dependencies that take care of the transition at the start of the render pass and at the end of the render pass, but the former does not occur at the right time. It assumes that the transition occurs at the start of the pipeline, but we haven't acquired the image yet at that point! There are two ways to deal with this problem. We could change the waitStages for the imageAvailableSemaphore to VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT to ensure that the render passes don't begin until the image is available, or we can make the render pass wait for the VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT stage. I've decided to go with the second option here, because it's a good excuse to have a look at subpass dependencies and how they work.

在render pass開始前和結束后,有2個內置的依賴that處理轉移問題,但是前者發生的時間不對。它假設轉移發生在管道開始時,但是我們那時還沒有請求image呢!有2個解決此問題的方法。我們可以修改imageAvailableSemaphore 的waitStages 為VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT  to確保render pass在image可用后才開始,或者我們可以讓render pass等待VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 階段。我決定用第二個選項,因為那是個好理由to看看subpass及其是如何工作的。

Subpass dependencies are specified in VkSubpassDependency structs. Go to the createRenderPass function and add one:

Subpass依賴在VkSubpassDependency 結構體中指定。找到createRenderPass 函數,添加一個:

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

 

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.

前2個字段指定依賴的索引和依賴的subpass。特殊值VK_SUBPASS_EXTERNAL 指向render pass之前或之后的隱式subpass-基於它是被指定位srcSubpass 或dstSubpassdstSubpass 必須總數比srcSubpass 高to防止依賴圖的循環。

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

 

The next two fields specify the operations to wait on and the stages in which these operations occur. We need to wait for the swap chain to finish reading from the image before we can access it. This can be accomplished by waiting on the color attachment output stage itself.

接下來的2個字段指定要等待的操作及其發生的階段。我們需要等待交換鏈完成讀取image之后才能讀取它。這可以實現by通過等待顏色附件輸出階段。

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

 

The operations that should wait on this are in the color attachment stage and involve the reading and writing of the color attachment. These settings will prevent the transition from happening until it's actually necessary (and allowed): when we want to start writing colors to it.

應當等待的操作位於顏色附件階段,涉及對顏色附件的讀寫。這些設置會防止轉移的發生until它是有必要(且被允許)的:當我們想開始寫入顏色to顏色附件。

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

 

The VkRenderPassCreateInfo struct has two fields to specify an array of dependencies.

VkRenderPassCreateInfo 結構體有2個字段to指定依賴數組。

Presentation 呈現

The last step of drawing a frame is submitting the result back to the swap chain to have it eventually show up on the screen. Presentation is configured through a VkPresentInfoKHR structure at the end of the drawFrame function.

繪制一幀的最后步驟是,提交結果到交換鏈to讓它最終顯示到屏幕上。呈現通過VkPresentInfoKHR 結構體配置-在drawFrame函數的最后。

VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
 
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;

 

The first two parameters specify which semaphores to wait on before presentation can happen, just like VkSubmitInfo.

前2個參數指定,在呈現開始前,要等待哪個semaphore,就像VkSubmitInfo那樣。

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

 

The next two parameters specify the swap chains to present images to and the index of the image for each swap chain. This will almost always be a single one.

presentInfo.pResults = nullptr; // Optional

There is one last optional parameter called pResults. It allows you to specify an array of VkResult values to check for every individual swap chain if presentation was successful. It's not necessary if you're only using a single swap chain, because you can simply use the return value of the present function.

vkQueuePresentKHR(presentQueue, &presentInfo);

 

The vkQueuePresentKHR function submits the request to present an image to the swap chain. We'll add error handling for both vkAcquireNextImageKHR and vkQueuePresentKHR in the next chapter, because their failure does not necessarily mean that the program should terminate, unlike the functions we've seen so far.

vkQueuePresentKHR 函數提交請求to呈現image到交換鏈。我們將添加錯誤處理for vkAcquireNextImageKHR 和vkQueuePresentKHR 在下一章,因為它們的失敗不一定等於程序應當關閉,這與我們一直以來所見的函數不同。

If you did everything correctly up to this point, then you should now see something resembling the following when you run your program:

如果到這里為止你做對了所有事,那么現在當你運行你的程序,你應該會看到像下圖的東西:

 

 

Yay! Unfortunately, you'll see that when validation layers are enabled, the program crashes as soon as you close it. The messages printed to the terminal from debugCallback tell us why:

嘢!不幸的是,你會看到當驗證層啟用時,程序會在你關閉它時崩潰。打印到終端的消息from debugCallback 告訴我們原因:

 

 

Remember that all of the operations in drawFrame are asynchronous. That means that when we exit the loop in mainLoop, drawing and presentation operations may still be going on. Cleaning up resources while that is happening is a bad idea.

回憶到drawFrame 中所有的操作都是異步的。這意味着當我們退出mainLoop的循環時,繪制和呈現操作可能還在繼續。此時清理資源是個壞主意。

To fix that problem, we should wait for the logical device to finish operations before exiting mainLoop and destroying the window:

為修復這個問題,我們應當等待邏輯設備完成操作后再退出mainLoop 並銷毀窗口:

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

 

You can also wait for operations in a specific command queue to be finished with vkQueueWaitIdle. These functions can be used as a very rudimentary way to perform synchronization. You'll see that the program now exits without problems when closing the window.

你也可以等待一個特定的命令queue的操作完成withvkQueueWaitIdle。這些函數可以作為實施同步的初級方法。現在你可以看到關閉窗口時程序退出就不再有問題了。

Frames in flight 即時幀

If you run your application with validation layers enabled and you monitor the memory usage of your application, you may notice that it is slowly growing. The reason for this is that the application is rapidly submitting work in the drawFrame function, but doesn't actually check if any of it finishes. If the CPU is submitting work faster than the GPU can keep up with then the queue will slowly fill up with work. Worse, even, is that we are reusing the imageAvailableSemaphore and renderFinishedSemaphore for multiple frames at the same time.

如果你運行你的程序with啟用驗證層,你監控程序的內存占用,你可能注意到它在緩慢地增長。原因是程序在drawFrame 函數中瘋狂地提交工作,但是實際上不檢查它是否完成了。如果CPU提交速度比GPU能跟上的速度快,那么queue會慢慢地填滿工作量。更糟的是,我們會同時在多個幀中使用imageAvailableSemaphore 和renderFinishedSemaphore 。

The easy way to solve this is to wait for work to finish right after submitting it, for example by using vkQueueWaitIdle:

解決此問題的最簡單方法是,等待工作完成后再提交,例如使用vkQueueWaitIdle

void drawFrame() {
    ...
 
    vkQueuePresentKHR(presentQueue, &presentInfo);
 
    vkQueueWaitIdle(presentQueue);
}

 

However, we are likely not optimally using the GPU in this way, because the whole graphics pipeline is only used for one frame at a time right now. The stages that the current frame has already progressed through are idle and could already be used for a next frame. We will now extend our application to allow for multiple frames to be in-flight while still bounding the amount of work that piles up.

但是,我們這樣好像沒有以最佳的方式使用GPU,因為整個圖形管道只在一個時間里用於一幀上。當前幀已經處理過的階段,空閑着,但卻已經可以用於下一幀了。我們現在要擴展我們的程序,允許它同時計算多個幀,同時仍舊不堆積工作。

Start by adding a constant at the top of the program that defines how many frames should be processed concurrently:

開始,在程序開始處添加常量that定義應該同時處理多少個幀:

const int MAX_FRAMES_IN_FLIGHT = 2;

 

Each frame should have its own set of semaphores:

每個幀都應該有自己的semaphore集合

std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;

 

The createSemaphores function should be changed to create all of these:

createSemaphores 函數應當被修改為創建所有這些:

 1 void createSemaphores() {
 2     imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
 3     renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
 4  
 5     VkSemaphoreCreateInfo semaphoreInfo = {};
 6     semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
 7  
 8     for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
 9         if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
10             vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS) {
11  
12             throw std::runtime_error("failed to create semaphores for a frame!");
13         }
14 }

 

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);
    }
 
    ...
}

 

To use the right pair of semaphores every time, we need to keep track of the current frame. We will use a frame index for that purpose:

為了每次都使用正確的semaphore對,我們需要追蹤當前幀。我們用一個幀索引for此目的:

size_t currentFrame = 0;

 

The drawFrame function can now be modified to use the right objects:

drawFrame 函數現在可以被修改為使用正確的對象了:

 1 void drawFrame() {
 2     vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
 3  
 4     ...
 5  
 6     VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
 7  
 8     ...
 9  
10     VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
11  
12     ...
13 }

 

Of course, we shouldn't forget to advance to the next frame every time:

當然,我們不該忘記每次推進到下一幀:

void drawFrame() {
    ...
 
    currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}

 

By using the modulo (%) operator, we ensure that the frame index loops around after every MAX_FRAMES_IN_FLIGHT enqueued frames.

通過使用模(%)操作符,我們確保了幀索引每MAX_FRAMES_IN_FLIGHT次就循環回去。

Although we've now set up the required objects to facilitate processing of multiple frames simultaneously, we still don't actually prevent more than MAX_FRAMES_IN_FLIGHT from being submitted. Right now there is only GPU-GPU synchronization and no CPU-GPU synchronization going on to keep track of how the work is going. We may be using the frame #0 objects while frame #0 is still in-flight!

盡管我們為同時運行多個幀設置了必要的對象,我們還是沒有防止超過個MAX_FRAMES_IN_FLIGHT 幀被提交。現在只有GPU-GPU同步,沒有CPU-GPU同步to追蹤工作進展得如何。我們可以用幀#0的對象,同時幀#0還在運算!

To perform CPU-GPU synchronization, Vulkan offers a second type of synchronization primitive called fences. Fences are similar to semaphores in the sense that they can be signaled and waited for, but this time we actually wait for them in our own code. We'll first create a fence for each frame:

為實施CPU-GPU同步,Vulkan提供了另一種同步機制,即fence。Fence與semaphore類似,它們都能等待,並發信號,但這次我們是在自己的代碼中等它們。我們先為每個幀各創建一個fence:

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

 

I've decided to create the fences together with the semaphores and renamed createSemaphores to createSyncObjects:

我決定和semaphore一起創建fence,將createSemaphores 重命名為createSyncObjects

 1 void createSyncObjects() {
 2     imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
 3     renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
 4     inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
 5  
 6     VkSemaphoreCreateInfo semaphoreInfo = {};
 7     semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
 8  
 9     VkFenceCreateInfo fenceInfo = {};
10     fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
11  
12     for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
13         if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
14             vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS ||
15             vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {
16  
17             throw std::runtime_error("failed to create synchronization objects for a frame!");
18         }
19     }
20 }

 

The creation of fences (VkFence) is very similar to the creation of semaphores. Also make sure to clean up the fences:

VkFence)的創建與semaphore的創建類似。確保要清理它們:

1 void cleanup() {
2     for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
3         vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
4         vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
5         vkDestroyFence(device, inFlightFences[i], nullptr);
6     }
7  
8     ...
9 }

 

We will now change drawFrame to use the fences for synchronization. The vkQueueSubmit call includes an optional parameter to pass a fence that should be signaled when the command buffer finishes executing. We can use this to signal that a frame has finished.

我們現在要修改drawFrame  to使用fence來同步。vkQueueSubmit 調用包含了一個可選參數to傳入一個fence,它會在命令buffer執行完畢后發信號。我們可以用它獲知一幀已經結束了。

1 void drawFrame() {
2     ...
3  
4     if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
5         throw std::runtime_error("failed to submit draw command buffer!");
6     }
7     ...
8 }

 

Now the only thing remaining is to change the beginning of drawFrame to wait for the frame to be finished:

現在唯一剩下的事就是修改drawFrame 的開頭to等待一幀的完成:

void drawFrame() {
    vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, std::numeric_limits<uint64_t>::max());
    vkResetFences(device, 1, &inFlightFences[currentFrame]);
 
    ...
}

 

The vkWaitForFences function takes an array of fences and waits for either any or all of them to be signaled before returning. The VK_TRUE we pass here indicates that we want to wait for all fences, but in the case of a single one it obviously doesn't matter. Just like vkAcquireNextImageKHR this function also takes a timeout. Unlike the semaphores, we manually need to restore the fence to the unsignaled state by resetting it with the vkResetFences call.

vkWaitForFences 函數接收fence數組,等地啊任何一個或所有的fence收到信號,之后開始與信念。這里傳入VK_TRUE ,表示我們要等待所有的fence,但是只有1個fence的時候,它顯然就無所謂了。就像vkAcquireNextImageKHR ,這個函數也接受一個timeout。不像semaphore,我們需要手動地恢復fence到無信號狀態by重置它withvkResetFences 調用。

If you run the program now, you'll notice something strange. The application no longer seems to be rendering anything. With validation layers enabled, you'll see the following message:

如果你現在運行程序,你會注意到奇怪的事情。程序不再渲染任何東西了。啟用驗證層,你會看到下述信息:

 

 

That means that we're waiting for a fence that has not been submitted. The problem here is that, by default, fences are created in the unsignaled state. That means that vkWaitForFences will wait forever if we haven't used the fence before. To solve that, we can change the fence creation to initialize it in the signaled state as if we had rendered an initial frame that finished:

這意味着,我們在等待一個fence that沒被提交。這里的問題是,默認的,fence創建時的狀態是無信號狀態。這意味着,vkWaitForFences 會永遠等待if我們之前沒有使用過這個fence。為解決這個問題,我們可以修改fence創建過程to初始化它為有符號的狀態,就像我們已經渲染了一個最初的幀那樣。

void createSyncObjects() {
    ...
 
    VkFenceCreateInfo fenceInfo = {};
    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
 
    ...
}

 

The program should now work correctly and the memory leak should be gone! We've now implemented all the needed synchronization to ensure that there are no more than two frames of work enqueued. Note that it is fine for other parts of the code, like the final cleanup, to rely on more rough synchronization like vkDeviceWaitIdle. You should decide on which approach to use based on performance requirements.

現在程序應當正確地工作了,內存泄漏現象消失了!我們現在實現了所有需要的同步to確保不超過2個幀的工作在隊列里。注意,其他部分的代碼(例如清理)沒問題to依賴更加粗糙的同步例如vkDeviceWaitIdle。你應該覺得用哪個方式-基於性能需求。

To learn more about synchronization through examples, have a look at this extensive overview by Khronos.

想通過例子學習更多關於同步的知識,看看Khronos編寫的this extensive overview

Conclusion 總結

A little over 900 lines of code later, we've finally gotten to the stage of seeing something pop up on the screen! Bootstrapping a Vulkan program is definitely a lot of work, but the take-away message is that Vulkan gives you an immense amount of control through its explicitness. I recommend you to take some time now to reread the code and build a mental model of the purpose of all of the Vulkan objects in the program and how they relate to each other. We'll be building on top of that knowledge to extend the functionality of the program from this point on.

在寫了900多行代碼后,我們終於能夠看到有東西呈現在屏幕上了!Vulkan的引導程序絕對是大量的工作,但是捎帶的消息是Vulkan通過其explicitness給你巨大的控制。我推薦你花點世界重讀代碼,構建一個程序中Vulkan對象及其關系的思想模型。從此開始,我們將基於這些知識to擴展程序的功能。

In the next chapter we'll deal with one more small thing that is required for a well-behaved Vulkan program.

下一章,我們將處理一個良好的Vulkan程序需要的一件小事。

C++ code / Vertex shader / Fragment shader

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM