目標:把「CPU 錄命令 → 交給 GPU 執行 → 顯示到螢幕」這條路走通,先做到把畫面清成你想要的顏色。
心法口訣:先準備容器(Command Pool/Buffer)→ 準備畫布(Render Pass/Framebuffer)→ 錄指令(Begin/End)→ 同步提交(Semaphore/Fence)。
每一幀會重複這件事:
Acquire(拿一張可畫的交換鏈影像)
↓
Record(在 Command Buffer 裡錄:開始 Render Pass → 清畫面/畫 → 結束)
↓
Submit(把 Command Buffer 交給圖形 Queue;用 Semaphore/Fence 同步)
↓
Present(把這張影像送去顯示)
Command Pool:分配 Command Buffer 的「記憶體池」。一個 Queue Family 至少準備一個 Pool。
Command Buffer:真的裝指令的盒子。
新手建議:先做一個 primary 就好;等會畫很多東西、要多執行緒,再用 secondary。
直覺:Render Pass 是「規格書」,Framebuffer 是「把規格書配到實體紙張」。
下面程式碼濃縮了最小必需: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 = ⊂
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));
}
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(); }
),你就會看到畫面被清成藍色,而且幀幀穩定同步。
如果你的驅動支援 VK_KHR_dynamic_rendering(Vulkan 1.3 核心),可以不用建立 Render Pass/Framebuffer,直接在 Command Buffer 裡宣告要渲染的附件:
更多請看這
VK_ERROR_OUT_OF_DATE_KHR
時要重建(先 vkDeviceWaitIdle
,再銷毀舊的)。pWaitDstStageMask
對顏色輸出常用 COLOR_ATTACHMENT_OUTPUT_BIT
,不是隨便填 TOP_OF_PIPE_BIT
。clearColor
改成別的 RGB。vkCmdBeginRenderPass
後加上 Pipeline/Vertex Buffer/vkCmdDraw
(Day 13 我們一起做)。Vulkan 的「畫一幀」= 拿影像 → 錄命令(Begin/End Render Pass)→ 提交(Semaphore/Fence)→ 呈現。
把 Command Buffer 當成錄影帶、Render Pass/Framebuffer 當成畫布規格、Semaphore/Fence 當成號誌,你就能用可控、可預測的方式把每一幀穩穩地送上螢幕。
做到「清畫面」已經走過最難的初始化門檻了:下一步,我們把第一個三角形畫出來!