教程 12 - Vulkan 逻辑设备和交换链
上一篇:Vulkan 表面和物理设备 | 下一篇:待定 | 返回目录
📚 快速导航
目录 (点击展开/折叠)🎯 本章目标
通过本教程,你将学会:
| 🎯 目标 | 📝 描述 | ✅ 成果 |
|---|---|---|
| 逻辑设备 | 从物理设备创建逻辑设备 | 掌握设备和队列创建 |
| 交换链 | 创建和管理交换链 | 实现双缓冲/三缓冲 |
| 图像管理 | 创建图像和图像视图 | 理解图像-内存绑定 |
| 深度附件 | 配置深度缓冲 | 实现深度测试 |
| 重建机制 | 处理窗口大小变化 | 实现交换链重建 |
📖 教程概述
在上一篇教程中,我们成功创建了 Vulkan 表面并选择了合适的物理设备。本篇教程将继续深入,我们将学习如何:
- 从选定的物理设备创建逻辑设备
- 获取设备队列句柄
- 理解交换链(Swapchain)的概念和作用
- 创建和配置 Vulkan 交换链
- 管理交换链图像和深度附件
- 实现交换链的重建机制
🔧 核心概念
逻辑设备(Logical Device)
物理设备代表系统中的 GPU 硬件,而逻辑设备是应用程序与物理设备之间的接口。它是对物理设备功能的抽象,允许我们:
- 创建和管理队列
- 分配内存
- 创建各种 Vulkan 对象(图像、缓冲区、管线等)
- 提交命令到 GPU
一个物理设备可以创建多个逻辑设备,每个逻辑设备可以请求不同的特性和队列。
设备队列(Device Queues)
队列是应用程序向 GPU 提交工作的通道。Vulkan 中有几种类型的队列:
- 图形队列(Graphics Queue): 执行图形渲染命令
- 呈现队列(Present Queue): 将渲染结果显示到屏幕
- 计算队列(Compute Queue): 执行计算着色器
- 传输队列(Transfer Queue): 专门用于内存传输操作
不同的队列族可能支持不同的操作,我们需要根据应用需求选择合适的队列。
交换链(Swapchain)
交换链是 Vulkan 渲染到屏幕的关键组件。它本质上是一个图像队列,包含:
- 多个图像缓冲区: 通常 2-3 个(双缓冲或三缓冲)
- 呈现模式: 控制图像如何显示到屏幕
- 表面格式: 定义颜色空间和像素格式
工作流程:
- 从交换链获取一个可用图像
- 渲染到该图像
- 将图像提交回交换链进行呈现
- 交换链将图像显示到屏幕
这种机制避免了屏幕撕裂,实现了流畅的画面切换。
代码实现
1. 数据结构定义
首先,让我们看看本篇涉及的核心数据结构:
// engine/src/renderer/vulkan/vulkan_types.inl
typedef struct vulkan_device {
VkPhysicalDevice physical_device; // 物理设备
VkDevice logical_device; // 逻辑设备
vulkan_swapchain_support_info swapchain_support; // 交换链支持信息
// 队列族索引
i32 graphics_queue_index;
i32 present_queue_index;
i32 transfer_queue_index;
// 队列句柄
VkQueue graphics_queue;
VkQueue present_queue;
VkQueue transfer_queue;
// 设备属性
VkPhysicalDeviceProperties properties;
VkPhysicalDeviceFeatures features;
VkPhysicalDeviceMemoryProperties memory;
VkFormat depth_format; // 深度格式
} vulkan_device;
typedef struct vulkan_image {
VkImage handle; // 图像句柄
VkDeviceMemory memory; // 图像内存
VkImageView view; // 图像视图
u32 width; // 宽度
u32 height; // 高度
} vulkan_image;
typedef struct vulkan_swapchain {
VkSurfaceFormatKHR image_format; // 图像格式
u8 max_frames_in_flight; // 最大同时进行的帧数
VkSwapchainKHR handle; // 交换链句柄
u32 image_count; // 图像数量
VkImage* images; // 图像数组
VkImageView* views; // 图像视图数组
vulkan_image depth_attachment; // 深度附件
} vulkan_swapchain;
设计要点:
vulkan_device: 封装了逻辑设备及其相关队列和属性vulkan_image: 通用图像结构,包含图像、内存和视图vulkan_swapchain: 包含交换链的所有资源,包括深度附件
2. 逻辑设备创建
// engine/src/renderer/vulkan/vulkan_device.c
b8 vulkan_device_create(vulkan_context* context) {
if (!select_physical_device(context)) {
return FALSE;
}
KINFO("Creating logical device...");
// 步骤1: 确定需要创建的队列
// 注意:对于共享的队列索引,不创建额外的队列
b8 present_shares_graphics_queue =
context->device.graphics_queue_index == context->device.present_queue_index;
b8 transfer_shares_graphics_queue =
context->device.graphics_queue_index == context->device.transfer_queue_index;
u32 index_count = 1;
if (!present_shares_graphics_queue) {
index_count++;
}
if (!transfer_shares_graphics_queue) {
index_count++;
}
u32 indices[index_count];
u8 index = 0;
indices[index++] = context->device.graphics_queue_index;
if (!present_shares_graphics_queue) {
indices[index++] = context->device.present_queue_index;
}
if (!transfer_shares_graphics_queue) {
indices[index++] = context->device.transfer_queue_index;
}
// 步骤2: 配置队列创建信息
VkDeviceQueueCreateInfo queue_create_infos[index_count];
for (u32 i = 0; i < index_count; ++i) {
queue_create_infos[i].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_create_infos[i].queueFamilyIndex = indices[i];
queue_create_infos[i].queueCount = 1;
queue_create_infos[i].flags = 0;
queue_create_infos[i].pNext = 0;
f32 queue_priority = 1.0f;
queue_create_infos[i].pQueuePriorities = &queue_priority;
}
// 步骤3: 请求设备特性
VkPhysicalDeviceFeatures device_features = {};
device_features.samplerAnisotropy = VK_TRUE; // 请求各向异性过滤
// 步骤4: 配置设备创建信息
VkDeviceCreateInfo device_create_info = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
device_create_info.queueCreateInfoCount = index_count;
device_create_info.pQueueCreateInfos = queue_create_infos;
device_create_info.pEnabledFeatures = &device_features;
device_create_info.enabledExtensionCount = 1;
const char* extension_names = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
device_create_info.ppEnabledExtensionNames = &extension_names;
// 已弃用,传递0
device_create_info.enabledLayerCount = 0;
device_create_info.ppEnabledLayerNames = 0;
// 步骤5: 创建逻辑设备
VK_CHECK(vkCreateDevice(
context->device.physical_device,
&device_create_info,
context->allocator,
&context->device.logical_device));
KINFO("Logical device created.");
// 步骤6: 获取队列句柄
vkGetDeviceQueue(
context->device.logical_device,
context->device.graphics_queue_index,
0,
&context->device.graphics_queue);
vkGetDeviceQueue(
context->device.logical_device,
context->device.present_queue_index,
0,
&context->device.present_queue);
vkGetDeviceQueue(
context->device.logical_device,
context->device.transfer_queue_index,
0,
&context->device.transfer_queue);
KINFO("Queues obtained.");
return TRUE;
}
关键步骤解析:
- 队列去重: 检查队列索引是否重复,避免创建重复队列
- 队列优先级: 设置为 1.0(最高优先级)
- 设备特性: 启用各向异性过滤,提升纹理质量
- 扩展启用: 必须启用
VK_KHR_swapchain扩展 - 获取队列: 使用
vkGetDeviceQueue获取队列句柄
3. 交换链创建
交换链的创建是一个复杂的过程,涉及多个配置选项:
// engine/src/renderer/vulkan/vulkan_swapchain.c
void create(vulkan_context* context, u32 width, u32 height, vulkan_swapchain* swapchain) {
VkExtent2D swapchain_extent = {width, height};
swapchain->max_frames_in_flight = 2; // 双缓冲
// 步骤1: 选择表面格式
b8 found = FALSE;
for (u32 i = 0; i < context->device.swapchain_support.format_count; ++i) {
VkSurfaceFormatKHR format = context->device.swapchain_support.formats[i];
// 首选格式: BGRA8 UNORM + sRGB 非线性色彩空间
if (format.format == VK_FORMAT_B8G8R8A8_UNORM &&
format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
swapchain->image_format = format;
found = TRUE;
break;
}
}
if (!found) {
// 如果首选格式不可用,使用第一个可用格式
swapchain->image_format = context->device.swapchain_support.formats[0];
}
// 步骤2: 选择呈现模式
VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; // 默认使用 FIFO
for (u32 i = 0; i < context->device.swapchain_support.present_mode_count; ++i) {
VkPresentModeKHR mode = context->device.swapchain_support.present_modes[i];
if (mode == VK_PRESENT_MODE_MAILBOX_KHR) {
present_mode = mode; // 首选 MAILBOX 模式
break;
}
}
// 步骤3: 重新查询交换链支持(确保最新数据)
vulkan_device_query_swapchain_support(
context->device.physical_device,
context->surface,
&context->device.swapchain_support);
// 步骤4: 确定交换链范围
if (context->device.swapchain_support.capabilities.currentExtent.width != UINT32_MAX) {
swapchain_extent = context->device.swapchain_support.capabilities.currentExtent;
}
// 将范围限制在GPU支持的范围内
VkExtent2D min = context->device.swapchain_support.capabilities.minImageExtent;
VkExtent2D max = context->device.swapchain_support.capabilities.maxImageExtent;
swapchain_extent.width = KCLAMP(swapchain_extent.width, min.width, max.width);
swapchain_extent.height = KCLAMP(swapchain_extent.height, min.height, max.height);
// 步骤5: 确定图像数量
u32 image_count = context->device.swapchain_support.capabilities.minImageCount + 1;
if (context->device.swapchain_support.capabilities.maxImageCount > 0 &&
image_count > context->device.swapchain_support.capabilities.maxImageCount) {
image_count = context->device.swapchain_support.capabilities.maxImageCount;
}
// 步骤6: 配置交换链创建信息
VkSwapchainCreateInfoKHR swapchain_create_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
swapchain_create_info.surface = context->surface;
swapchain_create_info.minImageCount = image_count;
swapchain_create_info.imageFormat = swapchain->image_format.format;
swapchain_create_info.imageColorSpace = swapchain->image_format.colorSpace;
swapchain_create_info.imageExtent = swapchain_extent;
swapchain_create_info.imageArrayLayers = 1;
swapchain_create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
// 步骤7: 配置队列共享模式
if (context->device.graphics_queue_index != context->device.present_queue_index) {
// 图形队列和呈现队列不同,使用并发模式
u32 queueFamilyIndices[] = {
(u32)context->device.graphics_queue_index,
(u32)context->device.present_queue_index
};
swapchain_create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
swapchain_create_info.queueFamilyIndexCount = 2;
swapchain_create_info.pQueueFamilyIndices = queueFamilyIndices;
} else {
// 图形队列和呈现队列相同,使用独占模式
swapchain_create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchain_create_info.queueFamilyIndexCount = 0;
swapchain_create_info.pQueueFamilyIndices = 0;
}
swapchain_create_info.preTransform = context->device.swapchain_support.capabilities.currentTransform;
swapchain_create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
swapchain_create_info.presentMode = present_mode;
swapchain_create_info.clipped = VK_TRUE;
swapchain_create_info.oldSwapchain = 0;
// 步骤8: 创建交换链
VK_CHECK(vkCreateSwapchainKHR(
context->device.logical_device,
&swapchain_create_info,
context->allocator,
&swapchain->handle));
// 初始化当前帧索引
context->current_frame = 0;
// 步骤9: 获取交换链图像
swapchain->image_count = 0;
VK_CHECK(vkGetSwapchainImagesKHR(
context->device.logical_device,
swapchain->handle,
&swapchain->image_count,
0));
if (!swapchain->images) {
swapchain->images = (VkImage*)kallocate(
sizeof(VkImage) * swapchain->image_count,
MEMORY_TAG_RENDERER);
}
if (!swapchain->views) {
swapchain->views = (VkImageView*)kallocate(
sizeof(VkImageView) * swapchain->image_count,
MEMORY_TAG_RENDERER);
}
VK_CHECK(vkGetSwapchainImagesKHR(
context->device.logical_device,
swapchain->handle,
&swapchain->image_count,
swapchain->images));
// 步骤10: 创建图像视图
for (u32 i = 0; i < swapchain->image_count; ++i) {
VkImageViewCreateInfo view_info = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = swapchain->images[i];
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = swapchain->image_format.format;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.subresourceRange.baseMipLevel = 0;
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.baseArrayLayer = 0;
view_info.subresourceRange.layerCount = 1;
VK_CHECK(vkCreateImageView(
context->device.logical_device,
&view_info,
context->allocator,
&swapchain->views[i]));
}
// 步骤11: 检测深度格式
if (!vulkan_device_detect_depth_format(&context->device)) {
context->device.depth_format = VK_FORMAT_UNDEFINED;
KFATAL("Failed to find a supported format!");
}
// 步骤12: 创建深度图像
vulkan_image_create(
context,
VK_IMAGE_TYPE_2D,
swapchain_extent.width,
swapchain_extent.height,
context->device.depth_format,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
TRUE,
VK_IMAGE_ASPECT_DEPTH_BIT,
&swapchain->depth_attachment);
KINFO("Swapchain created successfully.");
}
配置选项详解:
表面格式选择
// 首选: VK_FORMAT_B8G8R8A8_UNORM + VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
// - BGRA8: 每通道8位,常见且高效
// - sRGB: 符合人眼感知的颜色空间
呈现模式选择
-
VK_PRESENT_MODE_FIFO_KHR (默认):
- 类似垂直同步
- 图像队列满时阻塞
- 保证不撕裂
-
VK_PRESENT_MODE_MAILBOX_KHR (首选):
- 三缓冲
- 队列满时替换旧图像
- 低延迟,无撕裂
-
VK_PRESENT_MODE_IMMEDIATE_KHR:
- 立即呈现
- 可能撕裂
- 最低延迟
队列共享模式
// 独占模式 (VK_SHARING_MODE_EXCLUSIVE):
// - 性能最佳
// - 图像只能被一个队列族访问
// - 适用于图形队列和呈现队列相同的情况
// 并发模式 (VK_SHARING_MODE_CONCURRENT):
// - 性能略低
// - 图像可以被多个队列族访问,无需显式转移所有权
// - 适用于图形队列和呈现队列不同的情况
4. 图像和图像视图管理
Vulkan 中的图像需要配套的图像视图才能被着色器访问:
// engine/src/renderer/vulkan/vulkan_image.c
void vulkan_image_create(
vulkan_context* context,
VkImageType image_type,
u32 width,
u32 height,
VkFormat format,
VkImageTiling tiling,
VkImageUsageFlags usage,
VkMemoryPropertyFlags memory_flags,
b32 create_view,
VkImageAspectFlags view_aspect_flags,
vulkan_image* out_image) {
// 保存参数
out_image->width = width;
out_image->height = height;
// 步骤1: 配置图像创建信息
VkImageCreateInfo image_create_info = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
image_create_info.imageType = VK_IMAGE_TYPE_2D;
image_create_info.extent.width = width;
image_create_info.extent.height = height;
image_create_info.extent.depth = 1;
image_create_info.mipLevels = 4; // TODO: 支持 mipmap
image_create_info.arrayLayers = 1;
image_create_info.format = format;
image_create_info.tiling = tiling;
image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_create_info.usage = usage;
image_create_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// 步骤2: 创建图像
VK_CHECK(vkCreateImage(
context->device.logical_device,
&image_create_info,
context->allocator,
&out_image->handle));
// 步骤3: 查询内存需求
VkMemoryRequirements memory_requirements;
vkGetImageMemoryRequirements(
context->device.logical_device,
out_image->handle,
&memory_requirements);
// 步骤4: 查找合适的内存类型
i32 memory_type = context->find_memory_index(
memory_requirements.memoryTypeBits,
memory_flags);
if (memory_type == -1) {
KERROR("Required memory type not found. Image not valid.");
}
// 步骤5: 分配内存
VkMemoryAllocateInfo memory_allocate_info = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
memory_allocate_info.allocationSize = memory_requirements.size;
memory_allocate_info.memoryTypeIndex = memory_type;
VK_CHECK(vkAllocateMemory(
context->device.logical_device,
&memory_allocate_info,
context->allocator,
&out_image->memory));
// 步骤6: 绑定内存到图像
VK_CHECK(vkBindImageMemory(
context->device.logical_device,
out_image->handle,
out_image->memory,
0));
// 步骤7: 创建图像视图(如果需要)
if (create_view) {
out_image->view = 0;
vulkan_image_view_create(context, format, out_image, view_aspect_flags);
}
}
void vulkan_image_view_create(
vulkan_context* context,
VkFormat format,
vulkan_image* image,
VkImageAspectFlags aspect_flags) {
VkImageViewCreateInfo view_create_info = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_create_info.image = image->handle;
view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_create_info.format = format;
view_create_info.subresourceRange.aspectMask = aspect_flags;
view_create_info.subresourceRange.baseMipLevel = 0;
view_create_info.subresourceRange.levelCount = 1;
view_create_info.subresourceRange.baseArrayLayer = 0;
view_create_info.subresourceRange.layerCount = 1;
VK_CHECK(vkCreateImageView(
context->device.logical_device,
&view_create_info,
context->allocator,
&image->view));
}
图像创建步骤:
- 创建图像对象: 定义图像的基本属性
- 查询内存需求: GPU 告诉我们需要多少内存
- 分配内存: 从合适的内存堆分配
- 绑定内存: 将内存绑定到图像
- 创建视图: 定义如何访问图像数据
图像视图的作用:
图像视图是图像的"解释器",它定义了:
- 访问哪个 aspect(颜色、深度、模板)
- 访问哪些 mipmap 级别
- 访问哪些数组层
- 数据格式
5. 深度格式检测
深度缓冲对于 3D 渲染至关重要,我们需要找到 GPU 支持的深度格式:
// engine/src/renderer/vulkan/vulkan_device.c
b8 vulkan_device_detect_depth_format(vulkan_device* device) {
// 候选格式列表(按优先级排序)
const u64 candidate_count = 3;
VkFormat candidates[3] = {
VK_FORMAT_D32_SFLOAT, // 32位浮点深度
VK_FORMAT_D32_SFLOAT_S8_UINT, // 32位浮点深度 + 8位模板
VK_FORMAT_D24_UNORM_S8_UINT // 24位深度 + 8位模板
};
u32 flags = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
for (u64 i = 0; i < candidate_count; ++i) {
VkFormatProperties properties;
vkGetPhysicalDeviceFormatProperties(
device->physical_device,
candidates[i],
&properties);
if ((properties.linearTilingFeatures & flags) == flags) {
device->depth_format = candidates[i];
return TRUE;
} else if ((properties.optimalTilingFeatures & flags) == flags) {
device->depth_format = candidates[i];
return TRUE;
}
}
return FALSE;
}
6. 交换链重建
窗口大小改变时,需要重建交换链:
// engine/src/renderer/vulkan/vulkan_swapchain.c
void vulkan_swapchain_recreate(
vulkan_context* context,
u32 width,
u32 height,
vulkan_swapchain* swapchain) {
// 销毁旧交换链
destroy(context, swapchain);
// 创建新交换链
create(context, width, height, swapchain);
}
void destroy(vulkan_context* context, vulkan_swapchain* swapchain) {
// 销毁深度附件
vulkan_image_destroy(context, &swapchain->depth_attachment);
// 销毁图像视图(不销毁图像,因为图像归交换链所有)
for (u32 i = 0; i < swapchain->image_count; ++i) {
vkDestroyImageView(
context->device.logical_device,
swapchain->views[i],
context->allocator);
}
// 销毁交换链
vkDestroySwapchainKHR(
context->device.logical_device,
swapchain->handle,
context->allocator);
}
7. 获取和呈现图像
交换链的核心操作:
// engine/src/renderer/vulkan/vulkan_swapchain.c
b8 vulkan_swapchain_acquire_next_image_index(
vulkan_context* context,
vulkan_swapchain* swapchain,
u64 timeout_ns,
VkSemaphore image_available_semaphore,
VkFence fence,
u32* out_image_index) {
VkResult result = vkAcquireNextImageKHR(
context->device.logical_device,
swapchain->handle,
timeout_ns,
image_available_semaphore,
fence,
out_image_index);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
// 交换链过期,需要重建
vulkan_swapchain_recreate(
context,
context->framebuffer_width,
context->framebuffer_height,
swapchain);
return FALSE;
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
KFATAL("Failed to acquire swapchain image!");
return FALSE;
}
return TRUE;
}
void vulkan_swapchain_present(
vulkan_context* context,
vulkan_swapchain* swapchain,
VkQueue graphics_queue,
VkQueue present_queue,
VkSemaphore render_complete_semaphore,
u32 present_image_index) {
// 配置呈现信息
VkPresentInfoKHR present_info = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
present_info.waitSemaphoreCount = 1;
present_info.pWaitSemaphores = &render_complete_semaphore;
present_info.swapchainCount = 1;
present_info.pSwapchains = &swapchain->handle;
present_info.pImageIndices = &present_image_index;
present_info.pResults = 0;
VkResult result = vkQueuePresentKHR(present_queue, &present_info);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
// 交换链过期或次优,重建
vulkan_swapchain_recreate(
context,
context->framebuffer_width,
context->framebuffer_height,
swapchain);
} else if (result != VK_SUCCESS) {
KFATAL("Failed to present swap chain image!");
}
}
同步机制:
- Semaphore: GPU 间同步,用于协调获取图像和渲染完成
- Fence: CPU-GPU 同步,用于等待 GPU 操作完成
- timeout_ns: 等待可用图像的超时时间
8. 后端集成
在渲染后端中使用交换链:
// engine/src/renderer/vulkan/vulkan_backend.c
#include "vulkan_swapchain.h"
b8 vulkan_renderer_backend_initialize(
renderer_backend* backend,
const char* application_name,
struct platform_state* plat_state) {
// ... 创建实例、表面、设备 ...
// 创建交换链
vulkan_swapchain_create(
&context,
context.framebuffer_width,
context.framebuffer_height,
&context.swapchain);
KINFO("Vulkan renderer initialized successfully.");
return TRUE;
}
void vulkan_renderer_backend_shutdown(renderer_backend* backend) {
// 按照创建的相反顺序销毁
// 销毁交换链
vulkan_swapchain_destroy(&context, &context.swapchain);
// 销毁设备
KDEBUG("Destroying Vulkan device...");
vulkan_device_destroy(&context);
// 销毁表面
KDEBUG("Destroying Vulkan surface...");
if (context.surface) {
vkDestroySurfaceKHR(context.instance, context.surface, context.allocator);
context.surface = 0;
}
// ... 其他清理工作 ...
}
架构图解
逻辑设备和队列关系
┌─────────────────────────────────────────────────────┐
│ Physical Device (GPU) │
├─────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ Logical Device │ │
│ ├───────────────────────────────────────────┤ │
│ │ │ │
│ │ Queue Family 0 (Graphics + Present) │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Graphics │ │ Present │ │ │
│ │ │ Queue │ │ Queue │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ Queue Family 1 (Transfer) │ │
│ │ ┌─────────────┐ │ │
│ │ │ Transfer │ │ │
│ │ │ Queue │ │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ │ Extensions: │ │
│ │ - VK_KHR_swapchain │ │
│ │ │ │
│ │ Features: │ │
│ │ - samplerAnisotropy │ │
│ └───────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
交换链工作流程
┌────────────────────────────────────────────────────────────┐
│ Swapchain Workflow │
└────────────────────────────────────────────────────────────┘
Application GPU Display
│ │ │
│ Acquire Image │ │
├─────────────────────>│ │
│ │ │
│ Image 0 Available │ │
│<─────────────────────┤ │
│ │ │
│ Render to Image 0 │ │
├─────────────────────>│ │
│ │ │
│ Submit for Present │ │
├─────────────────────>│ │
│ │ │
│ │ Display Image 0 │
│ ├─────────────────────>│
│ │ │
│ Acquire Image │ │
├─────────────────────>│ │
│ │ │
│ Image 1 Available │ │
│<─────────────────────┤ │
│ │ │
│ Render to Image 1 │ │
├─────────────────────>│ │
│ │ │
交换链图像和视图结构
┌─────────────────────────────────────────────┐
│ Vulkan Swapchain │
├─────────────────────────────────────────────┤
│ │
│ VkSwapchainKHR handle │
│ max_frames_in_flight: 2 │
│ image_format: BGRA8_UNORM + sRGB │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Images (owned by swapchain) │ │
│ ├───────────────────────────────────┤ │
│ │ VkImage images[0] ────────┐ │ │
│ │ VkImage images[1] ────┐ │ │ │
│ │ VkImage images[2] ─┐ │ │ │ │
│ └─────────────────────┼──┼───┼─────┘ │
│ │ │ │ │
│ ┌─────────────────────┼──┼───┼─────┐ │
│ │ Image Views ▼ ▼ ▼ │ │
│ ├───────────────────────────────────┤ │
│ │ VkImageView views[0] │ │
│ │ VkImageView views[1] │ │
│ │ VkImageView views[2] │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Depth Attachment │ │
│ ├───────────────────────────────────┤ │
│ │ VkImage handle │ │
│ │ VkDeviceMemory memory │ │
│ │ VkImageView view │ │
│ │ format: D32_SFLOAT │ │
│ └───────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
图像内存绑定
┌────────────────────────────────────────────┐
│ Vulkan Image Creation │
└────────────────────────────────────────────┘
Step 1: Create Image
┌──────────────────┐
│ VkImage │ (Image object, no memory)
└──────────────────┘
Step 2: Query Memory Requirements
┌──────────────────┐
│ Size: 4 MB │
│ Alignment: 256 │
│ Memory Types: 3 │
└──────────────────┘
Step 3: Allocate Memory
┌──────────────────────────┐
│ VkDeviceMemory │
│ (4 MB on GPU) │
└──────────────────────────┘
Step 4: Bind Memory
┌──────────────────┐ ┌──────────────────────────┐
│ VkImage │─────>│ VkDeviceMemory │
└──────────────────┘ └──────────────────────────┘
Step 5: Create Image View
┌──────────────────┐ ┌──────────────────┐
│ VkImageView │─────>│ VkImage │
│ (Interpretation) │ └──────────────────┘
└──────────────────┘
│
└─ Format: BGRA8
└─ Aspect: COLOR
└─ Mip Levels: 0-1
└─ Array Layers: 0-1
使用示例
完整的渲染循环
// 在渲染循环中使用交换链
// 1. 获取下一个可用图像
u32 image_index;
b8 result = vulkan_swapchain_acquire_next_image_index(
&context,
&context.swapchain,
UINT64_MAX, // 无限等待
image_available_semaphore,
0, // 不使用 fence
&image_index);
if (!result) {
// 交换链重建,跳过此帧
return TRUE;
}
// 2. 等待前一帧完成
vkWaitForFences(
context.device.logical_device,
1,
&in_flight_fences[current_frame],
VK_TRUE,
UINT64_MAX);
// 3. 重置 fence
vkResetFences(
context.device.logical_device,
1,
&in_flight_fences[current_frame]);
// 4. 记录命令缓冲区
// ... record rendering commands ...
// 5. 提交渲染命令
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
VkSemaphore wait_semaphores[] = {image_available_semaphore};
VkPipelineStageFlags wait_stages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submit_info.waitSemaphoreCount = 1;
submit_info.pWaitSemaphores = wait_semaphores;
submit_info.pWaitDstStageMask = wait_stages;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffers[image_index];
VkSemaphore signal_semaphores[] = {render_complete_semaphore};
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = signal_semaphores;
VK_CHECK(vkQueueSubmit(
context.device.graphics_queue,
1,
&submit_info,
in_flight_fences[current_frame]));
// 6. 呈现图像
vulkan_swapchain_present(
&context,
&context.swapchain,
context.device.graphics_queue,
context.device.present_queue,
render_complete_semaphore,
image_index);
// 7. 更新当前帧
current_frame = (current_frame + 1) % MAX_FRAMES_IN_FLIGHT;
处理窗口调整
void on_window_resize(u32 width, u32 height) {
// 等待设备空闲
vkDeviceWaitIdle(context.device.logical_device);
// 更新帧缓冲区大小
context.framebuffer_width = width;
context.framebuffer_height = height;
// 重建交换链
vulkan_swapchain_recreate(
&context,
width,
height,
&context.swapchain);
// 重建依赖于交换链的资源
// - Framebuffers
// - Render passes
// - Pipeline viewports
}
最佳实践
1. 队列选择策略
// 优先选择专用队列
// 专用传输队列通常性能更好,因为可以与图形操作并行
// 理想配置:
// - Graphics Queue (支持图形、计算、传输)
// - Present Queue (通常与 Graphics 相同)
// - Dedicated Transfer Queue (仅支持传输,可并行)
// 次优配置:
// - Graphics Queue (支持图形、呈现、传输)
2. 图像数量选择
// 双缓冲: 2个图像
// - 优点: 内存占用小
// - 缺点: CPU 需要等待 GPU 完成
// 三缓冲: 3个图像
// - 优点: CPU 和 GPU 可以更好地并行
// - 缺点: 略高的内存占用和延迟
// 推荐: minImageCount + 1
u32 image_count = capabilities.minImageCount + 1;
if (capabilities.maxImageCount > 0 &&
image_count > capabilities.maxImageCount) {
image_count = capabilities.maxImageCount;
}
3. 呈现模式权衡
// FIFO (垂直同步):
// - 保证无撕裂
// - 可能输入延迟
// - 最低功耗
// 适用于: 移动设备、电池供电设备
// MAILBOX (三缓冲):
// - 无撕裂
// - 低延迟
// - 高功耗
// 适用于: 高性能桌面游戏
// IMMEDIATE:
// - 最低延迟
// - 可能撕裂
// - 中等功耗
// 适用于: 竞技游戏、基准测试
4. 交换链重建时机
// 必须重建的情况:
// 1. VK_ERROR_OUT_OF_DATE_KHR: 表面属性改变,交换链不兼容
// 2. 窗口大小改变
// 可选重建的情况:
// 1. VK_SUBOPTIMAL_KHR: 交换链仍可使用,但不是最优配置
// 2. 表面格式或呈现模式偏好改变
// 建议: 在帧边界重建,避免中断渲染
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
// 立即重建
recreate_swapchain();
return; // 跳过当前帧
} else if (result == VK_SUBOPTIMAL_KHR) {
// 可以等到帧结束后重建
needs_recreate = TRUE;
}
5. 内存管理
// 不要为每个图像单独分配内存
// 使用 VkMemoryAllocator 或自定义分配器
// 不推荐:
for (u32 i = 0; i < image_count; ++i) {
vkAllocateMemory(..., &image_memory[i]); // 浪费,碎片化
}
// 推荐:
// 批量分配,手动管理偏移
VkDeviceSize total_size = calculate_total_size();
vkAllocateMemory(..., total_size, &shared_memory);
for (u32 i = 0; i < image_count; ++i) {
vkBindImageMemory(..., shared_memory, offsets[i]);
}
6. 深度格式选择
// 优先级排序:
// 1. VK_FORMAT_D32_SFLOAT: 最高精度,适合大场景
// 2. VK_FORMAT_D32_SFLOAT_S8_UINT: 需要模板缓冲时
// 3. VK_FORMAT_D24_UNORM_S8_UINT: 兼容性最好,性能折中
// 对于移动设备,考虑:
// - VK_FORMAT_D16_UNORM: 节省带宽
常见问题
Q1: 为什么需要图像视图?
A: 图像视图定义了如何"解释"图像数据:
// 同一个图像可以有多个视图
VkImage image; // 2D array texture with 6 layers
// View 1: 作为 cube map
VkImageView cube_view; // VK_IMAGE_VIEW_TYPE_CUBE
// View 2: 作为单独的 2D 纹理
VkImageView layer_views[6]; // VK_IMAGE_VIEW_TYPE_2D, layer 0-5
// View 3: 只访问特定 mipmap 级别
VkImageView mip_view; // baseMipLevel = 2, levelCount = 1
Q2: CONCURRENT 和 EXCLUSIVE 共享模式的性能差异?
A:
// EXCLUSIVE: 更好的性能
// - GPU 可以做更多优化
// - 缓存利用率更高
// - 需要显式所有权转移(如果跨队列使用)
// CONCURRENT: 便利性
// - 无需所有权转移
// - 可能有额外的内存同步开销
// - 某些 GPU 上性能影响很小
// 性能差异通常 < 5%,便利性优先
Q3: 交换链图像数量是否越多越好?
A: 不是。
// 图像数量增加的影响:
// 优点:
// - 更多缓冲,CPU/GPU 并行性更好
// - 减少等待时间
// 缺点:
// - 内存占用增加
// - 输入延迟增加(更多帧在"管道"中)
// - 某些同步问题更复杂
// 推荐: 2-3 个图像
// 2: 低延迟应用(竞技游戏)
// 3: 平衡选择(大多数游戏)
// 4+: 通常不必要
Q4: 为什么需要重建交换链?
A: 交换链与表面紧密耦合:
// 交换链创建时绑定了:
// - 表面分辨率
// - 表面格式
// - 呈现模式
// 当这些属性改变时,交换链变得不兼容:
// - 窗口调整大小 → 分辨率改变
// - 全屏切换 → 呈现模式可能改变
// - 显示器改变 → 支持的格式可能改变
// Vulkan 不会自动调整,必须手动重建
Q5: 深度附件为什么属于交换链?
A:
// 深度附件的大小必须与颜色附件匹配
// 交换链决定了颜色附件的大小
// 因此深度附件逻辑上属于交换链
// 当交换链重建时:
// 1. 颜色图像大小改变
// 2. 深度图像也必须调整大小
// 3. 一起管理更简单
// 生命周期:
vulkan_swapchain_create(...);
↓
创建颜色图像(自动)
创建深度图像(手动)
↓
vulkan_swapchain_destroy(...);
↓
销毁颜色图像(自动)
销毁深度图像(手动)
Q6: vkAcquireNextImageKHR 为什么需要 semaphore?
A:
// 获取图像是异步的:
u32 image_index;
vkAcquireNextImageKHR(
device,
swapchain,
timeout,
image_available_semaphore, // GPU 信号量
VK_NULL_HANDLE, // 可选的 fence(CPU等待)
&image_index);
// 函数立即返回,但图像可能还在显示中
// semaphore 用于:
// 1. GPU 等待图像真正可用
// 2. 确保不会在图像显示时修改它
// 使用方式:
VkSubmitInfo submit_info;
submit_info.pWaitSemaphores = &image_available_semaphore;
submit_info.pWaitDstStageMask = &stage; // 在哪个阶段等待
// GPU 执行流程:
// 1. 开始执行命令缓冲区
// 2. 到达 wait stage
// 3. 等待 semaphore
// 4. semaphore 被信号 → 图像可用
// 5. 继续执行渲染命令
调试技巧
1. 验证交换链创建
void validate_swapchain(vulkan_swapchain* swapchain) {
KASSERT_MSG(swapchain->handle != VK_NULL_HANDLE, "Invalid swapchain handle");
KASSERT_MSG(swapchain->image_count >= 2, "Too few swapchain images");
KASSERT_MSG(swapchain->image_count <= 4, "Unusually many swapchain images");
for (u32 i = 0; i < swapchain->image_count; ++i) {
KASSERT_MSG(swapchain->images[i] != VK_NULL_HANDLE, "Invalid image");
KASSERT_MSG(swapchain->views[i] != VK_NULL_HANDLE, "Invalid image view");
}
KASSERT_MSG(swapchain->depth_attachment.handle != VK_NULL_HANDLE,
"Invalid depth attachment");
KASSERT_MSG(swapchain->depth_attachment.view != VK_NULL_HANDLE,
"Invalid depth view");
KINFO("Swapchain validation passed");
}
2. 监控交换链重建
void vulkan_swapchain_recreate(
vulkan_context* context,
u32 width,
u32 height,
vulkan_swapchain* swapchain) {
KWARN("Recreating swapchain: %ux%u -> %ux%u",
swapchain->extent.width, swapchain->extent.height,
width, height);
// 统计重建次数
static u32 recreate_count = 0;
recreate_count++;
if (recreate_count > 100) {
KERROR("Swapchain recreated %u times, possible resize loop!", recreate_count);
}
destroy(context, swapchain);
create(context, width, height, swapchain);
KINFO("Swapchain recreated successfully (count: %u)", recreate_count);
}
3. 性能分析
void profile_swapchain_acquire(vulkan_context* context) {
f64 start_time = platform_get_absolute_time();
u32 image_index;
b8 result = vulkan_swapchain_acquire_next_image_index(
context,
&context->swapchain,
UINT64_MAX,
semaphore,
VK_NULL_HANDLE,
&image_index);
f64 elapsed = platform_get_absolute_time() - start_time;
if (elapsed > 0.016) { // > 16ms (60 FPS)
KWARN("Swapchain acquire took %.2f ms", elapsed * 1000.0);
}
static f64 total_time = 0;
static u32 frame_count = 0;
total_time += elapsed;
frame_count++;
if (frame_count % 1000 == 0) {
KINFO("Average acquire time: %.2f ms",
(total_time / frame_count) * 1000.0);
}
}
本篇总结
在本篇教程中,我们实现了 Vulkan 渲染的关键组件:
核心成果
-
逻辑设备创建
- 从物理设备创建逻辑设备
- 配置多个队列(图形、呈现、传输)
- 启用设备扩展和特性
-
交换链系统
- 选择最优表面格式和呈现模式
- 创建和管理交换链图像
- 实现交换链重建机制
-
图像管理
- 创建图像和图像视图
- 分配和绑定 GPU 内存
- 管理深度附件
-
同步机制
- 使用 semaphore 同步 GPU 操作
- 实现图像获取和呈现流程
关键概念
- 逻辑设备: 应用程序与 GPU 交互的接口
- 队列: 提交工作到 GPU 的通道
- 交换链: 管理屏幕呈现的图像队列
- 图像视图: 定义如何访问图像数据
- 深度附件: 用于深度测试的特殊图像
文件清单
engine/src/renderer/vulkan/
├── vulkan_device.c # 逻辑设备创建和管理
├── vulkan_device.h
├── vulkan_swapchain.c # 交换链创建和操作
├── vulkan_swapchain.h
├── vulkan_image.c # 图像创建和视图管理
├── vulkan_image.h
├── vulkan_types.inl # 新增: vulkan_image, vulkan_swapchain
└── vulkan_backend.c # 集成交换链到后端
下一步
在下一篇教程中,我们将学习:
- 渲染通道(Render Pass): 定义渲染操作的结构
- 帧缓冲(Framebuffer): 将图像附加到渲染通道
- 图形管线(Graphics Pipeline): 配置 GPU 渲染流程
- 命令缓冲区(Command Buffers): 记录和提交 GPU 命令
这些组件将使我们能够真正开始渲染图形!
练习
-
队列优化: 修改代码,在传输队列索引与图形队列相同时,跳过创建传输队列。
-
呈现模式配置: 实现一个系统,允许用户在运行时切换呈现模式(需要重建交换链)。
-
内存统计: 添加代码,计算并打印交换链使用的总内存量(颜色图像 + 深度图像)。
-
多采样支持: 扩展交换链,支持多采样抗锯齿(MSAA):
- 创建多采样颜色图像
- 创建多采样深度图像
- 在呈现前解析多采样图像到交换链图像
-
帧时间分析: 实现一个工具,测量从 acquire 到 present 的完整帧时间,并显示统计信息(平均、最小、最大、标准差)。
提示:参考 vulkan_image_create 的实现,修改 samples 参数为 VK_SAMPLE_COUNT_4_BIT 或更高。
参考资料
📖 关注公众号
关注【shangshoushiyanshi】,领取章节视频教程
💖 支持作者
如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕
您的支持是我持续创作的动力!
感谢每一位支持者!🙏
📅 最后更新:2025-11-20
✍️ 作者:上手实验室
更多推荐

所有评论(0)