窗口、实例与设备

这篇文章介绍 Vulkan 中的实例和设备,我们使用 GLFW + Vulkan 搭建最基础的渲染环境。以下是涉及到的函数和结构体整理。虽然涉及到很多函数和结构体,但整体流程实际上非常简单:

1. 初始化并创建一个可显示的窗口(GLFWInit > WindowHint > CreateWindow)。

2. 初始化 Vulkan 全局环境并创建实例(VkInstanceCreateInfo > vkCreateInstance)。

3. 选择显卡、检查队列族、创建设备与获取队列(vkEnumeratePhysicalDevices > VkDeviceCreateInfo > vkCreateDevice > vkGetDeviceQueue)。

代码就不细讲了,记录在 Commit f68d8a2

一、 窗口

glfwInit() 是 GLFW 中的初始化函数。在使用 GLFW 进行任何图形窗口或输入操作之前,必须先调用它。它的函数原型为:

  • GLFWAPI int glfwInit(void);

glfwInit() 用来初始化 GLFW 库的内部状态。它主要完成以下几件事:

1. 设置全局状态:初始化内部全局变量。准备好窗口管理、输入事件系统、时间函数等模块。

2. 平台相关初始化:在 Windows 上,它会注册窗口类并准备消息循环;在 Linux 上,它会连接 X11 或 Wayland 等图形系统。

3. 准备错误回调机制:初始化之后你可以通过 glfwSetErrorCallback() 注册错误处理函数。如果初始化失败,GLFW 会在内部记录错误原因。


glfwWindowHint() 函数用于在窗口创建之前设置窗口的特性,例如 API 类型、是否可调整大小、上下文版本号和窗口样式等。它的函数原型为:

  • GLFWAPI void glfwWindowHint(int hint, int value);

参数 hint 指定窗口的某个属性。参数 value 是对应 hint 的值。

比如,GLFW_CLIENT_API 属性指定创建窗口时使用哪种图形客户端 API。常用值有:GLFW_OPENGL_API,使用 OpenGL 创建上下文(默认);GLFW_OPENGL_ES_API,使用 OpenGL ES;GLFW_NO_API,不创建任何图形上下文,通常用于 Vulkan 或自定义渲染 API。

GLFW_RESIZABLE 属性指定窗口是否可以被用户调整大小。可选值有:GLFW_TRUE,窗口可以被拖动改变大小(默认);GLFW_FALSE,窗口大小固定,不能被拖动。


glfwCreateWindow() 函数用于创建一个可渲染的窗口。它的函数原型为:

  • GLFWAPI GLFWwindow* glfwCreateWindow(
  •     int width,
  •     int height,
  •     const char* title,
  •     GLFWmonitor* monitor,
  •     GLFWwindow* share);

参数 width 指定窗口宽度(像素);参数 height 指定窗口高度(像素);参数 title 指定窗口标题(字符串)。

参数 monitor 指定全屏显示的显示器。如果为 NULL,则创建窗口模式;参数 share 指定要共享上下文的窗口,用于资源共享。如果不共享,传 NULL。


glfwWindowShouldClose() 函数用于查询窗口是否应该关闭。它通常在渲染循环中使用,用来判断窗口是否收到了关闭请求。它的函数原型为:

  • GLFWAPI int glfwWindowShouldClose(GLFWwindow* window);

glfwPollEvents() 函数处理所有已经在事件队列中的窗口系统事件,并触发相应的回调函数。简单来说,它让 GLFW 更新窗口状态、检测输入(鼠标、键盘、窗口关闭等)并分发事件回调。它的函数原型为:

  • GLFWAPI void glfwPollEvents(void);

glfwDestroyWindow() 函数用于销毁一个之前通过 glfwCreateWindow() 创建的窗口,并清理与该窗口相关的所有内部资源(如上下文、输入状态、回调函数、系统句柄等)。它的函数原型为:

  • GLFWAPI void glfwDestroyWindow(GLFWwindow* window);

glfwTerminate() 是 GLFW 库中用于清理所有全局资源、关闭库功能的函数。它是使用 GLFW 的最后一步,用来安全退出并释放内存。它的函数原型为:

  • GLFWAPI void glfwTerminate(void);

二、 实例

VkInstanceCreateInfo 结构体是用于创建实例的核心结构。它的定义如下:

  • typedef struct VkInstanceCreateInfo {
  •     VkStructureType             sType;
  •     const void*                 pNext;
  •     VkInstanceCreateFlags       flags;
  •     const VkApplicationInfo*    pApplicationInfo;
  •     uint32_t                    enabledLayerCount;
  •     const char* const*          ppEnabledLayerNames;
  •     uint32_t                    enabledExtensionCount;
  •     const char* const*          ppEnabledExtensionNames;
  • } VkInstanceCreateInfo;

sType 参数指定结构体类型,必须设置为 VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO

pNext 参数用于结构体链。设置为 NULL,表示没有扩展结构。

flags 参数在 Windows 平台下设置为 0。

pApplicationInfo 参数填写你的应用和引擎信息。有帮助驱动进行优化、指定 Vulkan API 版本等作用。

enabledLayerCount 和 ppEnabledLayerNames 参数用于设置验证层。

enabledExtensionCount 和 ppEnabledExtensionNames 参数指定启用哪些实例级扩展。


VkApplicationInfo 结构体,作为 VkInstanceCreateInfo 结构体的一部分传入。它用于描述应用程序和引擎基础信息。它不是必填的,但建议提供,因为驱动会根据这里的信息进行优化兼容。它的定义如下:

  • typedef struct VkApplicationInfo {
  •     VkStructureType    sType;
  •     const void*        pNext;
  •     const char*        pApplicationName;
  •     uint32_t           applicationVersion;
  •     const char*        pEngineName;
  •     uint32_t           engineVersion;
  •     uint32_t           apiVersion;
  • } VkApplicationInfo;

sType 参数指定结构体类型,必须为 VK_STRUCTURE_TYPE_APPLICATION_INFO

pNext 参数用于结构体链扩展。此处极少扩展,一般填写 NULL。

pApplicationName 参数填写你的程序名称。

applicationVersion 参数填写应用程序版本号,通常用 VK_MAKE_VERSION 宏指定,格式为 major/minor/patch。

pEngineName 参数指定引擎名称。比如 "UE5"、"Unity" 等。没有使用某个公共引擎,可以直接写 "NoEngine"。

engineVersion 参数指定引擎版本号,与 applicationVersion 相同格式。

apiVersion 参数指定你想使用的 Vulkan API 版本。比如选择 VK_API_VERSION_1_1VK_API_VERSION_1_2VK_API_VERSION_1_3


glfwGetRequiredInstanceExtensions() 函数是使用 GLFW + Vulkan 时必不可少的函数之一。它返回创建 Vulkan 实例时必须启用的扩展列表。可以用来设置 VkInstanceCreateInfo.enabledExtensionCountVkInstanceCreateInfo.ppEnabledExtensionNames。它的函数原型为:

  • GLFWAPI const char** glfwGetRequiredInstanceExtensions(uint32_t* count);

GLFW 会通过 count 参数返回所需的扩展数量。返回值是扩展名的字符串数组,长度为 count。

注意,函数返回的扩展数组是由 GLFW 内部管理。我们可以复制这个数组,方便后续使用或扩展。


VkExtensionProperties 是 Vulkan 中用于描述扩展信息的结构体。它的定义如下:

  • typedef struct VkExtensionProperties {
  •     char        extensionName[VK_MAX_EXTENSION_NAME_SIZE];
  •     uint32_t    specVersion;
  • } VkExtensionProperties;

extensionName 参数返回扩展名称的 C 字符串。specVersion 参数是扩展的规范版本号。


vkEnumerateInstanceExtensionProperties() 是 Vulkan 初始化阶段最常见的查询函数之一,用来获取当前系统上可用的实例扩展。它的函数原型为:

  • VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceExtensionProperties(
  •     const char*                                 pLayerName,
  •     uint32_t*                                   pPropertyCount,
  •     VkExtensionProperties*                      pProperties);

pLayerName 参数指定某个层支持的扩展。传 NULL 表示查询全局扩展。

pPropertyCount 参数返回扩展的数量。

pProperties 参数返回扩展信息的数组。

使用的话,先 pProperties 传 NULL,只获取扩展的数量。然后按数量分配数组大小,再设置获取扩展数据。

结合 vkEnumerateInstanceExtensionProperties(),可以对 glfwGetRequiredInstanceExtensions() 获取的扩展进行检查,增加程序健壮性。


vkCreateInstance() 函数是 Vulkan 程序的第一步、最核心的初始化函数之一。

它使用提供的配置(layers、extensions、AppInfo 等),创建一个 Vulkan 实例 VkInstance。这是任何 Vulkan 程序都会调用的第一个 Vulkan API,用来初始化 Vulkan 框架、激活扩展、启用验证层等。它的函数原型为:

  • VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(
  •     const VkInstanceCreateInfo*                 pCreateInfo,
  •     const VkAllocationCallbacks*                pAllocator,
  •     VkInstance*                                 pInstance);

pCreateInfo 参数是创建实例的配置结构体,我们在 VkInstanceCreateInfo 里已经介绍过。

pAllocator 参数用于自定义内存分配器。一般情况下传 NULL,直接让 Vulkan 用默认内存分配策略。

pInstance 参数返回创建好的 VkInstance

返回值为 VK_SUCCESS 表示创建成功。


vkDestroyInstance 函数用于销毁 Vulkan 实例并释放其相关的所有资源。它的函数原型为:

  • VKAPI_ATTR void VKAPI_CALL vkDestroyInstance(
  •     VkInstance                                  instance,
  •     const VkAllocationCallbacks*                pAllocator);

instance 参数传入要销毁的 VkInstance 对象。

pAllocator 参数指定自定义内存分配器回调函数,没有就传 NULL。


三、 物理设备

vkEnumeratePhysicalDevices() 函数用于获取系统中可用的物理设备列表。它的函数原型为:

  • VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDevices(
  •     VkInstance                                  instance,
  •     uint32_t*                                   pPhysicalDeviceCount,
  •     VkPhysicalDevice*                           pPhysicalDevices);

instance 参数传入已创建的 VkInstance

pPhysicalDeviceCount 参数输出物理设备的数量。pPhysicalDevices 参数输出 VkPhysicalDevice 的数组。


在创建逻辑设备之前,必须确定队列族支持哪些操作。VkQueueFamilyProperties 是 Vulkan 中用于描述队列族能力的核心结构。它的定义如下:

  • typedef struct VkQueueFamilyProperties {
  •     VkQueueFlags    queueFlags;
  •     uint32_t        queueCount;
  •     uint32_t        timestampValidBits;
  •     VkExtent3D      minImageTransferGranularity;
  • } VkQueueFamilyProperties;

queueFlags 参数表示该队列族支持哪些功能,常见位掩码:VK_QUEUE_GRAPHICS_BIT 支持图形渲染命令;VK_QUEUE_COMPUTE_BIT 支持计算着色器;VK_QUEUE_TRANSFER_BIT 支持纯传输操作;VK_QUEUE_SPARSE_BINDING_BIT 支持稀疏资源绑定;VK_QUEUE_PROTECTED_BIT 支持受保护的操作。

queueCount 参数表示该队列族中可创建多少条独立的队列。

timestampValidBits 参数表示该队列族支持的时间戳精度。

minImageTransferGranularity 参数表示图像传输时的最小传输粒度。


vkGetPhysicalDeviceQueueFamilyProperties() 函数用于查询一个物理设备支持的所有队列族的属性。它的函数原型为:

  • VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties(
  •     VkPhysicalDevice                            physicalDevice,
  •     uint32_t*                                   pQueueFamilyPropertyCount,
  •     VkQueueFamilyProperties*                    pQueueFamilyProperties);

physicalDevice 是由 vkEnumeratePhysicalDevices() 返回的一个物理设备句柄。

pQueueFamilyPropertyCount 参数输出队列族的数量。

pQueueFamilyProperties 参数输出数组,存放每个队列族的属性。


四、 逻辑设备

VkDeviceQueueCreateInfo 这个结构体用于 Vulkan 创建逻辑设备时,指定要从哪个队列族中创建多少条队列,以及这些队列的优先级。它的定义如下:

  • typedef struct VkDeviceQueueCreateInfo {
  •     VkStructureType             sType;
  •     const void*                 pNext;
  •     VkDeviceQueueCreateFlags    flags;
  •     uint32_t                    queueFamilyIndex;
  •     uint32_t                    queueCount;
  •     const float*                pQueuePriorities;
  • } VkDeviceQueueCreateInfo;

sType 参数指定结构类型,必须是 VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO

pNext 参数扩展用,通常填 NULL。

flags 参数大部分情况填 0。

queueFamilyIndex 参数指定要从哪个队列族创建队列。通常通过 vkGetPhysicalDeviceQueueFamilyProperties() 查到队列族数量,然后选择一个索引。

queueCount 参数指定从该队列族中创建多少条队列。

pQueuePriorities 指定每条队列的优先级,范围为 0.0 到 1.0。


VkDeviceCreateInfo 是创建 Vulkan 逻辑设备时最核心的配置结构体。它告诉 Vulkan,用这个物理设备创建一个逻辑设备,并启用哪些队列、扩展和特性。它的定义如下:

  • typedef struct VkDeviceCreateInfo {
  •     VkStructureType                    sType;
  •     const void*                        pNext;
  •     VkDeviceCreateFlags                flags;
  •     uint32_t                           queueCreateInfoCount;
  •     const VkDeviceQueueCreateInfo*     pQueueCreateInfos;
  •     // enabledLayerCount is deprecated and should not be used
  •     uint32_t                           enabledLayerCount;
  •     // ppEnabledLayerNames is deprecated and should not be used
  •     const char* const*                 ppEnabledLayerNames;
  •     uint32_t                           enabledExtensionCount;
  •     const char* const*                 ppEnabledExtensionNames;
  •     const VkPhysicalDeviceFeatures*    pEnabledFeatures;
  • } VkDeviceCreateInfo;

sType 参数必须设置为 VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO

pNext 参数为扩展链,通常为 NULL。

flags 参数,一般填 0。

queueCreateInfoCount 参数指定数组 pQueueCreateInfos 的数量。

pQueueCreateInfos 参数为 VkDeviceQueueCreateInfo 结构的数组,上面已经介绍过该结构体。

enabledLayerCount 和 ppEnabledLayerNames 在 Vulkan 1.0 后已被弃用。现在所有需要的 layer 都为 instance level。

enabledExtensionCount 参数指定设备扩展的数量;ppEnabledExtensionNames 参数指定要启用的扩展名称数组。

pEnabledFeatures 参数指定启用哪些物理设备特性。


vkCreateDevice() 函数用于创建逻辑设备 VkDevice。它从选定的物理设备的基础上创建一个逻辑设备,并初始化队列、扩展和特性等。它的函数原型为:

  • VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
  •     VkPhysicalDevice                            physicalDevice,
  •     const VkDeviceCreateInfo*                   pCreateInfo,
  •     const VkAllocationCallbacks*                pAllocator,
  •     VkDevice*                                   pDevice);

physicalDevice 参数指定要基于哪个物理设备创建逻辑设备。

pCreateInfo 参数为 VkDeviceCreateInfo 类型,在上面已经介绍过。

pAllocator 参数一般填 NULL,自定义内存分配器才会用到。

pDevice 参数返回创建好的逻辑设备句柄。


vkGetDeviceQueue() 函数用来从已创建的逻辑设备 VkDevice 中取得队列 VkQueue 句柄。

Vulkan 的所有 GPU 操作都通过队列进行。这个函数不会创建队列,只是取出你在 vkCreateDevice() 时声明过的队列。它的函数原型为:

  • VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue(
  •     VkDevice                                    device,
  •     uint32_t                                    queueFamilyIndex,
  •     uint32_t                                    queueIndex,
  •     VkQueue*                                    pQueue);

device 参数为已经创建好的逻辑设备。

queueFamilyIndex 参数指定队列族的索引。

queueIndex 参数指定该队列族中取第几个队列。

pQueue 参数输入队列句柄。