Vulkan demo入门教程三:逻辑设备、队列与交换链

Vulkan 嵌入式开发实战:逻辑设备、队列与交换链 (Swapchain)

系列回顾

  • 第一步\] 我们创建了 `VkInstance` 并加载了扩展。

本章目标

有了"画布"(Surface),我们需要"画笔"和"颜料"。本文将完成以下核心任务:

  1. 选择队列族 (Queue Families):找到能画图 (Graphics) 和能上屏 (Present) 的硬件队列。
  2. 创建逻辑设备 (Logical Device):从物理 GPU 中切分出一部分资源供程序使用。
  3. 创建交换链 (Swapchain):管理前后端缓冲,实现双缓冲/三缓冲机制,防止画面撕裂。

本文主要是基于 Direct Display (VK_KHR_display) 扩展,详解在嵌入式 Linux环境下,如何从零构建 Vulkan 渲染环境。与桌面窗口系统不同,直连模式要求开发者显式管理物理显示属性(分辨率、刷新率),任何参数不匹配都将导致初始化失败。

第一步:初始化队列族 (Queue Families)

Vulkan 的硬件架构是异构的。GPU 内部包含不同的硬件单元(引擎),分别处理图形渲染、计算、视频解码和显示输出。我们需要查询物理设备支持的队列族,并筛选出满足需求的队列。

  • Graphics Queue: 执行绘图命令(Draw Calls)。
  • Present Queue: 将渲染完成的图像提交至显示控制器。

直连模式关键点

即使没有窗口系统,vkGetPhysicalDeviceSurfaceSupportKHR 依然有效。它用于验证某个队列族是否支持向特定的 VkDisplayModeKHR (Surface) 进行呈现。若队列不支持 Present,后续的 vkQueuePresentKHR 必败。

cpp 复制代码
#include <vector>
#include <cstdio>
#include <vulkan/vulkan.h>

// 假设这些是类成员变量
VkPhysicalDevice mPhysicalDevice;
VkSurfaceKHR mSurface; // 在直连模式下,Surface 对应一个 Display Mode
bool mOnScreen = true; 

std::vector<VkQueueFamilyProperties> mQueueFamilies;
std::vector<uint32_t> mGraphicsQueueFamilies;
std::vector<uint32_t> mPresentQueueFamilies;
uint32_t mGraphicsQueueFamily = UINT32_MAX;
uint32_t mPresentQueueFamily = UINT32_MAX;

int VkEngine::initQueueFamilies() {
    uint32_t queueFamilyCount = 0;
    
    // 1. 获取队列族数量
    vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueFamilyCount, nullptr);
    if (queueFamilyCount == 0) {
        printf("错误: 未找到任何队列族!\n");
        return -1;
    }

    mQueueFamilies.resize(queueFamilyCount);
    // 2. 获取队列族详细信息
    vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueFamilyCount, mQueueFamilies.data());
    printf("物理设备支持 %u 个队列族.\n", queueFamilyCount);

    // 3. 筛选支持 Graphics 的队列
    uint32_t i = 0;
    for (const auto& queueFamily : mQueueFamilies) {
        if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
            mGraphicsQueueFamilies.push_back(i);
        } else {
            printf("队列族 %u 不支持图形渲染 (Graphics).\n", i);
        }
        i++;
    }

    if (mGraphicsQueueFamilies.empty()) {
        printf("致命错误: 没有找到任何支持图形渲染的队列族!\n");
        return -1;
    }
    
    // 策略:优先选择第一个支持 Graphics 的队列族
    mGraphicsQueueFamily = mGraphicsQueueFamilies[0];
    printf("选定图形队列族索引: %u\n", mGraphicsQueueFamily);

    // 4. 筛选支持 Present 的队列 (仅在屏幕渲染模式下需要)
    if (mOnScreen) {
        for (i = 0; i < mQueueFamilies.size(); i++) {
            VkBool32 presentSupport = VK_FALSE;
            // 核心检查:验证该队列族是否支持向当前 Surface (Display Mode) 呈现
            vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice, i, mSurface, &presentSupport);

            if (presentSupport == VK_TRUE) {
                mPresentQueueFamilies.push_back(i);
            } else {
                printf("队列族 %u 不支持呈现 (Present) 到当前 Surface.\n", i);
            }
        }

        if (mPresentQueueFamilies.empty()) {
            printf("致命错误: 没有找到任何支持呈现的队列族!\n");
            return -1;
        }

        // 策略:选择第一个支持 Present 的队列族
        // 优化:若 Graphics 和 Present 索引相同,可复用同一队列,减少同步开销
        mPresentQueueFamily = mPresentQueueFamilies[0];
        printf("选定呈现队列族索引: %u\n", mPresentQueueFamily);
    }

    return 0;
}

第二步:创建逻辑设备 (Logical Device)

物理设备代表真实硬件,应用程序通过逻辑设备 (VkDevice) 与之交互。创建逻辑设备时,必须显式声明:

  1. 使用的队列族及其优先级。
  2. 启用的设备特性 (Features)
  3. 启用的设备扩展 (Extensions) ,例如 VK_KHR_swapchain
cpp 复制代码
VkDevice mDevice;
VkQueue mGraphicsQueue;
VkQueue mPresentQueue;
std::vector<VkExtensionProperties> mDeviceExtensionProperties; // 需提前查询获取

int VkEngine::createLogicalDevice() {
    VkDeviceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

    // (1) 配置队列创建信息
    std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
    float queuePriority = 1.0f; // 优先级范围 0.0 ~ 1.0
    
    // (1-1) 配置图形队列
    VkDeviceQueueCreateInfo graphicsQueueCreateInfo{};
    graphicsQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    graphicsQueueCreateInfo.queueFamilyIndex = mGraphicsQueueFamily;
    graphicsQueueCreateInfo.queueCount = 1;
    graphicsQueueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(graphicsQueueCreateInfo);

    // (1-2) 配置呈现队列
    // 注意:若图形队列与呈现队列索引相同,Vulkan 驱动会自动去重,但显式分开写更清晰
    if (mGraphicsQueueFamily != mPresentQueueFamily) {
        VkDeviceQueueCreateInfo presentQueueCreateInfo{};
        presentQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
        presentQueueCreateInfo.queueFamilyIndex = mPresentQueueFamily;
        presentQueueCreateInfo.queueCount = 1;
        presentQueueCreateInfo.pQueuePriorities = &queuePriority;
        queueCreateInfos.push_back(presentQueueCreateInfo);
    }

    createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
    createInfo.pQueueCreateInfos = queueCreateInfos.data();

    // (2) 设置设备特性 (Features)
    // 默认全关闭。如需多重采样、几何着色器等,需在此处显式开启
    VkPhysicalDeviceFeatures deviceFeatures{}; 
    createInfo.pEnabledFeatures = &deviceFeatures;

    // (3) 启用所需的设备扩展
    std::vector<const char*> extensions;
    if (mOnScreen) {
        extensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
    }

    // (3-1) 运行时验证扩展支持 (防御性编程)
    for (const auto& extension : extensions) {
        bool checkSupport = false;
        for (const auto& phydevExt : mDeviceExtensionProperties) {
            if (strcmp(phydevExt.extensionName, extension) == 0) {
                checkSupport = true;
                break;
            }
        }

        if (!checkSupport) {
            printf("错误: 物理设备不支持必需的扩展:%s\n", extension);
            return -1;
        }
    }

    createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
    createInfo.ppEnabledExtensionNames = extensions.data();
    
    // 已废弃的验证层字段 (现代 Vulkan 推荐在 Instance 层处理)
    createInfo.enabledLayerCount = 0; 

    // (4) 创建逻辑设备
    if (vkCreateDevice(mPhysicalDevice, &createInfo, nullptr, &mDevice) != VK_SUCCESS) {
        printf("致命错误: 无法创建逻辑设备!\n");
        return -1;
    }
    printf("逻辑设备创建成功.\n");

    // (5) 从逻辑设备获取队列句柄
    // 即使两个队列来自同一个家族,获取到的 VkQueue 句柄也是独立的逻辑对象
    vkGetDeviceQueue(mDevice, mGraphicsQueueFamily, 0, &mGraphicsQueue);
    vkGetDeviceQueue(mDevice, mPresentQueueFamily, 0, &mPresentQueue);

    printf("图形队列和呈现队列获取成功.\n");
    return 0;
}

第三步:创建交换链 (Swapchain)

交换链是图像缓冲区的集合。我们在其中一个缓冲区渲染,然后将其"呈现"到屏幕,并切换到下一个缓冲区。
直连模式的核心约束imageExtent (分辨率) 必须严格匹配选定的 VkDisplayModeKHRvisibleRegion,不可随意设定。

cpp 复制代码
VkSwapchainKHR mSwapChain;
VkFormat mImageFormat;
VkExtent2D mExtent;
std::vector<VkImage> mImages;
std::vector<VkImageView> mImageViews;
VkSurfaceFormatKHR mSurfaceFormat;
// 假设 mSurfaceFormats, mSurfacePresentModes, mSurfaceCapabilities, mSelectDisplayModeProperty 已在前序步骤获取

int VkEngine::createSwapchain() {
    VkSwapchainCreateInfoKHR createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    createInfo.surface = mSurface;

    // (1) 选择图像格式
    // 策略:首选 B8G8R8A8 SRGB,若不支持则回退到第一个可用格式
    VkSurfaceFormatKHR chosenFormat;
    bool formatFound = false;
    
    VkSurfaceFormatKHR preferredFormat;
    preferredFormat.format = VK_FORMAT_B8G8R8A8_SRGB;
    preferredFormat.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;

    for (const auto& availableFormat : mSurfaceFormats) {
        if (availableFormat.format == preferredFormat.format && 
            availableFormat.colorSpace == preferredFormat.colorSpace) {
            chosenFormat = availableFormat;
            formatFound = true;
            break;
        }
    }

    if (!formatFound) {
        printf("警告: 首选格式不可用,使用默认可用格式.\n");
        chosenFormat = mSurfaceFormats[0];
    }
    
    mImageFormat = chosenFormat.format;
    createInfo.imageFormat = chosenFormat.format;
    createInfo.imageColorSpace = chosenFormat.colorSpace;
    printf("选定 Swapchain 格式: %d, 颜色空间: %d\n", chosenFormat.format, chosenFormat.colorSpace);

    // (2) 选择呈现模式 (Present Mode)
    // FIFO (垂直同步) 是最广泛支持且无撕裂的模式
    VkPresentModeKHR chosenPresentMode = VK_PRESENT_MODE_FIFO_KHR;
    bool modeFound = false;
    
    for (const auto& availableMode : mSurfacePresentModes) {
        if (availableMode == chosenPresentMode) {
            modeFound = true;
            break;
        }
    }
    
    if (!modeFound) {
        // 理论上 FIFO 是必须支持的,若缺失则表明驱动实现有问题
        printf("致命错误: 不支持 VK_PRESENT_MODE_FIFO_KHR!\n");
        return -1;
    }
    createInfo.presentMode = chosenPresentMode;

    // (3) 设置分辨率 (Extent) - 直连模式关键
    // 必须使用 DisplayMode 定义的分辨率,不能像窗口模式那样自由调整
    VkExtent2D extent = mSelectDisplayModeProperty.parameters.visibleRegion;
    
    // 双重检查:确保分辨率在 Surface 能力范围内
    if (extent.width < mSurfaceCapabilities.minImageExtent.width ||
        extent.width > mSurfaceCapabilities.maxImageExtent.width ||
        extent.height < mSurfaceCapabilities.minImageExtent.height ||
        extent.height > mSurfaceCapabilities.maxImageExtent.height) {
        printf("错误: 选定的分辨率超出 Surface 支持范围!\n");
        return -1;
    }
    
    mExtent = extent;
    createInfo.imageExtent = extent;
    printf("设定 Swapchain 分辨率: %ux%u\n", extent.width, extent.height);

    // (4) 设置图像数量 (缓冲区长度的深度)
    // 通常设为 3 (三重缓冲),需遵守 min/max 限制
    uint32_t imageCount = 3;
    if (mSurfaceCapabilities.maxImageCount > 0 && imageCount > mSurfaceCapabilities.maxImageCount) {
        imageCount = mSurfaceCapabilities.maxImageCount;
    }
    if (imageCount < mSurfaceCapabilities.minImageCount) {
        imageCount = mSurfaceCapabilities.minImageCount;
    }
    createInfo.minImageCount = imageCount;
    printf("设定 Swapchain 图像数量: %u\n", imageCount);

    // (5) 其他配置
    createInfo.imageArrayLayers = 1; // 单图层,除非是 VR 应用
    createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // 作为颜色附件渲染

    // (6) 队列共享模式
    uint32_t queueFamilyIndices[] = {mGraphicsQueueFamily, mPresentQueueFamily};
    if (mGraphicsQueueFamily != mPresentQueueFamily) {
        // 队列不同:必须设为 CONCURRENT,避免所有权转移开销
        createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
        createInfo.queueFamilyIndexCount = 2;
        createInfo.pQueueFamilyIndices = queueFamilyIndices;
    } else {
        // 队列相同:Exclusive 模式性能略优
        createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    }

    // (7) 变换 (Transform)
    // 直连屏幕通常不需要旋转,使用 IDENTITY
    createInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
    if (!(mSurfaceCapabilities.supportedTransforms & createInfo.preTransform)) {
         // 若不支持 Identity,回退到当前变换
         createInfo.preTransform = mSurfaceCapabilities.currentTransform;
    }

    // (8) Alpha 混合与裁剪
    createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; // 不透明
    createInfo.clipped = VK_TRUE; // 忽略被遮挡像素的性能优化
    createInfo.oldSwapchain = VK_NULL_HANDLE;

    // (9) 创建 Swapchain
    if (vkCreateSwapchainKHR(mDevice, &createInfo, nullptr, &mSwapChain) != VK_SUCCESS) {
        printf("致命错误: 无法创建 Swapchain!\n");
        return -1;
    }
    printf("Swapchain 创建成功!\n");

    // (10) 获取 Swapchain 中的图像句柄
    // 注意:此处仅获取 VkImage 句柄,后续需手动创建 VkImageView
    uint32_t realImageCount;
    vkGetSwapchainImagesKHR(mDevice, mSwapChain, &realImageCount, nullptr);
    mImages.resize(realImageCount);
    vkGetSwapchainImagesKHR(mDevice, mSwapChain, &realImageCount, mImages.data());
    
    printf("成功获取 %u 个 Swapchain 图像.\n", realImageCount);
    return 0;
}

第四步:资源清理 (Cleanup)

Vulkan 要求显式管理所有资源。当程序退出或需要重建交换链(如显示模式切换)时,必须按依赖关系的反向顺序销毁资源。

cpp 复制代码
// 假设成员变量
std::vector<VkFramebuffer> mFramebuffers;
VkPipeline mGraphicsPipeline;
VkPipelineLayout mPipelineLayout;
VkRenderPass mRenderPass;
// 同步对象 (semaphores, fences) 假设已有 cleanSyncObjects() 函数
void cleanSyncObjects(); 

void VkEngine::cleanupSwapChain() {
    // 1. 等待设备空闲,确保没有正在使用的资源
    vkDeviceWaitIdle(mDevice);

    // 2. 清理同步对象
    cleanSyncObjects();

    // 3. 销毁帧缓冲区 (依赖 ImageView)
    for (auto framebuffer : mFramebuffers) {
        vkDestroyFramebuffer(mDevice, framebuffer, nullptr);
    }
    mFramebuffers.clear();

    // 4. 销毁管线相关 (依赖 RenderPass 和 PipelineLayout)
    vkDestroyPipeline(mDevice, mGraphicsPipeline, nullptr);
    vkDestroyPipelineLayout(mDevice, mPipelineLayout, nullptr);
    vkDestroyRenderPass(mDevice, mRenderPass, nullptr);

    // 5. 销毁图像视图 (ImageView)
    // 注意:不要销毁 mImages 本身,它们由 Swapchain 统一管理
    for (auto imageView : mImageViews) {
        vkDestroyImageView(mDevice, imageView, nullptr);
    }
    mImageViews.clear();
    mImages.clear(); // 仅清空句柄列表

    // 6. 销毁 Swapchain
    if (mSwapChain != VK_NULL_HANDLE) {
        vkDestroySwapchainKHR(mDevice, mSwapChain, nullptr);
        mSwapChain = VK_NULL_HANDLE;
    }

    // 注意:mSurface 通常在销毁 Instance 前或整个引擎析构时统一销毁
    // 若是因分辨率变化重建 Swapchain,Surface 应保持不变
}

关键技术点解析

1. 队列族的复用与共享模式

  • 同队列族 (Graphics == Present):
    • imageSharingMode 设为 VK_SHARING_MODE_EXCLUSIVE
    • 优势:无需额外的所有权转移操作,性能最优。
  • 异队列族 (Graphics != Present):
    • imageSharingMode 必须 设为 VK_SHARING_MODE_CONCURRENT
    • 必须在 pQueueFamilyIndices 中传入两个队列索引。
    • 风险:若设为 EXCLUSIVE,在 vkQueuePresent 时会因图像所有权未从 Graphics 队列转移到 Present 队列而报错。

2. 直连模式的分辨率铁律

在桌面窗口系统中,currentExtent 可由用户动态调整。但在嵌入式直连模式下:

  • imageExtent 必须 等于 VkDisplayModePropertiesKHR::parameters.visibleRegion
  • 任何偏差(即使是一个像素)都可能导致 vkCreateSwapchainKHR 返回 VK_ERROR_INITIALIZATION_FAILED 或导致黑屏。

3. 格式回退策略

嵌入式 GPU 的格式支持差异巨大。

  • 稳健策略 :优先尝试 VK_FORMAT_B8G8R8A8_SRGB
  • 兜底方案 :若首选不可用,直接使用 mSurfaceFormats[0]
  • 强行指定 unsupported 格式是导致初始化失败的常见原因。

下一步预告

至此,我们已完成 Vulkan 初始化的核心基础设施:

  • ✅ 逻辑设备 (VkDevice)
  • ✅ 队列句柄 (VkQueue)
  • ✅ 交换链 (VkSwapchain) 及底层图像资源 (VkImage)

接下来的内容将进入渲染管线构建阶段:

  1. ImageView 创建 :如何将 VkImage 包装为着色器可读的视图。
  2. Render Pass 定义:描述附件的加载/存储操作及子依赖关系。
  3. Graphics Pipeline 构建:整合 Shader、顶点输入、光栅化状态等,打造不可变的渲染状态对象。
相关推荐
YMH.2 小时前
Day3.14c++
开发语言·c++
特种加菲猫2 小时前
C++ std::list 完全指南:从入门到精通所有接口
开发语言·c++
mjhcsp2 小时前
C++ A* 算法:启发式路径搜索的黄金标准
android·c++·算法
仰泳的熊猫3 小时前
题目2281:蓝桥杯2018年第九届真题-次数差
数据结构·c++·算法·蓝桥杯
实心儿儿3 小时前
C++ —— 多态
开发语言·c++
小小怪7503 小时前
C++中的代理模式高级应用
开发语言·c++·算法
格林威3 小时前
工业相机图像高速存储(C++版):直接IO存储方法,附海康相机实战代码!
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
小此方3 小时前
Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析
开发语言·c++
2401_891482173 小时前
C++中的观察者模式
开发语言·c++·算法