[译]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

 

 

posted @ 2019-07-09 20:00  BIT祝威  阅读(2043)  评论(0编辑  收藏  举报