這一節主要介紹創建幀緩沖(Framebuffer),創建命令池,創建命令緩存,和從文件加載 PNG 圖像數據,解碼為 RGBA 格式,并將像素數據暫存到 Vulkan 的 暫存緩沖區中。
一、創建幀緩沖
createFramebuffers
用于創建幀緩沖(Framebuffer)的核心部分,其功能是為交換鏈(Swap Chain)中的每個圖像視圖(Image View)創建對應的幀緩沖對象。
void HelloVK::initVulkan() {createInstance();createSurface();pickPhysicalDevice();createLogicalDeviceAndQueue();setupDebugMessenger();establishDisplaySizeIdentity();createSwapChain();createImageViews();createRenderPass();createDescriptorSetLayout();createGraphicsPipeline();createFramebuffers();...
}void HelloVK::createFramebuffers() {swapChainFramebuffers.resize(swapChainImageViews.size());for (size_t i = 0; i < swapChainImageViews.size(); i++) {VkImageView attachments[] = {swapChainImageViews[i]};VkFramebufferCreateInfo framebufferInfo{};framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;framebufferInfo.renderPass = renderPass;framebufferInfo.attachmentCount = 1;framebufferInfo.pAttachments = attachments;framebufferInfo.width = swapChainExtent.width;framebufferInfo.height = swapChainExtent.height;framebufferInfo.layers = 1;VK_CHECK(vkCreateFramebuffer(device, &framebufferInfo, nullptr,&swapChainFramebuffers[i]));}
}
1.1 調整幀緩沖數組大小
根據交換鏈圖像視圖的數量調整幀緩沖數組的大小,確保兩者一一對應。
swapChainFramebuffers.resize(swapChainImageViews.size());
1.2 遍歷交換鏈圖像視圖
對每個交換鏈圖像視圖創建對應的幀緩沖。
for (size_t i = 0; i < swapChainImageViews.size(); i++) {...}
1.3 定義附件
此處僅使用顏色附件(swapChainImageViews[i]
),即渲染結果將寫入交換鏈圖像。若需要深度、模板測試,需額外添加對應的圖像視圖。
VkImageView attachments[] = {swapChainImageViews[i]};
1.4 配置幀緩沖創建信息
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass; // 關聯的渲染流程
framebufferInfo.attachmentCount = 1; // 附件數量
framebufferInfo.pAttachments = attachments; // 附件數組指針
framebufferInfo.width = swapChainExtent.width; // 幀緩沖寬度
framebufferInfo.height = swapChainExtent.height; // 幀緩沖高度
framebufferInfo.layers = 1; // 層數(用于多視口/立體渲染)
關鍵參數
renderPass
:幀緩沖必須與渲染流程兼容(即附件格式、數量與渲染流程定義一致)。width
和height
:必須與交換鏈圖像尺寸一致,否則渲染結果可能無效。layers
:通常為 1,用于多圖層渲染(如 VR 立體視圖)。
1.5 創建幀緩沖
vkCreateFramebuffer
創建實際的 Vulkan 幀緩沖對象。
VK_CHECK(vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]));
二、創建命令池
創建一個命令池,用于分配和管理命令緩沖的內存。命令緩沖用于記錄 GPU 執行的渲染或計算指令。
命令池與特定的隊列族(Queue Family)綁定,確保命令緩沖被提交到正確的硬件隊列(如圖形隊列)。
void HelloVK::initVulkan() {createInstance();createSurface();pickPhysicalDevice();createLogicalDeviceAndQueue();setupDebugMessenger();establishDisplaySizeIdentity();createSwapChain();createImageViews();createRenderPass();createDescriptorSetLayout();createGraphicsPipeline();createFramebuffers();createCommandPool();...
}void HelloVK::createCommandPool() {QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);VkCommandPoolCreateInfo poolInfo{};poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool));
}
2.1 獲取隊列族索引 QueueFamilyIndices
findQueueFamilies
函數在前面已經詳細分析過,用于尋找物理設備支持的圖形隊列族和呈現隊列族。
2.2 配置命令池創建信息
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
sType
:指定結構體類型為命令池創建信息。flags
:控制命令池的行為,此處設置為VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
,允許單個命令緩沖通過vkResetCommandBuffer
重置,而無需重置整個命令池。queueFamilyIndex
:指定命令池關聯的隊列族索引(此處為圖形隊列族),確保命令緩沖提交到正確的隊列。
2.3 創建命令池
vkCreateCommandPool
調用 Vulkan API 創建命令池。
VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool));
三、創建命令緩存
從已創建的命令池(commandPool
)中分配一組主命令緩沖(Primary Command Buffers),用于記錄 GPU 執行的渲染指令。
使用 MAX_FRAMES_IN_FLIGHT
控制幀的并發數量(如雙緩沖或三緩沖),避免 CPU 和 GPU 之間的資源競爭。
void HelloVK::initVulkan() {createInstance();createSurface();pickPhysicalDevice();createLogicalDeviceAndQueue();setupDebugMessenger();establishDisplaySizeIdentity();createSwapChain();createImageViews();createRenderPass();createDescriptorSetLayout();createGraphicsPipeline();createFramebuffers();createCommandPool();createCommandBuffer();...
}void HelloVK::createCommandBuffer() {commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);VkCommandBufferAllocateInfo allocInfo{};allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;allocInfo.commandPool = commandPool;allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;allocInfo.commandBufferCount = commandBuffers.size();VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()));
}
3.1 調整命令緩沖數組大小
根據預定義的 MAX_FRAMES_IN_FLIGHT
(代碼內設置為 2)設置命令緩沖數組的大小。每個飛行的幀需要一個獨立的命令緩沖,確保 CPU 在錄制下一幀時不會覆蓋正在被 GPU 處理的幀數據。
commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
3.2 配置命令緩沖分配信息
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool; // 關聯的命令池
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; // 主命令緩沖級別
allocInfo.commandBufferCount = commandBuffers.size(); // 分配的緩沖數量
關鍵參數
commandPool
:指定從哪個命令池分配內存。命令池的類型需與后續提交的隊列兼容。level
:設置為VK_COMMAND_BUFFER_LEVEL_PRIMARY
,表示分配的是主命令緩沖(可直接提交到隊列)。
級別 | 用途 |
---|---|
VK_COMMAND_BUFFER_LEVEL_PRIMARY | 直接提交到隊列,可調用次級緩沖。適用于每幀的主要渲染指令。 |
VK_COMMAND_BUFFER_LEVEL_SECONDARY | 嵌入到主緩沖中,需通過主緩沖執行。適用于復用指令或并行錄制。 |
commandBufferCount
:需要分配的緩沖數量,與MAX_FRAMES_IN_FLIGHT
一致。
3.3 分配命令緩沖
vkAllocateCommandBuffers
從命令池中分配指定數量的命令緩沖。
VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()));
3.4 核心概念
3.4.1 主命令緩沖(Primary Command Buffer)
- 直接提交到隊列:主緩沖可獨立提交到隊列執行,通常包含完整的渲染指令序列。
- 次級緩沖的依賴:次級緩沖(
SECONDARY
)需通過vkCmdExecuteCommands
在主緩沖中調用,適用于復用指令或并行錄制。
3.4.2 幀并發控制(MAX_FRAMES_IN_FLIGHT)
- 雙緩沖/三緩沖:通過設置 2 或 3 個緩沖,允許 CPU 準備下一幀數據的同時,GPU 處理當前幀,避免資源沖突。
- 同步機制:需配合信號量(Semaphore)或柵欄(Fence)確保幀的正確同步。
3.4.3 命令池與緩沖的關系
- 內存管理:命令池負責底層內存分配,緩沖的生命周期由其所屬池控制。
- 重置行為:若命令池創建時指定了
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
,可單獨重置緩沖,否則需重置整個池。
3.5 完整工作流程示例
- 初始化階段:創建命令池 → 分配命令緩沖。
- 渲染循環:
- 等待前一幀完成(通過柵欄)。
- 重置命令緩沖 → 錄制渲染指令(如綁定管線、繪制調用)。
- 提交命令緩沖到隊列 → 呈現交換鏈圖像。
- 清理階段:銷毀命令池(自動釋放所有關聯的緩沖)。
四、獲取圖片
從文件加載 PNG 圖像數據,解碼為 RGBA 格式,并將像素數據暫存到 Vulkan 的 暫存緩沖區(Staging Buffer) 中,為后續將數據復制到 GPU 專用的紋理圖像做準備。
void HelloVK::initVulkan() {createInstance();createSurface();pickPhysicalDevice();createLogicalDeviceAndQueue();setupDebugMessenger();establishDisplaySizeIdentity();createSwapChain();createImageViews();createRenderPass();createDescriptorSetLayout();createGraphicsPipeline();createFramebuffers();createCommandPool();decodeImage();...
}void HelloVK::decodeImage() {std::vector<uint8_t> imageData = LoadBinaryFileToVector("texture.png",assetManager);if (imageData.size() == 0) {LOGE("Fail to load image.");return;}// Make sure we have an alpha channel, not all hardware can do linear filtering of RGB888.const int requiredChannels = 4;unsigned char* decodedData = stbi_load_from_memory(imageData.data(),imageData.size(), &textureWidth, &textureHeight, &textureChannels, requiredChannels);if (decodedData == nullptr) {LOGE("Fail to load image to memory, %s", stbi_failure_reason());return;}if (textureChannels != requiredChannels) {textureChannels = requiredChannels;}size_t imageSize = textureWidth * textureHeight * textureChannels;VkBufferCreateInfo createInfo{};createInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;createInfo.size = imageSize;createInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;VK_CHECK(vkCreateBuffer(device, &createInfo, nullptr, &stagingBuffer));VkMemoryRequirements memRequirements;vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);VkMemoryAllocateInfo allocInfo{};allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;allocInfo.allocationSize = memRequirements.size;allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory));VK_CHECK(vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0));uint8_t *data;VK_CHECK(vkMapMemory(device, stagingMemory, 0, memRequirements.size, 0,(void **)&data));memcpy(data, decodedData, imageSize);vkUnmapMemory(device, stagingMemory);stbi_image_free(decodedData);
}
4.1 加載圖像文件到內存
調用 LoadBinaryFileToVector
將文件內容讀取到字節數組 imageData
。若文件加載失敗(如路徑錯誤或文件不存在),記錄錯誤并退出。
std::vector<uint8_t> imageData = LoadBinaryFileToVector("texture.png", assetManager);
if (imageData.size() == 0) {LOGE("Fail to load image.");return;
}
4.2 解碼圖像數據
使用 STB 圖像庫中的函數 stbi_load_from_memory
從內存解碼圖像。
const int requiredChannels = 4;
unsigned char* decodedData = stbi_load_from_memory(imageData.data(), imageData.size(), &textureWidth, &textureHeight, &textureChannels, requiredChannels
);
if (decodedData == nullptr) {LOGE("Fail to load image to memory, %s", stbi_failure_reason());return;
}if (textureChannels != requiredChannels) {textureChannels = requiredChannels; // 強制設為 4
}
requiredChannels = 4
:強制解碼為 RGBA 格式(4 通道),確保兼容性(某些 GPU 對 RGB 格式的線性過濾支持不佳)。- 輸出參數:
textureWidth
、textureHeight
(圖像尺寸)、textureChannels
(實際解碼的通道數)。
4.3 計算圖像數據大小
size_t imageSize = textureWidth * textureHeight * textureChannels; // 總字節數
4.4 創建暫存緩沖區
暫存緩沖區作為 CPU 與 GPU 之間的數據傳輸橋梁。后續需通過傳輸命令將數據從此緩沖區復制到 GPU 專用的紋理圖像。
VkBufferCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
createInfo.size = imageSize; // 緩沖區大小
createInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; // 用途:傳輸源
createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // 獨占訪問模式
VK_CHECK(vkCreateBuffer(device, &createInfo, nullptr, &stagingBuffer));
關鍵參數
usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT
:標記為傳輸源。sharingMode = VK_SHARING_MODE_EXCLUSIVE
:緩沖區僅由圖形隊列獨占使用(無需多隊列共享)。
4.5 查詢內存需求
調用 vkGetBufferMemoryRequirements
獲取緩沖區的內存需求(大小、對齊、內存類型掩碼)。
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);
4.6 分配暫存內存
調用 vkAllocateMemory
用于分配暫存內存。
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory));
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
:內存可被 CPU 直接訪問(通過vkMapMemory
)。VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
:確保 CPU 與 GPU 內存訪問的自動一致性(無需手動刷新緩存)。
findMemoryType
用于找到符合特定緩沖區內存要求的內存堆的索引。Vulkan 將這些要求以位集的形式管理,在這種情況下通過 uint32_t
來表示。
uint32_t HelloVK::findMemoryType(uint32_t typeFilter,VkMemoryPropertyFlags properties) {VkPhysicalDeviceMemoryProperties memProperties;vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags &properties) == properties) {return i;}}assert(false); // failed to find suitable memory type!return -1;
}
- 調用
vkGetPhysicalDeviceMemoryProperties
獲取物理設備的內存信息,包括內存類型(memoryTypes
)和內存堆(memoryHeaps
)。 - 遍歷所有可用的內存類型(通常數量較小)。
typeFilter & (1 << i)
檢查第i
位是否為 1。若為真,表示內存類型i
是候選類型。(memProperties.memoryTypes[i].propertyFlags & properties) == properties
確保內存類型的屬性(propertyFlags
)包含properties
的所有標志。例如,若properties
要求內存同時是主機可見和一致的,則內存類型必須同時具備這兩個屬性。- 返回第一個滿足條件的內存類型索引。
- 若未找到合適內存類型,觸發斷言錯誤(調試模式下終止程序),并返回無效值 -1。
4.7 綁定內存到緩沖區
將分配的內存與緩沖區關聯,偏移量設為 0(從內存起始位置綁定)。
VK_CHECK(vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0));
4.8 映射內存并拷貝數據
uint8_t *data;
VK_CHECK(vkMapMemory(device, stagingMemory, 0, memRequirements.size, 0, (void **)&data));
memcpy(data, decodedData, imageSize);
vkUnmapMemory(device, stagingMemory);
vkMapMemory
將 GPU 內存映射到 CPU 可訪問的指針data
。memcpy
將解碼后的像素數據復制到映射的內存中。vkUnmapMemory
解除映射,確保數據寫入完成。
4.9 釋放解碼數據
STB 庫要求手動釋放解碼后的像素數據,避免內存泄漏。
stbi_image_free(decodedData);
4.10 關鍵概念
4.10.1 暫存緩沖區(Staging Buffer)
GPU 專用內存通常無法直接被 CPU 訪問,需通過暫存緩沖區中轉。
典型流程
- CPU 將數據寫入暫存緩沖區。
- 提交傳輸命令(如
vkCmdCopyBufferToImage
),將數據復制到設備本地紋理。 - 銷毀暫存資源。
4.10.2 內存一致性
HOST_COHERENT_BIT
:確保 CPU 寫入的數據立即可被 GPU 讀取(無緩存同步問題)。若無此標志需手動調用 vkFlushMappedMemoryRanges
和 vkInvalidateMappedMemoryRanges
刷新緩存。