初识Vulkun(13):渲染和呈现

08/09/2020

文章目录

  • 渲染和呈现三步骤
  • 同步
    • 栅栏和信号量(fences and Semaphores)
    • 创建Semaphores 信号量
  • 第一步:从交换链获取下一帧的图像
  • 第二步:执行命令缓冲区开始绘图
    • 等待图像准备完成的信号量
    • 发送绘图完成信号
  • 第三步:呈现
  • 窗口关闭报错
  • 知识拓展
    • 等待呈现完成
    • 多个信号量对应不同的帧(GPU-GPU同步)
    • 栅栏(Fences)
  • 总结

渲染和呈现三步骤

  • 从交换链获取一张图像
  • 使用该图像作为帧缓冲区中的附件执行命令缓冲区,进行绘制工作
  • 将绘制完的图像返回给交换链并呈现出来
    以上3个步骤可以异步执行,但是需要保证执行结果,所以需要同步机制

同步

由于Vulkan大多数操作都是多线程,如果某些步骤需要同步,即按照顺序执行,我们需要启动同步机制。

栅栏和信号量(fences and Semaphores)

  • 栅栏和信号量机制实现时间的同福,fences主要用于客户程序和Vulkan之间的同步,信号量完全交给Vulkan自己使用,用户不需要管理这个信号量
    它们都是对象,它们可以通过发出一个操作信号来进行协调操作,而另一个操作则等待栅栏或信号灯从无信号状态变为发信号状态。区别在于可以使用vkWaitForFences之类的调用从程序中访问fence状态,而信号量则不能。栅栏主要用于将应用程序本身与呈现操作同步,而信号量用于在命令队列之内或之间同步操作。我们要同步绘制命令和表示的队列操作,这使信号量最合适。

创建Semaphores 信号量

信号量用于在命令队列内或者跨命令队列同步操作,我们需要同步绘制与呈现的队列操作。
两个信号量,一个表示准备进行渲染,另一个表示渲染结束,准备呈现

VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;

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

第一步:从交换链获取下一帧的图像

//acquire an image from the swap chain
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, 
	VK_NULL_HANDLE, &imageIndex);

首先因为交换链是一个扩展,我们需要使用vkXXXKHR来使用扩展函数。

  • 第三参数:指定图像可用的超时时间(以纳秒为单位)。使用64位无符号整数的最大值可禁用超时
  • 接下来的两个参数指定使用的同步对象,当presentation引擎完成了图像的呈现后会使用该对象发起信号。这就是开始绘制的时间点。它可以指定一个信号量semaphore或者栅栏或者两者。出于目的性,我们会使用imageAvailableSemaphore。
  • 最后一个参数:最后一个参数指定一个变量,以输出已可用的交换链图像的索引。索引引用了我们swapChainImages数组中的VkImage。我们将使用该索引来选择正确的命令缓冲区

第二步:执行命令缓冲区开始绘图

基本逻辑:

  • 绘图之前,需要等待图像准备完成的信号,但只需要在图形管道的COLOR_ATTACHMENT_OUTPUT等待
  • 一旦绘图完成,发射渲染完成信号
//submit the command buffer
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

//参数指定在执行开始之前要等待的哪个信号量及要等待的通道的哪个阶段
VkSemaphore waitSemaphores[] = { imageAvailableSemaphore };
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; 	//等待图像状态变为available
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;

//应该提交命令缓冲区,它将我们刚获取的交换链图像做为颜色附件进行绑定。
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex]; //提交命令缓冲区

//当命令缓冲区执行结束向哪些信号量发出信号
VkSemaphore signalSemaphores[] = { renderFinishedSemaphore };
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;

//向图像队列提交命令缓冲区
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) 
{
	throw std::runtime_error("failed to submit draw command buffer!");
}

填写VkSubmitInfo信息:

  • 需要等待的信号量和完成的信号
  • 需要在流水线哪一个阶段等待:最终输出颜色时进行同步等待
  • 调用vkQueueSubmit提交command buffer 到图像队列中

等待图像准备完成的信号量

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;

前三个参数指定执行前要等待的信号量以及要在管道的哪个阶段中等待。我们要等到将颜色写入图像后再使用,因此我们要指定写入颜色附件的图形管道的阶段。这意味着从理论上讲,该实现已经可以开始执行我们的顶点着色器在图像还不能使用时候

发送绘图完成信号

//指定了当命令缓冲区执行结束向哪些信号量发出信号
VkSemaphore signalSemaphores[] = { renderFinishedSemaphore };
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;

signalSemaphoreCount和pSignalSemaphores参数指定在命令缓冲区完成执行后要发信号的信号量。在本例中,我们为此使用renderFinishedSemaphore。

vkQueueSubmit:当工作量大得多时,该函数将VkSubmitInfo结构的数组作为效率的参数。最后一个参数引用一个可选的fences,该fences将在命令缓冲区完成执行时发出信号。我们正在使用信号量进行同步,因此我们不需要fences

第三步:呈现

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

//需要等待的信号量renderFinished
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;

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

vkQueuePresentKHR(presentQueue,&presentInfo);
  • 需要配置呈现的属性,并传入该帧需要等待的信号量renderFinishedSemaphores
  • 显示交换链中的图像的索引
  • 它允许您指定VkResult值的数组,以检查演示是否成功,以检查每个交换链。如果只使用单个交换链,则没有必要,因为您可以简单地使用现函数的返回值
  • vkQueuePresentKHR 将最终在呈现队列的内容呈现在屏幕上,vkAcquireNextImageKHR和vkQueuePresentKHR添加错误处理,因为它们的失败并不一定意味着程序应该终止,这不同于到目前为止我们看到的功能

窗口关闭报错

窗口关闭时候,有可能绘图和呈现指令还在运转,导致程序问题

void mainLoop()
{
	while(!glfwWindowShouldClose(window))
	{
		glfwPollEvents();
		drawFram();
	}
	vkDeviceWaitIdle(device); //等待设备空闲
}

知识拓展

  • CPU与GPU同步:因为CPU速度快于GPU,这样会导致提交命令快速填满队列

等待呈现完成

vkQueuePresentKHR(presentQueue,&presentInfo);
vkQueueWaitIdle(presentQueue);

但意味着图像管道每次都只绘制一帧图像,其实某些管道是处于空闲状态的,完全可以加以利用于下一帧。

多个信号量对应不同的帧(GPU-GPU同步)

可以每次绘制两帧的内容,但依然没有解决CPU-GPU问题

const int MAX_FRAMES_IN_FLIGHT = 2;

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

部分修改

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

栅栏(Fences)

std::vector<VkFence> inFlightFences;
std::vector<VkFence> imagesInFlight;


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;					//Fence 一开始处于有信号状态

        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() {
		//等待fences这一帧呈现完成,才可以执行下一步
        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]);
        
		//命令缓冲区完成这一帧的绘制,标志fences在这一帧正在前往呈现的路上,不允许再被使用
        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;
    }
  • 解决MAX_FRAMES_IN_FLIGHT 数量高于交换链的数量引起索引混乱,所以我们需要跟踪交换链中每个正在使用的图像

总结

  • 准备同步机制
  • 获取交换链中图像的索引
  • 提交命令缓冲区,开始绘制
  • 绘制完成之后,呈现开始
  • 保证不超过两帧在队列中和这一帧意外的使用相同的图像。

你可能感兴趣的:(初识Vulkan,vulkan)