第 3 章 与设备握手
利用我们前两章中获得的知识,我们现在已经达到了可以从 0 开始进行 Vulkan 编程的水平。 这两章奠定了基础,并帮助我们理解这一革命性 API 的基本原理。 现在,在更高层次上,我们要了解该技术背后的动机,核心块以及相关的功能和术语。 另外,我们通过 Vulkan 伪代码,并构建了一个简单的程序来理解以及可视化 Vulkan 编程模型。
从本章开始,我们会深入 Vulkan 编程的核心,并开始将我们的 Hello World!伪代码转换成现实世界可执行的程序。
注意
本书的所有章节都是以结构化的方式设计和编写的;每一个新章节都依赖于前一章的内容。 建议您按照章节的顺序进行高效的学习体验。
在本章中,我们将介绍以下主题:
- LunarG SDK 入门
- 用 CMake 设置第一个项目
- 层和扩展的介绍
- 创建一个 Vulkan 实例
- 理解物理和逻辑设备
- 理解队列和队列族
- 将设备喝队列整合在一起
LunarG SDK 入门
本书中的所有章节都使用 LunarG SDK 进行 Vulkan 编程。 该 SDK 可以从 https://vulkan.lunarg.com 下载;你需要一个 LunarG 账户才能下载。
SDK 的默认安装路径始终位于 C:\ VulkanSDK \ 『版本』。 安装成功后,将 SDK 的 Bin 目录位置添加到 PATH 环境变量(C:\ VulkanSDK \ 1.0.26.0 \ Bin)中。 另外,添加 VK_SDK_PATH 环境变量,指向 SDK 的路径(C:\ VulkanSDK \ 1.0.26.0)。
安装程序还会将 Vulkan 加载程序(vulkan-1.dll)添加到 C:\ Windows \ System32 目录中。 根据不同的目标窗口系统,加载器会是一个 32 位或 64 位的 DLL。
以下是本章通用的一些常用术语:
ICD :这是 Installable Client Driver 的缩写。 这是一个 Vulkan 兼容的显卡驱动程序。 多个 ICD - 例如 NVIDIA 和英特尔的驱动程序 - 可以共存,而不会相互干扰。
Layers :这些是可插拔的组件,可以 hook 或拦截 Vulkan 命令。 它们提供诸如调试、验证、跟踪等服务。
Loader :加载程序的工作是定位显卡驱动程序并以平台无关的方式暴露层的库。 在 Windows 上,加载库(vulkan-1.dll)使用注册表来定位 ICD 和层配置。
以下是 LunarG SDK 目录中文件夹结构以及相应的说明:
Bin 和 Bin32 :它们包含可执行文件和加载程序的 32 位(Bin32 文件夹)和 64 位(Bin 文件夹)发行版本。 它们还包含层的库和工具。
Config :这是为了存储不同的 Vulkan 配置。 例如,它包含 vk_layer_settings.txt 文件,该文件用于在不同验证层设置配置参数。 这些配置可以动态地影响层。
Demo :这是 cub、tri 和 vulkaninfo 程序的 Vulkan 示例源码。
Doc :其中包含规范、手册,发行说明以及其他重要的文档。
glslang :其中包含 glslang 的源代码和头文件。 它为 GLSL 提供了一个前端解析器,并为 shader 验证提供了一个名为 glslangValidator 的独立包装工具。
Include :其中包含了用于构建和编译 Vulkan 应用程序所需的头文件。
Runtime installer :Vulkan 运行时安装程序提供的 Vulkan 运行时库,可由 Vulkan 应用程序或驱动程序包含这些运行环境。 有关更多信息,请参阅 README.txt 文件。 Source :其中包含加载器(vulkan-1.dll)和层库的源代码实现。 spir-v tools :其中包括 SPIR-V 工具的源代码和头文件。
注意
多个 SDK 的安装不会影响其他 SDK 的安装。 PATH 环境变量总是指向最近安装的 SDK 版本。
使用 CMake 设置我们的第一个项目
CMake 是一种构建过程管理工具,可以以独立于编译器的方式在操作系统中工作。 它利用 CMakeLists.txt 文件构建项目的解决方案。 在本节中,我们将学习为第一个 Vulkan 应用程序构建 CMake 文件的过程。 请参阅以下说明了解这个配置文件(CMakeLists.txt)的创建方法:
- 按照指定的文件夹结构约定创建一个空的 CMakeLists.txt 文件,即 chapter_3> Sample Name> CMakeLists.txt。 为了确保不同 CMake 版本之间的兼容性,您需要指定最低支持的 CMake 版本号。 如果当前版本的 CMake 碰巧低于指定的版本,它就会停止构建解决方案。 CMake 的最小支持版本是用 cmake_minimum_required 指定的。 以下是 CMakeList.txt 文件中的代码:
cmake_minimum_required(VERSION 3.7.1)
- 使用 set CMake 关键字指定用来查找 Vulkan SDK 路径的必要变量。 另外,在起一个有意义的名字:
set (Recipe_Name "3_0_DeviceHandshake")
- 在这个小节中,我们使用的 CMake 版本为 3.7.1,因为 Vulkan 模块自带的就是这个版本。 此模块有助于自动查找 Vulkan SDK 安装,包含目录以及所需的库,来构建 Vulkan 应用程序。 在下面的 CMake 代码中,我们首先尝试使用 CMake Vulkan 模块来定位 Vulkan SDK,如果不成功,我们就使用手动的方式指定 Vulkan SDK 路径。 请根据代码中给定的注释获得更加详细的描述:
# AUTO_LOCATE_VULKAN - accepted value ON or OFF # ON - Use CMake to auto locate the Vulkan SDK.
# OFF - Vulkan SDK path can be specified manually.
This is helpful to test the build on various Vulkan version.
option(AUTO_LOCATE_VULKAN “AUTO_LOCATE_VULKAN” ON ) if(AUTO_LOCATE_VULKAN)
message(STATUS "Attempting auto locate Vulkan using CMake ")
Find Vulkan Path using CMake’s Vulkan Module
This will return Boolean ‘Vulkan_FOUND’ indicating
the status of find as success(ON) or fail(OFF).
Include directory path - ‘Vulkan_INCLUDE_DIRS’ # and ‘Vulkan_LIBRARY’ with required libraries.
find_package(Vulkan)
Try extracting VulkanSDK path from ${Vulkan_INCLUDE_DIRS}
if (NOT ${Vulkan_INCLUDE_DIRS} STREQUAL “”)
set(VULKAN_PATH ${Vulkan_INCLUDE_DIRS}) STRING(REGEX REPLACE “/Include” “” VULKAN_PATH
${VULKAN_PATH})
endif()
if(NOT Vulkan_FOUND)
CMake may fail to locate the libraries but could be able to # provide some path in Vulkan SDK include directory variable # ‘Vulkan_INCLUDE_DIRS’, try to extract path from this.
message(STATUS “Failed to locate Vulkan SDK, retrying again…”)
Check if Vulkan path is valid, if not switch to manual mode.
if(EXISTS “${VULKAN_PATH}”)
message(STATUS “Successfully located the
Vulkan SDK: ${VULKAN_PATH}”)
else()
message(“Error: Unable to locate Vulkan SDK. Please
turn off auto locate option by
specifying ‘AUTO_LOCATE_VULKAN’ as ‘OFF’”) message(“and specify manually path using ‘VULKAN_SDK’
and ‘VULKAN_VERSION’ variables in the CMakeLists.txt.”)
return()
endif() endif()
else()
message(STATUS "Attempting to locate Vulkan SDK
using manual path ")
set(VULKAN_SDK “C:/VulkanSDK”) set(VULKAN_VERSION “1.0.33.0”)
set(VULKAN_PATH “
V
U
L
K
A
N
S
D
K
/
{VULKAN_SDK}/
VULKANSDK/{VULKAN_VERSION}”)
message(STATUS “Using manual specified path: ${VULKAN_PATH}”)
Check if manual set path exists if(NOT EXISTS “${VULKAN_PATH}”)
message(“Error: Unable to locate this Vulkan SDK path VULKAN_PATH:
${VULKAN_PATH}, please specify correct path.
For more information on correct installation process, please refer to subsection ‘Getting started with
Lunar- G SDK’ and ‘Setting up first project with CMake’ in Chapter 3, ‘Shaking hands with the device’ in this book ‘Learning Vulkan’, ISBN - 9781786469809.”)
return() endif()
endif()
- 使用 project 关键字,您可以指定任何想要的项目名称。 在 Windows 上,窗口系统集成 Window System Integration(WSI)需要 VK_KHR_WIN32_SURFACE_EXTENSION_NAME 扩展 API。 为此,您需要在 CMake 文件中使用 add_definitions()定义 VK_USE_PLATFORM_WIN32_KHR 预处理器指令(带 -D 前缀)。 包含存放 Vulkan 头文件的路径。 另外,添加 Bin 文件夹的路径用来链接必要的 Vulkan 运行时或静态库:
# Specify a suitable project name
project(${Recipe_Name})
Add preprocessor definitions here
add_definitions(-DVK_USE_PLATFORM_WIN32_KHR)
- 在 VULKAN_LIB_LINK_LIST 变量中指定所有必需的库,然后使用 target_link_libraries()将其链接到构建项目。 另外,使用 CMake 的 include_directories()API 提供包含 Vulkan 头文件的正确路径。 此外,使用 link_directories()API 指定链接库所在路径。
# Add ‘vulkan- 1’ library for build Vulkan application.
set(VULKAN_LINK_LIST “vulkan-1”) if(${CMAKE_SYSTEM_NAME} MATCHES “Windows”)
Include Vulkan header files from Vulkan SDK
include_directories(AFTER ${VULKAN_PATH}/Include)
Link directory for vulkan- 1
link_directories(${VULKAN_PATH}/Bin)
endif()
- 以下代码用于在构建源项目中将头文件和源文件组合在一起,以更好地可视化和管理代码结构:
# Bunch the header and source files together
if (WIN32)
source_group (“include” REGULAR_EXPRESSION “include/") source_group (“source” REGULAR_EXPRESSION "source/”)
endif (WIN32)
- 指定示例的头文件路径。 使用 file()API 读取示例中的所有头文件和源文件,并将它们存储在 CPP_Lists 和 HPP_Lists 变量中。 使用这些列表向构建解决方案指定用于编译所需的所有文件。 为项目构建提供一个名称并将其链接到所有必需的 Vulkan 库:
# Define include path
include_directories (${CMAKE_CURRENT_SOURCE_DIR}/include)
Gather list of header and source files for compilation
file (GLOB_RECURSE CPP_FILES
${CMAKE_CURRENT_SOURCE_DIR}/source/.cpp) file (GLOB_RECURSE HPP_FILES
${CMAKE_CURRENT_SOURCE_DIR}/include/.*)
Build project, provide name and include files to be compiled
add_executable (${Recipe_Name} ${CPP_FILES} ${HPP_FILES})
Link the debug and release libraries to the project
target_link_libraries ( R e c i p e N a m e {Recipe_Name} RecipeName{VULKAN_LIB_LIST})
- 定义项目属性以及要在项目编译中使用的正确的 C / C ++ 标准版本。 指定二进制可执行文件的路径:
# Define project properties
set_property(TARGET ${Recipe_Name} PROPERTY RUNTIME_OUTPUT_- DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/binaries)
set_property(TARGET ${Recipe_Name} PROPERTY RUNTIME_OUTPUT_- DIRECTORY_DEBUG ${CMAKE_CURRENT_SOURCE_DIR}/binaries)
set_property(TARGET ${Recipe_Name} PROPERTY RUNTIME_OUTPUT_- DIRECTORY_RELEASE ${CMAKE_CURRENT_SOURCE_DIR}/binaries)
set_property(TARGET ${Recipe_Name} PROPERTY RUNTIME_OUTPUT_- DIRECTORY_MINSIZEREL ${CMAKE_CURRENT_SOURCE_DIR}/binaries)
set_property(TARGET ${Recipe_Name} PROPERTY RUNTIME_OUTPUT_- DIRECTORY_RELWITHDEBINFO ${CMAKE_CURRENT_SOURCE_DIR}/binaries)
Define C++ version to be used for building the project set_property(TARGET ${Recipe_Name} PROPERTY CXX_STANDARD 11) set_property(TARGET ${Recipe_Name} PROPERTY
CXX_STANDARD_REQUIRED ON)
Define C version to be used for building the project set_property(TARGET ${Recipe_Name} PROPERTY C_STANDARD 99) set_property(TARGET ${Recipe_Name} PROPERTY
C_STANDARD_REQUIRED ON)
如何构建 CMake 文件
按照以下步骤构建 CMake 文件:
- 打开命令行终端并转到示例的 build 目录。 如果该目录不存在,请创建一个。 这个空的 build 文件夹将包含通过命令行构建的 Visual Studio 项目。 您也可以使用 CMake GUI 进行操作。
- 执行以下命令来构建项目(选择正确的 IDE 版本)。 最后一个参数指定平台架构;因此,如果您使用的是 32 位机器,请使用 Win32:
cmake - G “Visual Studio 14 2015 Win64” …
这就是命令行界面的外观:
命令末尾的两个点指定 CMakeLists.txt(当前所在文件夹的上一级目录,其中就有该文件)的路径,这是 CMake 命令构建项目所需的。 成功执行后,根据之前指定的项目名称,您将找到以下项目文件:
下图显示了本书中所有示例的文件夹结构
扩展
在实现 Vulkan 应用程序时,开发人员可能需要做的第一件事就是关注 API 的扩展特性、功能以及性能。 这些设施能够让开发人员收集一些可用于报告错误、调试以及跟踪命令的重要信息;当然也可以用于验证目的。 Vulkan 利用层和扩展来暴露这些附加的功能:
- 层:层与现有的 Vulkan API 挂钩,并将其自身插入到与指定层关联的 Vulkan 命令链中。 它通常用于验证开发过程。 例如,驱动程序不需要检查 Vulkan API 中提供的参数;验证传入参数是否正确是层的责任。
- 扩展:扩展提供了扩展的功能或特性,这些扩展功能或特性可能是也可能不是标准规范的一部分。 扩展可以是实例或设备的一部分。 扩展命令不能静态链接;首先查询这些扩展,然后将其动态链接到函数指针。 这些函数指针可能已经在 vulkan.h 中进行了定义,用于注册扩展以及必要的数据结构和枚举。
扩展可以分为两种类型:
- 基于实例的扩展:这表示独立于任何设备的全局功能,无需任何 VkDevice 即可访问。
- 基于设备的扩展:扩展对于设备是说特定的,即特定于设备,并且需要一个有效的设备句柄对其进行操作并且暴露特定的功能。
注意
建议在应用程序的开发阶段启用层和扩展,并在产品预期要进行发布时,在生产阶段关闭。 在生产阶段关闭扩展和层允许应用程序节省不必要的验证开销,从而提供更高的性能。
在我们开始编写应用程序之前,让我们来看一下示例会使用到哪些用户自定义的类以及它们的职责是什么:
- 主程序:这是 Hello World 的入口点!!! 它是包含 main()函数的程序。 程序的控制逻辑保存在 main.cpp 文件中。
- Headers.h:这是包含所有头文件的地方;我们会把我们的 Vulkan 头文件放在这里。
- VulkanLayerAndExtension:该类在 VulkanLayerAndExtension.h / .cpp 中实现,并为实例和设备提供基于层和扩展的功能。 它还提供调试的能力。
- VulkanInstance:该类创建 Vulkan 实例对象,在初始化期间很有用,在 VulkanInstance.h / .cpp 中实现。
- VulkanDevice:VulkanDevice.h / .cpp 负责创建逻辑设备和物理设备。 每个物理设备都能够暴露一个或多个队列。 该类还管理设备的队列及其相应的属性。
查询层和扩展
在本节中,我们将实现 main 函数、VulkanApplication 以及 VulkanLayerAndExtension 类。 现在我们就开始 Vulkan 编程。 我们首先查询公开的 Vulkan 层。 请参阅以下说明来实现此目的:
- Vulkan 编程首先需要将添加到头文件 Header.h 中。 它包含最常用的 Vulkan API 和结构声明。
- 创建 VulkanLayerAndExtension 类并声明函数和变量,如以下代码所示。 有关更多详细信息,请参阅其中的内联注释:
struct LayerProperties {
VkLayerProperties properties; vector<VkExtensionProperties> extensions;
};
class VulkanLayerAndExtension{
// Layers and corresponding extension list std::vector<LayerProperties> // Instance/global layergetInstanceLayerProperties();
// Global extensions
VkResult getExtensionProperties(LayerProperties &layerProps, VkPhysicalDevice* gpu = NULL);
// Device based extensions
VkResult getDeviceExtensionProperties(VkPhysicalDevice*gpu);
};
- 在程序启动时,getInstanceLayerProperties()辅助函数会查询实例级或全局级的层, 获取层的数量,并将所有的层信息存储在名为 layerProperties 变量中,类型为 vector。 这两个操作(计数和存储)是通过调用 vkEnumerateInstanceLayerProperties()两次完成的。 第一次,将第二个参数设置为 NULL,会在第一个参数 instanceLayerCount 中返回层的数量。 在第二次调用中,不是将第二个参数设置为 NULL,而是向其传递一个 VkLayerProperties 的 vector,并将获得的详细属性信息传递到其中进行存储。
注意
根据提供参数的不同,Vulkan 下的大多数枚举 API 能够用于执行多个功能。 就在现在,我们已经看到,vkEnumerateInstanceLayerProperties API 不仅用于检索层的数量(通过设置 NULL 参数),还用于获取包含信息的层数组(通过提供一个数据结构数组)。
上述代码的语法如下:
VkResult VulkanLayerAndExtension::getInstanceLayerProperties()
{
// Stores number of instance layers
uint32_t instanceLayerCount;
// Vector to store layer properties
std::vector<VkLayerProperties> layerProperties;
// Check Vulkan API result status
VkResult result;
// Query all the layers
do {
result = vkEnumerateInstanceLayerProperties (&instanceLayerCount, NULL);
if (result)
return result;
if (instanceLayerCount == 0)
return VK_INCOMPLETE; // return fail
layerProperties.resize(instanceLayerCount); result = vkEnumerateInstanceLayerProperties
(&instanceLayerCount, layerProperties.data());
} while (result == VK_INCOMPLETE);
// Query all the extensions for each layer and store it.
std::cout << “\nInstanced Layers” << std::endl; std::cout << “===================” << std::endl; for (auto globalLayerProp: layerProperties) {
// Print layer name and its description
std::cout <<"\n"<< globalLayerProp.description << “\n\t|\n\t|—[Layer Name]–> " << globalLayerProp.layerName <<”\n";
LayerProperties layerProps; layerProps.properties = globalLayerProp;
// Get Instance level extensions for
// corresponding layer properties
result = getExtensionProperties(layerProps);
if (result){
continue;
}
layerPropertyList.push_back(layerProps);
// Print extension name for each instance layer
for (auto j : layerProps.extensions){ std::cout << "\t\t|\n\t\t|—
[Layer Extension]–> " << j.extensionName << “\n”;
}
}
return result;
}
vkEnumerateInstanceLayerProperties() 的语法如下所示:
VkResult vkEnumerateInstanceLayerProperties (
uint32_t* pPropertyCount, VkLayerProperties* pProperties);
下表介绍了 vkEnumerateInstanceLayerProperties()API 的各个参数:
pPropertyCount :该变量表示实例级的层数。 该变量可用作输入或输出变量,具体取决于传递给 pProperties 的值。
pProperties :这个变量可以取两个值。 当指定为 NULL 时,API 将返回层数,参数 pPropertyCount 中就是层的总数。 当用作数组传递给该参数时,API 将检索同一数组中层属性的信息。
一旦我们检索到每个层的层属性信息,我们就会遍历所有层,查询每个层公开的扩展。 我们会通过调用用户自定义的辅助函数 getExtensionProperties()来完成此操作。
在成功执行层及其扩展后,您将在控制台上看到以下输出:
LunarG debug layer
|—[Layer Name]–> VK_LAYER_LUNARG_api_dump
LunarG Validation Layer
|—[Layer Name]–> VK_LAYER_LUNARG_core_validation
|—[Layer Extesion]–> VK_EXT_debug_report
LunarG Standard Validation Layer
|—[Layer Name]–> VK_LAYER_LUNARG_standard_validation
LunarG Validation Layer
|—[Layer Name]–> VK_LAYER_LUNARG_device_limits
|—[Layer Extesion]–> VK_EXT_debug_report
每层可以支持一个或多个扩展。 getExtensionProperties()函数首先枚举层来获取层所暴露的扩展数量。 然后,使用 vkEnumerateInstanceExtensionProperties()API 将扩展的属性信息存储在 LayerProperties 数据结构中。 整个过程与层的枚举非常相似;有关最后一步的更多信息,请参阅 getInstanceLayerProperties()。 getExtensionProperties()函数查询实例和设备的扩展:
// This function retrieves extension and its
// properties at instance and device level.
// Pass a valid physical device pointer (gpu) to retrieve
// device level extensions, otherwise use NULL to
// retrieve extension specific to instance level.
VkResult VulkanLayerAndExtension::getExtensionProperties (LayerProperties &layerProps, VkPhysicalDevice* gpu)
{
// Stores number of extension per layer
uint32_t extensionCount; VkResult result;
// Name of the layer
char* layerName = layerProps.properties.layerName;
do {
// Get the total number of extension in this layer
if(gpu){
result = vkEnumerateDeviceExtensionProperties
(*gpu, layerName, &extensionCount, NULL);
}
else{
result = vkEnumerateInstanceExtensionProperties (layerName, &extensionCount, NULL);
}
if (result |:extensionCount == 0) continue;
layerProps.extensions.resize(extensionCount);
// Gather all extension properties
if (gpu){
result = vkEnumerateDeviceExtensionProperties (*gpu, layerName, &extensionCount,
layerProps.extensions.data());
}
else{
result = vkEnumerateInstanceExtensionProperties (layerName, &extensionCount, layerProps.extensions.data());
}
} while (result == VK_INCOMPLETE);
创建 Vulkan 实例
Vulkan 实例是构建应用程序需要的主要对象,存储了应用程序的所有状态。 属于 VkInstance 类型,并在 VulkanInstance 类中进行管理,该类是用户定义的,实现于 VulkanInstance.h / cpp 文件中。 该类负责创建和销毁 Vulkan 实例对象。 以下是头文件的实现:
class VulkanInstance { // Many lines skipped
// Vulkan instance object variable
VkInstance instance;
// Vulkan instance specific layer and extensions
VulkanLayerAndExtension layerExtension;
// Functions for Creation and Deletion of Vulkan instance
VkResult createInstance( vector<const char *>& layers,
vector<const char >& extensions, const char applicationName);
// Destroy Vulkan instance
void destroyInstance();
};
创建 Vulkan 实例需要使用 VkApplicationInfo 结构对象指定的若干信息,如以下代码中的 appInfo 所示。 此结构对象提供有关应用程序的重要信息,例如其名称、版本、引擎等。 此外,它还会向驱动程序通知应用程序使用的 Vulkan API 版本。 如果指定的版本与底层驱动程序不兼容,应用程序就会报告错误(如果启用了验证层)。 有关更多信息,请参阅本节后面将介绍的 VkApplicationInfo 结构的 apiVersion 字段:
VkResult VulkanInstance::createInstance( vector<const char *>& layers, vector<const char >& extensionNames, char constconst appName) {
// Set the instance specific layer and extension information layerExtension.instanceExtensionNames = extensionNames; layerExtension.instanceLayerNames = layers;
// Define the Vulkan application structure
VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pNext = NULL; appInfo.pApplicationName = appName; appInfo.applicationVersion = 1;
appInfo.pEngineName = appName;
appInfo.engineVersion = 1;
appInfo.apiVersion = VK_API_VERSION_1_0;
// Define the Vulkan instance create info structure
VkInstanceCreateInfo instInfo = {};
instInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instInfo.pNext = NULL;
instInfo.flags = 0;
instInfo.pApplicationInfo = &appInfo;
VkResult res = vkCreateInstance(&instInfo, NULL, &instance); return res;
}
VkInstance 对象是使用 vkCreateInstance()API 创建的。 该 API 用到了 VkInstanceCreateInfo 控制结构对象(instInfo)。 此结构对象包含 appInfo(VkApplicationInfo)的引用,以了解应用程序特定的属性。 另外,VkInstanceCreateInfo 对象也可用于启用实例特定的层及其相应的扩展。
注意
要了解有关层以及在 Vulkan 应用程序中启用层的更多信息,请参阅下一节。
以下是 Vulkan 实例创建 API 的语法:
VkResult vkCreateInstance(
const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
以下是 vkCreateInstance API 的参数介绍:
pCreateInfo :该参数指的是指向 VkInstanceCreateInfo 结构(稍后介绍)的指针,其中包含应用程序(应用程序创建信息)、层以及 Vulkan 特定的信息。
pAllocator :该参数指定了如何控制主机内存的分配。 有关更多信息,请参阅第 5 章,“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。
pInstance :该参数引用了 VkInstance 类型的 Vulkan 实例对象。
以下是 VKInstanceCreateInfo 的语法和结构描述:
typedef struct VKInstanceCreateInfo (
VkStructureType type;
const void* pNextnext;
VkInstanceCreateFlags flags;
const VkApplicationInfo* pApplicationInfo;
uint32_t enabledLayerCount;
const char* const* ppEnabledLayerNames;
uint32_t enabledExtensionCount;
const char* const* ppEnabledExtensionNames;
} VkInstanceCreateInfo;
VKInstanceCreateInfo 结构的字段如下所示:
type :这是该控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO。
pNext :该参数可以是指向扩展特定结构的有效指针或 Null。
flags :该参数为将来保留并且目前未被使用。
pApplicationInfo :这表示 VkApplicationInfo 的对象指针,包含特定于应用程序的信息,例如 Vulkan API 版本、名称和引擎版本等。有关更多信息,请参阅稍后会详细定义的 VkApplicationInfo。 该参数也可以是 NULL。
enabledLayerCount :该参数指定在实例级别启用的层数。
ppEnabledLayerNames :这个参数包含需要在实例级别启用的、数组形式的层名称列表。 有关更多信息,请参阅“层和扩展简介”部分。
enabledExtensionCount :该参数指定在实例级别启用的扩展的数量。
ppEnabledExtensionNames :这个参数包含需要在实例级别启用的、数组形式的扩展名列表。 有关更多详细信息,请参阅“层和扩展简介”部分。
VKInstanceCreateInfo 将 VkApplicationInfo 作为其成员变量之一使用。 我们来看看这个结构的说明:
typedef struct VkApplicationInfo {
VkStructureType type; const void* pNext;
const char* pApplicationName;
uint32_t applicationVersion;
const char* pEngineName;
uint32_t pEngineVersion;
uint32_t apiVersion;
} VkApplicationInfo;
以下是结构中描述的字段:
type :这是该控制结构的类型信息。 这种结构的类型必须指定为 VK_STRUCTURE_TYPE_APPLICATION_INFO 。 pNext :该参数可以是指向特定于扩展结构的有效指针,也可以是 Null。
pApplicatonName :此参数指示为应用程序提供的、用户自定义的应用程序名称,例如 Hello World !!!。
applicationVersion :使用此参数指示开发的应用程序版本。 这有利于直接从应用程序可执行文件本身检索应用程序版本信息。
engineName :这是应用程序使用的后端引擎的名称。
engineVersion :这表示后端应用程序的引擎版本(如果使用了的话); 如果没有,应用程序版本就足够了。
apiVersion :此参数公开了用于运行该应用程序的 Vulkan API 的版本号。 实现会读取此值并验证该值是否可以被忽略(如果指定为 0)或可以被使用(如果指定为非零),如果是不支持的 API 版本,就会报错。 如果有不兼容的问题,则实现会返回 VK_ERROR_INCOMPATIBLE_DRIVER。
注意
创建实例对象时,会忽略 apiVersion 中指定的补丁程序版本号。 只有实例的主版本和次版本必须与 apiVersion 中请求的版本匹配。
当应用程序不再使用时,可以使用用户自定义的函数 destroyInstance()来销毁 Vulkan 实例:
void VulkanInstance::destroyInstance(){ vkDestroyInstance(instance, NULL); // Destroy the instance
}
在这个函数内部,调用了 vkDestroyInstance()API,它接受需要销毁的 Vulkan 实例的句柄作为参数。 以下是此 API 的语法及其说明:
VkResult vkDestroyInstance(
VkInstance instance,
const VkAllocationCallbacks* pAllocator);
以下是与 vkDestroyInstance API 相关联的参数:
instance :这是需要销毁的 Vulkan 实例的句柄。
pAllocator :这个参数指定了主机内存的释放控制。
启用层和扩展
在 Vulkan 中启用层很简单。 应用程序必须知道当前 Vulkan 实现中的可用层, 这可以通过查询以及打印可用的、基于实例的层轻松实现;我们已经在“查询层和扩展”部分中介绍过这个内容。
请参考以下步骤启用层和扩展:
- 向 VulkanLayerAndExtension 类添加两个 vector 列表。 第一个列表包含需要启用的层名称。 第二个列表包含应用程序使用的扩展列表:
class VulkanLayerAndExtension{
. . . .
// List of layer names requested by the application.
std::vector<const char *> appRequestedLayerNames;
// List of extension names requested by the application.
std::vector<const char *> appRequestedExtensionNames;
. . .
};
- 在此应用程序中,我们已经在 main.cpp 中启用了一个层(VK_LAYER_LUNARG_api_dump)和两个扩展(VK_KHR_SURFACE_EXTENSION_NAME 和 VK_KHR_WIN32- _SURFACE_EXTENSION_NAME)。 有关更多信息,请参阅下一小节“测试启用的层和扩展”。
- createInstance()函数包含一个层和扩展列表。 如果没有要指定的列表,则可以向 ppEnabledLayerNames 和 ppEnabledExtensionNames 指定一个 NULL 指针:
VkResult VulkanInstance::createInstance(char constconst appName, VulkanLayerAndExtension layerExtension){
. . . // Many line skipped
VkInstanceCreateInfo instInfo = {};
// Specify the list of layer name to be enabled. instInfo.enabledLayerCount = layers.size(); instInfo.ppEnabledLayerNames = layers.data();
// Specify the list of extensions to be enabled. instInfo.enabledExtensionCount = extensionNames.size(); instInfo.ppEnabledExtensionNames = extensionNames.data();
VkResult res = vkCreateInstance(&instInfo,NULL,&instance);
}
注意
LunarG Vulkan SDK 支持用于调试和验证目的的不同类型的层。 在这个例子中,我们将启用 VK_LAYER_LUNARG_api_dump; 此层将打印 Vulkan API 调用及其参数和值。 层可以在运行时注入到基于实例的层。 有关其他层所提供功能的更多信息,请参阅下一章中的“理解层特性”部分。
测试启用的层和扩展
按照以下说明测试输出:
- 创建 VulkanApplication 类并实现构造函数和一个包装函数(createVulkanInstance)来创建实例。 请注意,这是一个单体类。 有关更多信息,请参阅 VulkanApplication.h / .cpp 文件:
#include “VulkanInstance.h” #include “VulkanLED.h”
class VulkanApplication { private:
VulkanApplication();
public:
~VulkanApplication();
// Many lines skipped please refer to source
// code for full implementation.
public:
// Create the Vulkan instance object
VkResult createVulkanInstance
(vector<const char >& layers, vector<const char
> & extensions, const char* applicationName);
// Vulkan Instance object
VulkanInstance instanceObj;
};
// Application constructor responsible for layer enumeration.
VulkanApplication::VulkanApplication() {
// At application start up, enumerate instance layers
instanceObj.layerExtension.getInstanceLayerProperties();
}
// Wrapper function to create the Vulkan instance
VkResult VulkanApplication::createVulkanInstance (vector<const char*>& layers, vector<const char >&
extensions, const char appName){ instanceObj.createInstance(layers, extensions, appName);
return VK_SUCCESS;
}
- 从 main(main.cpp)程序中设置实例级的层和扩展,启用实例层 VK_LAYER_LUNARG_api_dump。 另外,在添加扩展 VK_KHR_SURFACE_EXTENSION_NAME 和 VK_KHR_WIN32_SURFACE_EXTENSION_NAME。 该层会输出 API 调用,并带有它们相应的参数和值:
#include “Headers.h”
#include “VulkanApplication.h”
std::vector<const char *> instanceExtensionNames = {
VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME
};
std::vector<const char *> layerNames = {
“VK_LAYER_LUNARG_api_dump”
};
int main(int argc, char *argv){ VkResult res;
// Create singleton object, calls Constructor function
VulkanApplication appObj =VulkanApplication::GetInstance(); appObj->initialize();
}
// Application constructor responsible for layer enumeration.
void VulkanApplication::initialize()
{
char title[] = “Hello World!!!”;
// Create the Vulkan instance with
// specified layer and extension names.
createVulkanInstance(layerNames, instanceExtensionNames,
title);
}
- 编译项目,打开终端,然后转到包含可执行文件的文件夹。 键入『可执行文件名』 .exe> 『输出重定向的文件名』,例如,3_0_DeviceHandshake.exe> apiDump.txt:
- 这会产生以下输出:
t{0} vkCreateInstance(pCreateInfo = 000000C697D0F570, pAllocator = 0000000000000000, pInstance = 0000025A40AED010) = VK_SUCCESS
pCreateInfo (000000C697D0F570)
sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO pNext = 000000C697D0F548
flags = 0
pApplicationInfo = 000000C697D0F6B8 enabledLayerCount = 1 ppEnabledLayerNames = 0000025A3F0ED490 enabledExtensionCount = 2
ppEnabledExtensionNames = 0000025A3F0ED9E0 ppEnabledExtensionNames[0] = VK_KHR_surface ppEnabledExtensionNames[1] = VK_KHR_win32_surface ppEnabledLayerNames[0] = VK_LAYER_LUNARG_api_dump pApplicationInfo (000000C697D0F588)
sType = VK_STRUCTURE_TYPE_APPLICATION_INFO pNext = 0000000000000000
pApplicationName = Draw Cube applicationVersion = 1 pEngineName = Draw Cube engineVersion = 1
apiVersion = 4194304 pNext (000000C697D0F578)
注意
也可以显式启用层,例如将 Windows 环境变量设置为 VK_INSTANCE_LAYERS = VK_LAYER_LUNARG_api_dump。
理解物理设备和逻辑设备
Vulkan 将设备的表示划分为两种形式,即逻辑设备和物理设备:
- 物理设备:一个物理设备代表一个实体,可能包含一个 GPU 以及其他的硬件部分,这些部分协同工作以帮助系统完成提交的作业任务。 在一个非常简单的系统中,物理设备可以被视为代表物理 GPU 的单元。
- 逻辑设备:逻辑设备表示实际设备的应用程序视图。
物理设备
OpenGL 没有暴露物理设备;,是在幕后连接物理设备的。 另一方面,Vulkan 将系统真正的计算设备或 GPU 公开给应用程序。 它允许应用程序枚举系统上可用的物理设备。
注意
在本节中,我们将添加一个新的名为 VulkanDevice 的用户自定义类;这个类在 VulkanDevice.h / .cpp 中实现。 它负责管理物理设备(VkPhysicalDevice)和逻辑设备(VkDevice)。 另外,它还管理物理设备的队列族。
以下是 VulkanDevice 类的声明;在我们继续阅读本章的过程中,我们还会遇到本类中使用到的大部分函数。 请参阅随附的源代码以获取此头文件声明的完整实现:
class VulkanDevice{ public:
VulkanDevice(VkPhysicalDevice* gpu); ~VulkanDevice();
// Device related member variables
VkDevice device; // Logical device VkPhysicalDevice* gpu; // Physical device VkPhysicalDeviceProperties gpuProps; // Physical device attributes VkPhysicalDeviceMemoryProperties memoryProperties;
// Queue related properties
// Vulkan Queues object
VkQueue queue;
// Store all queue families exposed by the physical device.
vector<VkQueueFamilyProperties>queueFamilyProps;
// Stores graphics queue index
uint32_t graphicsQueueFamilyIndex;
// Number of queue family exposed by device
uint32_t queueFamilyCount;
// Device specific extensions
VulkanLayerAndExtension layerExtension;
// This class exposes the below function to the outer world
createDevice(), memoryTypeFromProperties()
destroyDevice(), getGrahicsQueueHandle(), initializeDeviceQueue(), getPhysicalDeviceQueuesAndProperties();
};
枚举物理设备
为了建立与可用物理设备的连接,应用程序必须列举这些物理设备。 物理设备枚举就是 Vulkan 将系统中可用的实际设备数量暴露给应用程序的过程。 可以使用 vkEnumeratePhysicalDevices()检索物理设备的列表。
该 API 的语法如下所示:
VkResult (
VkInstance instance,
uint32_t pPhysicalDeviceCount,
VkPhysicalDevice* pPhysicalDevice);
以下是与此 API 关联的参数:
instance :这是 Vulkan 实例的句柄。
pPhysicalDeviceCount |这指定了物理设备的数量。
pPhysicalDevice :这是 Vulkan 物理设备对象。
该 API 包装在 Application 类的 enumeratePhysicalDevices 函数中。 它返回系统上可用物理设备对象的数量:
VkResult VulkanApplication::enumeratePhysicalDevices (std::vector<VkPhysicalDevice>& gpuList){
// Holds the gpu count
uint32_t gpuDeviceCount;
// Get the gpu count vkEnumeratePhysicalDevices (instanceObj.instance, &gpuDeviceCount, NULL);
// Make space for retrieval
gpuList.resize(gpuDeviceCount);
// Get Physical device object
return vkEnumeratePhysicalDevices
(instanceObj.instance, &gpuDeviceCount, gpuList.data());
}
下图显示了系统上枚举的物理设备,并在查询时与 VkInstance 对象关联:
查询物理设备的扩展
物理设备暴露了类似于 Vulkan 实例的扩展。 对于检索到的每个实例级的层属性(VkLayerProperties),可以使用 vkEnumerateDeviceExtensionProperties()API 查询每个物理设备的扩展属性。
该 API 的语法如下所示:
VkResult vkEnumerateDeviceExtensionProperties (
VkPhysicalDevice physicalDevice,
const char* pLayerName,
uint32_t* pExtensionCount, VkExtensionProperties* pProperties);
以下是与此 API 相关联的参数:
physicalDevice :这表示将要查询扩展属性的物理设备。
pLayerName :这是需要查询扩展的层的名称。
pExtensionCount :这指的是当前 physicalDevice 公开的扩展属性的数量,与 pLayerName 相对应。
pProperties :这表示检索到的数组;它包含扩展的属性对象,对应于 pLayerName。
查询基于设备的扩展属性的过程与查询基于实例的扩展属性的过程非常类似。 以下是该功能的实现:
VkResult VulkanLayerAndExtension::getDeviceExtensionProperties
(VkPhysicalDevice* gpu)
{
// Variable to check Vulkan API result status
VkResult result;
// Query all the extensions for each layer and store it.
std::cout << “\Device extensions” << std::endl; std::cout << “===================” << std::endl;
VulkanApplication* appObj = VulkanApplication::GetInstance(); std::vector<LayerProperties>* instanceLayerProp =
&appObj->GetInstance()->instanceObj. layerExtension.layerPropertyList;
for (auto globalLayerProp : *instanceLayerProp) { LayerProperties layerProps;
layerProps.properties = globalLayerProp.properties;
if (result = getExtensionProperties(layerProps, gpu)) continue;
layerPropertyList.push_back(layerProps);
// Many lines skipped…
}
return result;
}
获取物理设备的属性
物理设备的属性可以使用 vkGetPhysicalDeviceProperties()API 检索;检索到的属性保存在 VkPhysicalDeviceProperties 控制结构中。 以下是这个函数的语法:
void vkGetPhysicalDeviceMemoryProperties (
VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties* pMemoryProperties );
以下是 vkGetPhysicalDeviceMemoryProperties 的参数:
physicalDevice :这是 GPU 句柄,需要检索其内存属性。
pMemoryproperties :这是要检索的 GPU 内存属性的结构。
从物理设备查询内存属性
一个物理设备可能具有不同的内存类型,这些内存类型根据属性的不同可以进一步细分。 应用程序了解内存的特性非常重要;这有助于更好地分配资源,具体取决于应用程序的逻辑或资源的类型。 以下的语法是检索物理设备的内存属性:
void vkGetPhysicalDeviceMemoryProperties (
VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties* pMemoryProperties );
以下是 vkGetPhysicalDeviceMemoryProperties 参数的描述:
physicalDevice :该参数是需要查询其内存属性的 GPU 句柄。
pMemoryProperties :该参数用于检索内存属性。
逻辑设备
逻辑设备是物理设备的逻辑表示,但它在应用程序空间中使用;它提供了物理设备的特定视图。 例如,物理设备可能由三个队列组成:图形队列、计算队列和传输队列。 然而,可以使用一个单独的队列(如图形队列)创建一个逻辑设备,即附加单个队列到逻辑设备上;这使得提交命令缓冲区变得异常简单。
创建逻辑设备
逻辑设备使用 VkDevice 表示,可以使用 vkCreateDevice API 创建。 下面是其语法:
VkResult vkCreateDevice(
VkPhysicalDevice pPhysicalDevice, Const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice);
有关 API 参数的更多信息,请参阅下表:
pPhysicalDevice :这表示要创建逻辑设备的物理设备的句柄。
pCreateInfo :这是一个 VkDeviceCreateInfo 结构,其中包含了 vkCreateDevice()API 要使用的特定信息,用于控制逻辑设备的创建。
pAllocator :这指定了如何控制主机内存的分配。 有关更多信息,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存” 一节。
pDevice :该参数是指创建的逻辑设备的指针,其中包含了新建的 VkDevice 对象。
该 API 使用 VkDeviceCreateInfo 控制结构对象(deviceInfo),其中包含了创建逻辑设备对象所需的必要信息。 例如,它包含层的名称(此功能已弃用并保留,用于向后兼容)以及需要在设备上启用的扩展。 另外,它还指定了应该创建并连接到哪个队列。 在我们的案例中,我们对绘图操作感兴趣;因此,我们需要一个队列句柄(graphicsQueueIndex),它用来表示具有绘图功能的队列。 换句话说,我们需要图形队列句柄。
注意
队列信息包含在 VkDeviceQueueCreateInfo 结构对象 queueInfo 中。 当创建一个逻辑设备时,它也使用这个结构创建了与之关联的队列。 有关队列的更多信息,如何查找图形队列索引以及队列的创建过程,请参阅下一节“理解队列和队列族”。
VulkanDevice :: createDevice 是一个用户定义的包装方法,用来帮助创建逻辑设备对象。 下面是它的实现:
VkResult VulkanDevice::createDevice(vector<const char *>& layers,
vector<const char *>& extensions){
VkResult result;
float queuePriorities[1] = { 0.0 };
// Create the object information
VkDeviceQueueCreateInfo queueInfo = {}; queueInfo.queueFamilyIndex = graphicsQueueIndex; queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueInfo.pNext = NULL;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = queuePriorities;
VkDeviceCreateInfo deviceInfo = {};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceInfo.pNext = NULL; deviceInfo.queueCreateInfoCount = 1; deviceInfo.pQueueCreateInfos = &queueInfo; deviceInfo.enabledLayerCount = 0;
// Device layers are deprecated deviceInfo.ppEnabledLayerNames = NULL; deviceInfo.enabledExtensionCount = extensions.size(); deviceInfo.ppEnabledExtensionNames = extensions.data(); deviceInfo.pEnabledFeatures = NULL;
result = vkCreateDevice(*gpu, &deviceInfo, NULL, &device);
assert(result == VK_SUCCESS); return result;
}
在宿主机上等待
在队列中,只要设备有任务需要处理,就说设备处于活动状态。 一旦队列没有更多的命令缓冲区需要处理,设备就会变成空闲状态。 接下来的 vkDeviceWaitIdle API 会在宿主主机上等待,直到逻辑设备的所有队列都变为空闲为止。 该 API 接受需要检查空闲状态的、逻辑设备对象的句柄的作为参数。 以下是其语法:
VkResult vkDeviceWaitIdle( VkDevice device);
丢失设备
由于某些原因(例如硬件故障,设备错误,执行超时,电源管理事件和 / 或特定于平台的事件)而使用逻辑(VKDevice)设备和物理(VKPhysicalDevice)设备时,设备可能会丢失。 这可能导致无法执行挂起的命令缓冲区。
提示
当物理设备丢失时,试图创建逻辑设备对象(VKDevice)就会失败并返回 VK_ERROR_DEVICE_LOST。 如果逻辑设备对象丢失,某些命令在使用时就会返回 VK_ERROR_DEVICE_LOST。 不过,相应的物理设备可能仍然不受影响。 无法重置逻辑设备的丢失状态,并且此状态的丢失对于逻辑设备对象(VKDevice)来说是局部的,并且不会影响任何其他活动的逻辑设备对象。
理解队列以及队列族
队列是应用程序和物理设备通信的手段。 应用程序以向队列提交命令缓冲区的形式提供作业任务。 物理设备对它们进行读取并异步处理。
物理设备可能支持四种类型的队列,如下图所示。 物理设备上可能有多个相同类型的队列;这就允许应用程序选择它需要的队列数量和队列类型。 例如,一个简单的应用程序可能需要两个队列:计算队列和图形队列;在这里,前者用于卷积计算,第二个渲染计算后的高斯模糊图像(blur image)。
物理设备可能由一个或多个队列族组成,在每个队列族内公开了存在什么类型的队列。 此外,每个队列族可以具有一个或多个队列计数。 下图显示了三个队列族及其各自的若干队列:
查询队列族
物理设备能够暴露多个队列族。
队列族属性(queue family properties)的数量由 vkGetPhysicalDeviceQueueFamilyProperties()API 获得,如下所示:
VkResult vkGetPhysicalDeviceQueueFamilyProperties ( VkPhysicalDevice physicalDevice,
uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties* pQueueFamilyProperties);
以下是这个 API 的参数意义:
physicalDevice :这是要检索其队列属性的物理设备的句柄。 pQueueFamilyPropertyCount :这是指设备公开的队列族的数量。 pQueueFamilyProperties :该参数使用一个尺寸等于 queueFamilyPropertyCount 的数组,检索队列族的属性。
按照性质相似的功能将若干队列分成多个族。 以下的 VulkanDevice 类的代码片段显示了如何查询 VkQueueFamilyProperties 控制结构对象中的队列族及其属性,即 queueFamilyProps。
在我们的实现中,在 VulkanDevice 类中定义的名为 getPhysicalDeviceQueuesAndProperties()的包装函数中查询队列族的属性。 以下是具体的实现:
void VulkanDevice::getPhysicalDeviceQueuesAndProperties(){
// Query queue families count by passing NULL as second parameter vkGetPhysicalDeviceQueueFamilyProperties(*gpu, &queueFamilyCount, NULL);
// Allocate space to accomodate Queue properties
queueFamilyProps.resize(queueFamilyCount);
// Get queue family properties
vkGetPhysicalDeviceQueueFamilyProperties
(gpu, &queueFamilyCount, queueFamilyProps.data());
}
此结构的 queueFlag 字段包含了族信息,其标志位的具体含义如下所示:
VK_QUEUE_GRAPHICS_BIT :这是一个图形队列;支持与图形相关的操作。 VK_QUEUE_COMPUTE_BIT :这是一个计算队列;提供计算能力。 VK_QUEUE_TRANSFER_BIT :这是一个转移队列;支持传输操作。 VK_QUEUE_SPARSE_BINDING_BIT :这是一个稀疏队列;能够进行稀疏的内存管理。
每个队列族可以支持一个或多个队列类型,这些队列类型由 VkQueueFamilyProperties 的 queueFlag 字段指示。 queueCount 指定了队列族中的队列数量。 第三个字段 timestampVaildBits 用于为命令的执行进行计时。 最后一个参数 minImageTransferGranularity 指定当前队列族中支持的最小粒度的图像传输操作。 下面是具体的语法:
typedef struct VkQueueFamilyProperties { VkQueueFlags queueFlags; uint32_t queueCount;
uint32_t timestampValidBits; VkExtent3D minImageTransferGranularity;
} VkQueueFamilyProperties;
下图显示了队列和队列族在物理设备中的关系。 在这个特定的例子中,一个物理设备包括四种类型的队列族,根据它支持的队列类型(queueFlags)以及每个族中的队列数量(queueCount),其中的每个队列会包含不同的功能。
存储图形队列句柄
逻辑设备对象的创建也需要一个有效的队列句柄(以索引的形式),以便用创建关联的队列。 为此,遍历查询到的所有队列族属性,并检查 VkQueueFamilyProperties :: queueFlags 标志位信息,从而找到适当的队列。 例如,我们对图形队列句柄感兴趣。 以下代码将图形队列的句柄存储在 graphicsQueueIndex 中,该图形队列用于创建逻辑设备(VkDevice)对象:
uint32_t VulkanDevice::getGrahicsQueueHandle(){ bool found = false;
// 1. Iterate number of Queues supported by the Physical device
for (unsigned int i = 0; i < queueFamilyCount; i++){
// 2. Get the Graphics Queue type
if (queueFamilyProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT){
// 3. Get the handle/index ID of graphics queue family.
found = true;
graphicsQueueIndex = i; break;
}
} return 0;
}
创建队列
当使用 vkCreateDevice()API 创建逻辑设备对象时,就隐式创建了队列。 该 API 还会以 VkDeviceQueueCreateInfo 的形式提取队列信息。 以下是语法和相关字段的说明:
typedef struct VkDeviceQueueCreateInfo { VkStructureType type; const void pNext; VkDeviceQueueCreateFlags flags; uint32_t queueFamilyIndex; uint32_t queueCount; const float pQueuePriorities; } VkDeviceQueueCreateInfo;
下表描述了此结构体的每个字段:
type :这是该控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO。
pNext :该参数可以是指向特定于扩展的结构的有效指针或 NULL。
flags :这是未使用的标志,保留供将来使用。
queueFamilyIndex :这个队列族信息是以 32 位无符号整数类型的形式进行指定的队列族索引。 例如,在我们的例子中,我们提供了 graphicsQueueIndex 变量,其中包含了图形队列的索引。
queueCount :该参数是指要创建的队列族的数量。
pQueuePriorities :该参数表示一组规范化的浮点值(数组),用于指定提交给每个创建队列的作业的优先级。
我们已经知道,创建一个逻辑设备对象时会自动创建若干队列。 然后,应用程序可以使用 vkGetDeviceQueue()API 检索创建的队列。 如下所示,VulkanDevice 类中的函数(getDeviceQueue())提供了一个高级的包装函数,用来获取设备的关联队列:
void VulkanDevice::getDeviceQueue(){
vkGetDeviceQueue(device, graphicsQueueWithPresentIndex, 0, &queue);
}
其语法为:
void vkGetDeviceQueue (
VkDevice logicalDevice,
uint32_t queueFamilyIndex,
uint32_t queueIndex,
VkQueue pQueue);
有关 API 参数的更多信息,请参阅下表:
logicalDevice :该参数引用了拥有队列的逻辑设备(VkDevice)对象。
queueFamilyIndex :该参数指示队列(pQueue)所属的族的索引号。
queueIndex :一个队列族中可能有多个队列,每个队列由唯一的索引标识。 该参数指示队列在队列族中的索引(由 queueFamilyIndex 指示)。
pQueue :该参数指的是由此 API 返回的、检索到的队列对象。
注意
打算从逻辑设备对象查询所需的队列对象的想法在此刻要暂时推迟一下了, 这是因为现在我们对能够提供展示功能的队列感兴趣,为此,我们需要等到第 6 章,分配图像资源和使用 WSI 构建 Swapchain。 在本章中,我们将学习如何实现交换链,以便用于展示目的。
整合设备和队列
在本节中,我们将重新审视,在本章中我们迄今为止获得的所有知识,并实现一个程序,以此来从应用程序的视角创建设备和队列。 我们来看看描述信息流的分步操作过程。
首先,使用 enumeratePhysicalDevices()枚举系统上的物理设备;检索到的物理设备存储在 gpuList 向量中。 为了简单起见,我们假设系统只有一个 GPU(使用 gpuList 的第一个元素)。 接下来,我们使用 handShakeWithDevice()函数与设备握手:
/********** VulkanApplication.cpp **********/
// Get the list of physical devices on the system
vector<VkPhysicalDevice> gpuList; enumeratePhysicalDevices(gpuList);
if (gpuList.size() > 0) { appObj->handShakeWithDevice
(&gpuList[0], layerNames, deviceExtensionNames);
}
void VulkanApplication::initialize()
{
// Many lines skipped please refer to the source code.
// Get the list of physical devices on the system std::vector<VkPhysicalDevice> gpuList; enumeratePhysicalDevices(gpuList);
// This example use only one device which is available first.
if (gpuList.size() > 0) { handShakeWithDevice(&gpuList[0], layerNames,
deviceExtensionNames);
}
}
VulkanApplication :: enumeratePhysicalDevices()函数利用了变量 vkEnumeratePhysicalDevices 并顺便获得了物理设备(VKPhysicalDevice)的数量。 应用程序判断是否有 GPU 可用。 接下来,它分配必要的空间将这个信息存储在一个 vector 列表中,并再次将其提供给同一个 API(vkEnumeratePhysicalDevices)以及 GPU 计数,用来获取物理设备对象:
/***************** Application.cpp *****************/
VkResult VulkanApplication::enumeratePhysicalDevices
(vector<VkPhysicalDevice>& gpuList){ uint32_t gpuDeviceCount;
VkResult result = vkEnumeratePhysicalDevices (instanceObj.instance, &gpuDeviceCount, NULL);
assert(result == VK_SUCCESS);
gpuList.resize(gpuDeviceCount); assert(gpuDeviceCount);
result = vkEnumeratePhysicalDevices (instanceObj.instance, &gpuDeviceCount, gpuList.data());
assert(result == VK_SUCCESS);
return result;
}
VulkanApplication :: handShakeWithDevice()负责创建逻辑设备对象以及与它们关联的若干队列。 它还会执行一些初始化工作,这些工作在应用程序开发的后期阶段是必需的,例如获取物理设备的属性和内存属性。 以下是此 API 的语法:
void VulkanApplication::handShakeWithDevice (
VkPhysicalDevice* gpu,
std::vector<const char*>& extensions,
int queueIndex);
以下是对这些参数的描述:
gpu :这是应用程序执行握手的物理设备。 layers :这是需要在 GPU 上启用的层的名称。 extensions :这是需要在 GPU 上启用的扩展的名称。
VulkanApplication :: handShakeWithDevice()函数的内部过程描述如下:
- 使用了 VulkanDevice 对象并查询关联的物理设备公开的扩展。 检索到的扩展可以与应用程序请求的扩展进行交叉查询,以检查它们是否受物理设备所支持。
- 使用了 VulkanDevice 对象并查询关联的物理设备公开的扩展。 检索到的扩展可以与应用程序请求的扩展进行交叉查询,以检查它们是否受物理设备所支持。
- 使用 vkGetPhysicalDeviceMemoryProperties()API 获取由物理设备提供的内存信息及其属性。
- 使用 VulkanDevice 类的 getPhysicalDeviceQueuesAndProperties()辅助函数查询物理设备支持的所有队列族,并存储它们的属性供以后使用。
- 遍历所有的队列并检查哪个队列支持图形操作。 这是使用 getGraphicsQueueHandle()函数完成的;此函数返回(可用于执行图形操作的)队列的索引或句柄。
最后,调用 VulkanDevice :: createDevice()来创建与物理设备关联的逻辑设备对象。 该函数使用了图形队列句柄,并创建与逻辑设备对象关联的队列。 此外,该功能还接受需要在物理设备上启用的扩展名的列表作为参数:
/***************** Application.cpp *****************/
// High level function for creating device and queues
VkResult VulkanApplication::handShakeWithDevice( VkPhysicalDevice* gpu, std::vector<const char *>& layers, std::vector<const char *>& extensions )
{
// The user define Vulkan Device object.
// This will manage the Physical and logical
// device and their queue and properties
deviceObj = new VulkanDevice(gpu); if (!deviceObj){
return VK_ERROR_OUT_OF_HOST_MEMORY;
}
// Print the devices available layer and their extension
deviceObj->layerExtension.
getDeviceExtensionProperties(gpu);
// Get the physical device or GPU properties
vkGetPhysicalDeviceProperties(*gpu, &deviceObj->gpuProps);
// Get memory properties from the physical device or GPU.
vkGetPhysicalDeviceMemoryProperties(*gpu,
&deviceObj->memoryProperties);
// Query the availabe queues on the physical
// device and their properties.
deviceObj->getPhysicalDeviceQueuesAndProperties();
// Retrive the queue which support graphics pipeline.
deviceObj->getGrahicsQueueHandle();
// Create Logical Device, ensure that this
// device is connect to graphics queue
deviceObj->createDevice(layers, extensions);
return VK_SUCCESS;
}
总结
在本章中,我们开始了 Vulkan 编程。 我们知道了使用 CMake 和 LunarG SDK 设置和构建 Vulkan 项目的过程。 我们从 Vulkan 的基础知识开始,包括层和扩展,并学习了按照分步操作的过程来对它们进行查询。 我们创建了一个 Vulkan 实例,并演示了在实例级别启用以及测试层和扩展的方法。
此外,我们讨论了设备和队列。 我们研究了物理设备对象和逻辑设备对象之间的差异。 我们对系统上物理设备的枚举进行了编程示范,并学会了启用设备特定的扩展。 我们列举了每个物理设备关联的队列族。 使用队列属性,我们选择了图形队列并创建逻辑设备对象。
最后,我们总结了我们理解的所有内容,并实现了与设备握手的过程,其中包括创建物理设备对象和逻辑设备对象以及它们对应的若干队列。
调试,提供了一个学习 Vulkan 更好的机会,通过携带有效原因的错误信息也会获得更深的理解。 在下一章中,我们将学习 Vulkan 中的调试过程,从开发人员的角度来看这非常重要。 Vulkan 是一种新的图形 API,与传统的 API 有着完全不同的编程模式。 调试功能提供了一种理解这些 API 的更好方式。