上一篇:Vulkan 表面和物理设备 | 下一篇:待定 | 返回目录


📚 快速导航

目录 (点击展开/折叠)

🎯 本章目标

通过本教程,你将学会:

🎯 目标 📝 描述 ✅ 成果
逻辑设备 从物理设备创建逻辑设备 掌握设备和队列创建
交换链 创建和管理交换链 实现双缓冲/三缓冲
图像管理 创建图像和图像视图 理解图像-内存绑定
深度附件 配置深度缓冲 实现深度测试
重建机制 处理窗口大小变化 实现交换链重建

📖 教程概述

在上一篇教程中,我们成功创建了 Vulkan 表面并选择了合适的物理设备。本篇教程将继续深入,我们将学习如何:

  • 从选定的物理设备创建逻辑设备
  • 获取设备队列句柄
  • 理解交换链(Swapchain)的概念和作用
  • 创建和配置 Vulkan 交换链
  • 管理交换链图像和深度附件
  • 实现交换链的重建机制

🔧 核心概念

逻辑设备(Logical Device)

物理设备代表系统中的 GPU 硬件,而逻辑设备是应用程序与物理设备之间的接口。它是对物理设备功能的抽象,允许我们:

  • 创建和管理队列
  • 分配内存
  • 创建各种 Vulkan 对象(图像、缓冲区、管线等)
  • 提交命令到 GPU

一个物理设备可以创建多个逻辑设备,每个逻辑设备可以请求不同的特性和队列。

设备队列(Device Queues)

队列是应用程序向 GPU 提交工作的通道。Vulkan 中有几种类型的队列:

  1. 图形队列(Graphics Queue): 执行图形渲染命令
  2. 呈现队列(Present Queue): 将渲染结果显示到屏幕
  3. 计算队列(Compute Queue): 执行计算着色器
  4. 传输队列(Transfer Queue): 专门用于内存传输操作

不同的队列族可能支持不同的操作,我们需要根据应用需求选择合适的队列。

交换链(Swapchain)

交换链是 Vulkan 渲染到屏幕的关键组件。它本质上是一个图像队列,包含:

  • 多个图像缓冲区: 通常 2-3 个(双缓冲或三缓冲)
  • 呈现模式: 控制图像如何显示到屏幕
  • 表面格式: 定义颜色空间和像素格式

工作流程:

  1. 从交换链获取一个可用图像
  2. 渲染到该图像
  3. 将图像提交回交换链进行呈现
  4. 交换链将图像显示到屏幕

这种机制避免了屏幕撕裂,实现了流畅的画面切换。

代码实现

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. 队列去重: 检查队列索引是否重复,避免创建重复队列
  2. 队列优先级: 设置为 1.0(最高优先级)
  3. 设备特性: 启用各向异性过滤,提升纹理质量
  4. 扩展启用: 必须启用 VK_KHR_swapchain 扩展
  5. 获取队列: 使用 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));
}

图像创建步骤:

  1. 创建图像对象: 定义图像的基本属性
  2. 查询内存需求: GPU 告诉我们需要多少内存
  3. 分配内存: 从合适的内存堆分配
  4. 绑定内存: 将内存绑定到图像
  5. 创建视图: 定义如何访问图像数据

图像视图的作用:

图像视图是图像的"解释器",它定义了:

  • 访问哪个 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 渲染的关键组件:

核心成果

  1. 逻辑设备创建

    • 从物理设备创建逻辑设备
    • 配置多个队列(图形、呈现、传输)
    • 启用设备扩展和特性
  2. 交换链系统

    • 选择最优表面格式和呈现模式
    • 创建和管理交换链图像
    • 实现交换链重建机制
  3. 图像管理

    • 创建图像和图像视图
    • 分配和绑定 GPU 内存
    • 管理深度附件
  4. 同步机制

    • 使用 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 命令

这些组件将使我们能够真正开始渲染图形!

练习

  1. 队列优化: 修改代码,在传输队列索引与图形队列相同时,跳过创建传输队列。

  2. 呈现模式配置: 实现一个系统,允许用户在运行时切换呈现模式(需要重建交换链)。

  3. 内存统计: 添加代码,计算并打印交换链使用的总内存量(颜色图像 + 深度图像)。

  4. 多采样支持: 扩展交换链,支持多采样抗锯齿(MSAA):

    • 创建多采样颜色图像
    • 创建多采样深度图像
    • 在呈现前解析多采样图像到交换链图像
  5. 帧时间分析: 实现一个工具,测量从 acquire 到 present 的完整帧时间,并显示统计信息(平均、最小、最大、标准差)。

提示:参考 vulkan_image_create 的实现,修改 samples 参数为 VK_SAMPLE_COUNT_4_BIT 或更高。

参考资料


📖 关注公众号

关注【shangshoushiyanshi】,领取章节视频教程


💖 支持作者

如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕

您的支持是我持续创作的动力!

感谢每一位支持者!🙏


📅 最后更新:2025-11-20
✍️ 作者:上手实验室

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐