一、渲染循環核心
while (!glfwWindowShouldClose(window)) {glfwPollEvents();helloTriangleApp.drawFrame(); // 繪制幀}
在 Vulkan 中渲染幀包含一組常見的步驟
-
等待前一幀完成(vkWaitForFences)
-
從交換鏈獲取圖像(vkAcquireNextImageKHR)
-
錄制一個命令緩沖區將場景繪制到圖像上(vkBeginCommandBuffer、vkEndCommandBuffer)
-
提交已記錄的命令緩沖區(vkQueueSubmit)
-
呈現交換鏈圖像(vkQueuePresentKHR)
這就是一個drawFrame函數要做的主要工作。
二、同步
GPU執行需顯式同步。
例如下面這些事件:
-
從交換鏈獲取圖像(vkAcquireNextImageKHR)
-
執行在獲取的圖像上繪制的命令
-
將該圖像呈現到屏幕上進行呈現,將其返回到交換鏈(vkQueuePresentKHR)
信號量
用于控制GPU上的同步操作。
VkCommandBuffer A, B = ... // 錄制命令緩沖
VkSemaphore S = ... // 創建一個信號// 當操作 A 完成時,將發出信號量 S 的信號,而操作 B 將不會啟動,直到 S 發出信號
vkQueueSubmit(work: A, signal: S, wait: None)// 在操作 B 開始執行后,信號量 S 將自動重置回未發出信號的狀態,從而允許再次使用它。
vkQueueSubmit(work: B, signal: None, wait: S)
柵欄
它是用于對 CPU(也稱為主機)上的執行進行排序的。如果主機需要知道 GPU 何時完成某件事,我們會使用柵欄。
例如:截屏操作
VkCommandBuffer A = ... // 記錄包含傳輸操作的命令緩沖區
VkFence F = ... // 創建圍欄對象// 將命令緩沖區A提交到隊列,立即開始執行,并在完成時發出圍欄F的信號
vkQueueSubmit(work: A, fence: F)vkWaitForFence(F) // 阻塞當前執行線程,直到命令緩沖區A完成執行save_screenshot_to_disk() // 必須等待傳輸操作完成后才能執行
三、繪制過程
創建同步對象
void HelloTriangle::createSyncObjects() {// 為每一幀預分配同步對象的存儲imageAvailableSemaphores.resize(MAX_CONCURRENT_FRAMES);renderFinishedSemaphores.resize(MAX_CONCURRENT_FRAMES);inFlightFences.resize(MAX_CONCURRENT_FRAMES);// 設置信號量創建信息結構體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_CONCURRENT_FRAMES; 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 HelloTriangle::recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {// 1. 開始記錄命令緩沖區VkCommandBufferBeginInfo beginInfo{};beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {throw std::runtime_error("Failed to begin recording command buffer!");}// 2. 設置渲染通道信息VkRenderPassBeginInfo renderPassInfo{};renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;renderPassInfo.renderPass = renderPass; // 指定使用的渲染通道renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; // 指定目標幀緩沖renderPassInfo.renderArea.offset = {0, 0}; // 渲染區域起點renderPassInfo.renderArea.extent = swapChainExtent; // 渲染區域大小// 設置清除顏色 (黑色)VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};renderPassInfo.clearValueCount = 1;renderPassInfo.pClearValues = &clearColor;// 3. 開始渲染通道,使用內聯子通道內容vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);// 4. 綁定圖形管線vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);// 5. 設置視口 (Viewport) - 定義裁剪空間坐標到幀緩沖坐標的映射VkViewport viewport{};viewport.x = 0.0f;viewport.y = 0.0f;viewport.width = static_cast<float>(swapChainExtent.width);viewport.height = static_cast<float>(swapChainExtent.height);viewport.minDepth = 0.0f;viewport.maxDepth = 1.0f;vkCmdSetViewport(commandBuffer, 0, 1, &viewport);// 6. 設置剪刀區域 (Scissor) - 定義實際渲染的區域VkRect2D scissor{};scissor.offset = {0, 0};scissor.extent = swapChainExtent;vkCmdSetScissor(commandBuffer, 0, 1, &scissor);// 7. 執行繪制命令 - 繪制3個頂點,組成一個三角形vkCmdDraw(commandBuffer, 3, 1, 0, 0);// 8. 結束渲染通道vkCmdEndRenderPass(commandBuffer);// 9. 結束命令緩沖區記錄if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {throw std::runtime_error("Failed to record command buffer!");}
}
繪制幀
void HelloTriangle::drawFrame() {// 1. 等待當前幀的圍欄被信號化,確保上一幀的渲染操作已完成vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);// 重置圍欄狀態,為下一幀做準備vkResetFences(device, 1, &inFlightFences[currentFrame]);// 2. 從交換鏈獲取下一可用圖像,使用當前幀的"圖像可用"信號量uint32_t imageIndex;vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);// 3. 重置并記錄當前幀的命令緩沖區vkResetCommandBuffer(commandBuffers[currentFrame], 0);recordCommandBuffer(commandBuffers[currentFrame], imageIndex);// 4. 設置提交信息,定義命令執行的依賴關系VkSubmitInfo submitInfo{};submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;// 等待"圖像可用"信號量,確保在圖像可用后再開始渲染VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};submitInfo.waitSemaphoreCount = 1;submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentFrame];submitInfo.pWaitDstStageMask = waitStages;// 指定要執行的命令緩沖區submitInfo.commandBufferCount = 1;submitInfo.pCommandBuffers = &commandBuffers[currentFrame];// 當命令執行完成時,信號化"渲染完成"信號量submitInfo.signalSemaphoreCount = 1;submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame];// 提交命令到圖形隊列,并關聯圍欄以跟蹤完成狀態if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {throw std::runtime_error("Failed to submit draw command buffer!");}// 5. 設置呈現信息,準備將渲染結果呈現到屏幕VkPresentInfoKHR presentInfo{};presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;// 等待"渲染完成"信號量,確保渲染完成后再呈現presentInfo.waitSemaphoreCount = 1;presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];// 指定要呈現的交換鏈和圖像索引presentInfo.swapchainCount = 1;presentInfo.pSwapchains = &swapChain;presentInfo.pImageIndices = &imageIndex;// 提交呈現請求到呈現隊列vkQueuePresentKHR(presentQueue, &presentInfo);// 6. 更新當前幀索引,循環使用預分配的同步對象currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
}
完整的初始化過程關系圖
呈現效果:
四、新增成員變量和成員函數
新增/更新的成員變量
// 全局變量
const int MAX_CONCURRENT_FRAMES = 3; // 定義繪制的最大并行幀數// 類成員變量更新
std::vector<VkCommandBuffer> commandBuffers; // 為每一幀創建一個命令緩沖
std::vector<VkSemaphore> imageAvailableSemaphores; // 為每一幀創建一個圖片信號量
std::vector<VkSemaphore> renderFinishedSemaphores; // 為每一幀創建渲染器完成信號量
std::vector<VkFence> inFlightFences; // 為每一幀創建一個柵欄
新增成員函數
void createCommandBuffers();void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex);void createSyncObjects();void cleanupSwapChain()void drawFrame();
上一節和本節代碼保存為分支 04_render_present