iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

渲染與GPU編程系列 第 13

Day 12|Vulkan 渲染流程:Command Buffer 與 Render Pass

  • 分享至 

  • xImage
  •  

目標:把「CPU 錄命令 → 交給 GPU 執行 → 顯示到螢幕」這條路走通,先做到把畫面清成你想要的顏色
心法口訣:先準備容器(Command Pool/Buffer)→ 準備畫布(Render Pass/Framebuffer)→ 錄指令(Begin/End)→ 同步提交(Semaphore/Fence)


1)大的流程地圖(先有鳥瞰不迷路)

每一幀會重複這件事:

Acquire(拿一張可畫的交換鏈影像)
  ↓
Record(在 Command Buffer 裡錄:開始 Render Pass → 清畫面/畫 → 結束)
  ↓
Submit(把 Command Buffer 交給圖形 Queue;用 Semaphore/Fence 同步)
  ↓
Present(把這張影像送去顯示)
  • Command Buffer:像錄影帶,裡面是一連串 GPU 指令。
  • Render Pass/Framebuffer:像畫布的規格書(用哪個附件、如何載入/儲存、尺寸…)。
  • Semaphore/Fence:交通號誌,避免你畫到一半就去呈現,或拿到還沒準備好的影像。

2)Command Pool & Command Buffer:GPU 指令的「容器」

  • Command Pool:分配 Command Buffer 的「記憶體池」。一個 Queue Family 至少準備一個 Pool。

  • Command Buffer:真的裝指令的盒子。

    • Primary:可以被提交(submit)。
    • Secondary:不能獨立提交,但能被 primary 呼叫(適合多執行緒分工)。

新手建議:先做一個 primary 就好;等會畫很多東西、要多執行緒,再用 secondary。


3)Render Pass / Framebuffer:定義「這一回合怎麼畫」

  • Render Pass:定義這次繪製要用的附件(顏色/深度)、載入/儲存策略(loadOp/storeOp)初末布局(layout),以及(需要的話)Subpass 與依賴。
  • Framebuffer:把「具體要用哪張貼圖(image view)」綁到該 Render Pass(一張交換鏈影像 = 一個 framebuffer)。

直覺:Render Pass 是「規格書」,Framebuffer 是「把規格書配到實體紙張」。


4)同步物件:兩種號誌別搞混

  • SemaphoreGPU↔GPU 的同步(例如:等「取得交換鏈影像」這件事完成,再開始畫;畫完再通知「可以 Present」)。
  • FenceCPU↔GPU 的同步(例如:CPU 要等上一幀真的畫完,才能重複使用該幀的資源)。

5)把 Day 11 的骨架「補上繪製必要件」

下面程式碼濃縮了最小必需:Command Pool/Buffer、Render Pass、Framebuffer、同步物件、每幀流程
假設你已經有:device/phys/graphicsQ/presentQ/swapchain/swapFormat/swapExtent/swapViews(來自 Day 11)。

// 只示意核心關鍵;錯誤處理/銷毀請照樣對應加上
VkCommandPool       cmdPool;
std::vector<VkCommandBuffer> cmdBufs;
VkRenderPass        renderPass;
std::vector<VkFramebuffer> framebuffers;
VkSemaphore         semImage, semRender;
VkFence             inFlight;

// 1) Command Pool + Buffers(每個 swap image 一個 cmd buf)
void createCommands() {
    VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
    pci.queueFamilyIndex = qGraphics;                         // 圖形 queue family
    pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
    VK_CHECK(vkCreateCommandPool(device, &pci, nullptr, &cmdPool));

    cmdBufs.resize(swapViews.size());
    VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
    ai.commandPool = cmdPool;
    ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    ai.commandBufferCount = (uint32_t)cmdBufs.size();
    VK_CHECK(vkAllocateCommandBuffers(device, &ai, cmdBufs.data()));
}

// 2) Render Pass(單色附件,進來就清除、出去要用來顯示)
void createRenderPass() {
    VkAttachmentDescription color{};
    color.format         = swapFormat;
    color.samples        = VK_SAMPLE_COUNT_1_BIT;
    color.loadOp         = VK_ATTACHMENT_LOAD_OP_CLEAR;      // 進來先清
    color.storeOp        = VK_ATTACHMENT_STORE_OP_STORE;     // 出去要存(給 Present)
    color.initialLayout  = VK_IMAGE_LAYOUT_UNDEFINED;        // 不需保留先前內容
    color.finalLayout    = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;  // 給呈現使用

    VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};

    VkSubpassDescription sub{};
    sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
    sub.colorAttachmentCount = 1;
    sub.pColorAttachments = &colorRef;

    // 最簡單的外部依賴,確保顏色輸出階段轉換正確
    VkSubpassDependency dep{};
    dep.srcSubpass = VK_SUBPASS_EXTERNAL;
    dep.dstSubpass = 0;
    dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

    VkRenderPassCreateInfo ci{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
    ci.attachmentCount = 1; ci.pAttachments = &color;
    ci.subpassCount = 1; ci.pSubpasses = &sub;
    ci.dependencyCount = 1; ci.pDependencies = &dep;

    VK_CHECK(vkCreateRenderPass(device, &ci, nullptr, &renderPass));
}

// 3) Framebuffer(每張 swap image 一個)
void createFramebuffers() {
    framebuffers.resize(swapViews.size());
    for (size_t i = 0; i < swapViews.size(); i++) {
        VkImageView atts[] = { swapViews[i] };
        VkFramebufferCreateInfo fci{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
        fci.renderPass = renderPass;
        fci.attachmentCount = 1; fci.pAttachments = atts;
        fci.width = swapExtent.width; fci.height = swapExtent.height; fci.layers = 1;
        VK_CHECK(vkCreateFramebuffer(device, &fci, nullptr, &framebuffers[i]));
    }
}

// 4) 同步物件(兩個 semaphore + 一個 fence)
void createSync() {
    VkSemaphoreCreateInfo sci{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
    VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &semImage));   // 圖像可用
    VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &semRender));  // 渲染完成

    VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
    fci.flags = VK_FENCE_CREATE_SIGNALED_BIT;                        // 一開始設為已完成,避免第一幀卡住
    VK_CHECK(vkCreateFence(device, &fci, nullptr, &inFlight));
}

// 5) 錄指令:把畫面清成清藍色(每張 cmd buf 對應它的 framebuffer)
void recordCmd(uint32_t imageIndex) {
    VkCommandBuffer cmd = cmdBufs[imageIndex];
    VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
    bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    VK_CHECK(vkBeginCommandBuffer(cmd, &bi));

    VkClearValue clearColor; clearColor.color = {{0.05f, 0.25f, 0.9f, 1.0f}};
    VkRenderPassBeginInfo rbi{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
    rbi.renderPass = renderPass;
    rbi.framebuffer = framebuffers[imageIndex];
    rbi.renderArea = {{0,0}, swapExtent};
    rbi.clearValueCount = 1; rbi.pClearValues = &clearColor;

    vkCmdBeginRenderPass(cmd, &rbi, VK_SUBPASS_CONTENTS_INLINE);
    // 這裡未來會放:vkCmdBindPipeline / vkCmdBindVertexBuffers / vkCmdDraw...
    vkCmdEndRenderPass(cmd);

    VK_CHECK(vkEndCommandBuffer(cmd));
}

6)每幀主流程(Acquire → Submit → Present)

void drawFrame() {
    // 1) 等上一幀結束(避免覆寫還在用的資源)
    VK_CHECK(vkWaitForFences(device, 1, &inFlight, VK_TRUE, UINT64_MAX));
    VK_CHECK(vkResetFences(device, 1, &inFlight));

    // 2) 取一張可畫的交換鏈影像(GPU 取得後會發出 semImage)
    uint32_t imageIndex;
    VK_CHECK(vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, semImage, VK_NULL_HANDLE, &imageIndex));

    // 3) 錄製對應那張影像的指令
    VK_CHECK(vkResetCommandBuffer(cmdBufs[imageIndex], 0));
    recordCmd(imageIndex);

    // 4) 提交給圖形 Queue:等 semImage → 執行 cmd → 發出 semRender
    VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
    si.waitSemaphoreCount = 1; si.pWaitSemaphores = &semImage; si.pWaitDstStageMask = &waitStage;
    si.commandBufferCount = 1; si.pCommandBuffers = &cmdBufs[imageIndex];
    si.signalSemaphoreCount = 1; si.pSignalSemaphores = &semRender;

    VK_CHECK(vkQueueSubmit(graphicsQ, 1, &si, inFlight));

    // 5) Present:等 semRender(代表這張影像畫好了)
    VkPresentInfoKHR pi{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
    pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &semRender;
    pi.swapchainCount = 1; pi.pSwapchains = &swapchain; pi.pImageIndices = &imageIndex;

    VK_CHECK(vkQueuePresentKHR(presentQ, &pi));
}

把這個 drawFrame() 放進你的主迴圈(while(!glfwWindowShouldClose(window)) { glfwPollEvents(); drawFrame(); }),你就會看到畫面被清成藍色,而且幀幀穩定同步

render blue


7)Dynamic Rendering:更簡短的「免 Render Pass」寫法(Vulkan 1.3)

如果你的驅動支援 VK_KHR_dynamic_rendering(Vulkan 1.3 核心),可以不用建立 Render Pass/Framebuffer,直接在 Command Buffer 裡宣告要渲染的附件:
更多請看這


8)小心的地方(最常見 8 個坑)

  1. 忘記等上一幀(Fence) → 你在覆寫還在用的 Command Buffer / UBO。
  2. Semaphore 用錯方向 → Present 等到的是 semImage(錯),應該等 semRender。
  3. Framebuffer 尺寸不合 → swapchain 重建後沒同步重建 framebuffer。
  4. 沒重建 swapchain → 視窗大小改變或 VK_ERROR_OUT_OF_DATE_KHR 時要重建(先 vkDeviceWaitIdle,再銷毀舊的)。
  5. Layout/Barrier 缺失 → 雖然清畫面時 Render Pass 幫你轉了,但真正畫圖時記得正確的 image layout 與 pipeline barrier。
  6. 沒有 Validation Layers → 小錯變大錯。開啟它,你會獲得「講人話」的錯誤訊息。
  7. 同一幀重複使用同一個 cmd buf → 若要多個錄製策略,請 reset 後再錄或多分配幾個。
  8. 等待 Stage 設錯pWaitDstStageMask 對顏色輸出常用 COLOR_ATTACHMENT_OUTPUT_BIT,不是隨便填 TOP_OF_PIPE_BIT

9)你現在可以玩什麼(10 分鐘 3 個練習)

  1. 改清除顏色:把 clearColor 改成別的 RGB。
  2. 加深度附件:建立一張 depth image + image view,把 Render Pass/Framebuffer 加上深度;感受多一個附件的流程。
  3. 畫三角形預告:在 vkCmdBeginRenderPass 後加上 Pipeline/Vertex Buffer/vkCmdDraw(Day 13 我們一起做)。

10)一句話總結

Vulkan 的「畫一幀」= 拿影像 → 錄命令(Begin/End Render Pass)→ 提交(Semaphore/Fence)→ 呈現
Command Buffer 當成錄影帶、Render Pass/Framebuffer 當成畫布規格、Semaphore/Fence 當成號誌,你就能用可控、可預測的方式把每一幀穩穩地送上螢幕。
做到「清畫面」已經走過最難的初始化門檻了:下一步,我們把第一個三角形畫出來!


上一篇
Day 11|Vulkan 專案骨架與初始化(零基礎友善版|程式碼精簡)
系列文
渲染與GPU編程13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言