004.Vulkan绘制一个三角形3——初始化过程分析2
# Vulkan 绘制一个三角形3——初始化过程分析2
示例代码中定义了一个全局的 struct 用来保存我们需要初始化的 Swapchain 相关的对象:
struct VulkanSwapchainInfo {
VkSwapchainKHR swapchain_;
uint32_t swapchainLength_;
VkExtent2D displaySize_;
VkFormat displayFormat_;
// array of frame buffers and views
std::vector<VkImage> displayImages_;
std::vector<VkImageView> displayViews_;
std::vector<VkFramebuffer> framebuffers_;
};
2
3
4
5
6
7
8
9
10
11
12
# 1. VkImage、VkSwapchainKHR 与 VkSurfaceKHR 的关系
我们用"幻灯片投影仪"来做比喻:

(1999年,我在四川一个小镇上小学的时候,老师就是用这个家伙放教学幻灯片)
- VkImage 是【幻灯片/胶片】(真正的画布),这是你真正拿画笔(GPU 命令)涂写颜色的地方,它是一块内存区域。
- VkSwapchainKHR 是 【投影仪的转盘】(交换链),这个转盘上插着 2 张或 3 张幻灯片(VkImage)。它的作用是旋转:把一张空白的幻灯片转到你面前让你画,同时把另一张画好的幻灯片转到灯光口去投影。
- VkSurfaceKHR 是【投影幕布/墙上的投影区】(显示表面)它是一个位置或接口。它本身不存储图像数据,它只是定义了“图像最终要投射到哪里”。它代表了操作系统窗口(Window)在 Vulkan 眼里的样子。
# 1.1 核心职责:解决"供需矛盾"
显卡(GPU)和显示器(Display)是两个完全独立的硬件,它们的工作节奏往往不同步:
- 显卡:画得可能很快(200 FPS),也可能很慢(30 FPS)。
- 显示器:刷新率通常是固定的(比如 60Hz,即每 16.6ms 刷新一次)。
如果没有 Swapchain,显卡画完直接改写屏幕内存,就会出现画面撕裂(Tearing)(显示器读到一半,显卡就把下半截改了)。
VkSwapchainKHR 通过管理一个"图像队列"来解决这个问题:
- 持有图像:它向显存申请了 2 到 3 张 VkImage。
- 轮转调度:它决定哪张图正在被显示(Front Buffer),哪张图可以借给 GPU 去画(Back Buffer)。
- 垂直同步:它根据你的设置,决定是在显示器刷新的间隙“优雅地交换”,还是"暴力地交换"。
# 1.2 关键配置参数
在创建 Swapchain 时,你必须做出的三个最关键的决定:
- 图像数量 (Image Count) —— 转盘上有几个槽位?
- 双重缓冲 (Double Buffering, minImageCount = 2):一张给观众看,一张给画家画。如果画家画慢了,观众就要盯着旧图看很久(掉帧)。
- 三重缓冲 (Triple Buffering, minImageCount = 3):一张给观众看,一张画好了在排队,一张正在画。优势:平滑,减少卡顿感,是现代游戏的标配。
- 表面格式 (Surface Format) —— 胶片的材质,你需要告诉 Swapchain,这些胶片存什么样的数据。
- Color Format: 通常是 VK_FORMAT_B8G8R8A8_SRGB(蓝绿红透,sRGB 空间)。如果选错,画面颜色会怪异(比如变成蓝色调)。
- Color Space: 也就是色彩空间,通常是标准 sRGB。
呈现模式 (Present Mode) —— 转盘的旋转策略,这是 Swapchain 的灵魂,决定了游戏的流畅度和延迟
VK_PRESENT_MODE_FIFO_KHR (标准 V-Sync)
- 机制:严格排队。如果屏幕还没刷新完,GPU 必须停下来等(阻塞)。
- 效果:无撕裂,但有延迟,且如果 FPS 低于刷新率会感到明显的卡顿。
- 地位:这是唯一保证所有显卡都支持的模式。
VK_PRESENT_MODE_MAILBOX_KHR (高性能模式)
- 机制:邮箱策略。如果 GPU 画得太快,队列满了,它不会等,而是把队列里旧的那张图扔掉,换上最新的。
- 效果:无撕裂,延迟极低(总是显示最新的),画面最流畅。
- 代价:GPU 会一直满载运行,费电,风扇狂转。
VK_PRESENT_MODE_IMMEDIATE_KHR (电竞模式)
- 机制:不排队。画完立刻推给屏幕,不管屏幕是不是刷到一半。
- 效果:延迟最低,但有严重的画面撕裂。
# 1.3 工作流程:一帧的生命周期
有了 Swapchain,你的渲染循环(Render Loop)实际上就是向 Swapchain 借胶片、画胶片、还胶片的过程:
vkAcquireNextImageKHR (借)
- 你:“Swapchain,给我一张空闲的胶片(Image Index)。”
- Swapchain:“给,这是第 2 号胶片。但在我通知你之前(通过 Semaphore),你不能开始画,因为屏幕可能还在读它。”
vkQueueSubmit (画)
- 你:“GPU,往第 2 号胶片上画画!记得等 Semaphore 变绿了再动手。”
vkQueuePresentKHR (还)
- 你:“Swapchain,第 2 号胶片画好了,排进队列准备显示吧。
# 1.4 初始化示例程序
#include <vulkan/vulkan.h>
#include <vector>
#include <algorithm> // for std::clamp
#include <iostream>
// -----------------------------------------------------------------------------
// 辅助结构:用于传递队列族索引 (假设你之前已经获取到了)
// -----------------------------------------------------------------------------
struct QueueFamilyIndices {
uint32_t graphicsFamily;
uint32_t presentFamily;
};
// -----------------------------------------------------------------------------
// 1. 选择表面格式 (Surface Format)
// 目标:寻找 B8G8R8A8_SRGB + SRGB_NONLINEAR
// -----------------------------------------------------------------------------
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
// 如果找不到理想的,就直接返回列表里的第一个
return availableFormats[0];
}
// -----------------------------------------------------------------------------
// 2. 选择呈现模式 (Present Mode)
// 目标:首选 Mailbox (三重缓冲),次选 FIFO (垂直同步)
// -----------------------------------------------------------------------------
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
for (const auto& availablePresentMode : availablePresentModes) {
// Mailbox = 三重缓冲 (低延迟,无撕裂)
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
return availablePresentMode;
}
}
// FIFO = 强制垂直同步 (所有硬件都必须支持,作为保底)
return VK_PRESENT_MODE_FIFO_KHR;
}
// -----------------------------------------------------------------------------
// 3. 选择交换范围 (Extent / 分辨率)
// 目标:匹配窗口的实际大小
// -----------------------------------------------------------------------------
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, uint32_t windowWidth, uint32_t windowHeight) {
if (capabilities.currentExtent.width != 0xFFFFFFFF) {
// 大多数情况下,Surface 已经定义好了大小 (比如在 Windows 上)
return capabilities.currentExtent;
} else {
// 特殊情况 (如某些视网膜屏幕或 Wayland),允许我们自己设定大小
// 但必须限制在 minImageExtent 和 maxImageExtent 之间
VkExtent2D actualExtent = {windowWidth, windowHeight};
actualExtent.width = std::clamp(actualExtent.width,
capabilities.minImageExtent.width,
capabilities.maxImageExtent.width);
actualExtent.height = std::clamp(actualExtent.height,
capabilities.minImageExtent.height,
capabilities.maxImageExtent.height);
return actualExtent;
}
}
// -----------------------------------------------------------------------------
// 主函数:创建交换链
// -----------------------------------------------------------------------------
void createSwapchain(
VkPhysicalDevice physicalDevice,
VkDevice device,
VkSurfaceKHR surface,
uint32_t width, uint32_t height,
QueueFamilyIndices queueIndices,
VkSwapchainKHR& swapchain, // 输出:交换链句柄
std::vector<VkImage>& swapchainImages, // 输出:交换链里的 Image 句柄
VkFormat& swapchainImageFormat, // 输出:选中的格式
VkExtent2D& swapchainExtent // 输出:选中的大小
) {
// --- A. 查询支持情况 ---
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &capabilities);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, nullptr);
std::vector<VkSurfaceFormatKHR> formats(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, formats.data());
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, nullptr);
std::vector<VkPresentModeKHR> presentModes(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, presentModes.data());
// --- B. 做出选择 ---
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(formats);
VkPresentModeKHR presentMode = chooseSwapPresentMode(presentModes);
VkExtent2D extent = chooseSwapExtent(capabilities, width, height);
// 保存格式和大小供后续使用 (比如创建 RenderPass 时)
swapchainImageFormat = surfaceFormat.format;
swapchainExtent = extent;
// --- C. 决定缓冲区数量 (Image Count) ---
// 推荐:最小数量 + 1 (实现三重缓冲)
uint32_t imageCount = capabilities.minImageCount + 1;
// 检查不要超过硬件的最大限制 (0 表示没有最大限制)
if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) {
imageCount = capabilities.maxImageCount;
}
// --- D. 填充创建信息结构体 ---
VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1; // 除非是 VR 应用,否则总是 1
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // 我们要往上画颜色
// --- E. 处理跨队列族共享 (重要!) ---
// 如果图形队列和呈现队列不是同一个 (很少见,但存在),需要设置共享模式
uint32_t queueFamilyIndices[] = {queueIndices.graphicsFamily, queueIndices.presentFamily};
if (queueIndices.graphicsFamily != queueIndices.presentFamily) {
// 并发模式:图像可以在两个队列族之间使用,不需要显式转移所有权
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
// 独占模式:性能更好,但如果跨族需要显式 Barrier
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = 0; // 可选
createInfo.pQueueFamilyIndices = nullptr; // 可选
}
// --- F. 其他设置 ---
// 预变换:比如手机横屏时,显卡需要把图像旋转90度。
// 这里我们设置 currentTransform,表示“按照当前 Surface 的要求来”,不做额外操作。
createInfo.preTransform = capabilities.currentTransform;
// 混合 alpha:忽略窗口的 Alpha 通道 (不透明)
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE; // 裁剪掉被其他窗口遮挡的像素 (性能优化)
// 旧交换链:在窗口大小改变重建 Swapchain 时需要传入旧的,这里初始化传 NULL
createInfo.oldSwapchain = VK_NULL_HANDLE;
// --- G. 创建交换链 ---
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapchain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swapchain!");
}
std::cout << "Swapchain created successfully!" << std::endl;
// --- H. 获取 Swapchain 里的 VkImage 句柄 ---
// 注意:我们只创建了 Swapchain,但里面的 Image 是驱动自动创建的,我们需要把句柄拿出来
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, nullptr);
swapchainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapchain, &imageCount, swapchainImages.data());
std::cout << "Retrieved " << imageCount << " swapchain images." << std::endl;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# 2. VkSwapchainKHR 与 BlastBufferQueue 的关系
如果你对 Android 显示系统很熟悉,你应该马上反应到,VkSwapchainKHR 与 BlastBufferQueue 功能非常类似。那他们之间到底是什么关系呢?
简单来说:VkSwapchainKHR 是“前端 API”,而 BlastBufferQueue 是现代 Android 系统中负责支撑它的“后端实现机制”。它们之间的关系可以概括为:VkSwapchainKHR 在底层通过 ANativeWindow 接口,间接地由 BlastBufferQueue 来驱动和管理 buffer。
# 2.1 层级关系
我们可以把它们想象成一个“洋葱”结构,从外(应用层)到内(系统层):
- 最上层:Vulkan App
你在这里调用 vkCreateSwapchainKHR,vkQueuePresentKHR。你只关心 VkImage。
- 中间层:Vulkan Driver (ICD)
这是高通/ARM/Mali 的驱动程序。它负责实现 VkSwapchainKHR。它并不知道 BlastBufferQueue 的存在,它只认标准接口 ANativeWindow。
- 接口层:ANativeWindow (libnativewindow)
这是 Android 的通用窗口抽象接口。驱动程序调用 ANativeWindow_dequeueBuffer 来拿内存,调用 ANativeWindow_queueBuffer 来提交内存。
- 实现层:BlastBufferQueue (BBQ) —— [这里是关键]
在 Android 12+ (以及部分 Android 11) 的现代架构中,ANativeWindow 的背后就是 BlastBufferQueue。它取代了旧版直接使用 BufferQueue 的方式。它负责连接 App 和 SurfaceFlinger。
- 合成层:SurfaceFlinger
最终负责把画面合成并推到屏幕上的系统服务。
# 2.2 它们是如何协作的?
当你在 Vulkan 中进行渲染循环时,实际发生了这样的传导:
创建 Swapchain (vkCreateSwapchainKHR)
- Vulkan 层:你请求创建交换链。
- Android 层:驱动程序通过 ANativeWindow 配置 buffer 的大小、格式。此时 BlastBufferQueue 在后台负责分配真正的显存块(GraphicBuffer / AHardwareBuffer)。
获取图像 (vkAcquireNextImageKHR)
- Vulkan 层:你请求一张空闲图片。
- 桥接:驱动程序调用 ANativeWindow_dequeueBuffer。
- BBQ 层:BlastBufferQueue 从它的空闲队列里拿出一个 AHardwareBuffer 给驱动。
- 结果:驱动把这个 AHardwareBuffer 包装成 VkImage 给你的 App。
- 呈现图像 (vkQueuePresentKHR)
- Vulkan 层:你提交画好的图。
- 桥接:驱动程序调用 ANativeWindow_queueBuffer。
- BBQ 层:BlastBufferQueue 接收到这个 buffer,它不会像以前那样直接塞给 SurfaceFlinger,而是创建一个事务 (Transaction)。
- BLAST 协议:BBQ 利用 Android 的 BLAST (Basic Layering Architecture for Surfaces and Transactions) 机制,将 buffer 的更新与窗口属性的更新打包,原子性地发送给 SurfaceFlinger。