Vulkan学习苦旅03:零号显卡,启动!(选择物理设备VkPhysicalDevcie)

随着近几年AI的迅速发展,GPU变得越来越抢手。然而,GPU的全称为Graphics Processing Unit, 从名字中就可以看出,GPU是为了处理图形而诞生的,后来才被应用到科学计算等领域中。

1. 我们眼中的GPU

GPU的架构大致如下:

其中,标有SP(Streaming Processor)的方格是一个小处理器,它比CPU的功能要弱很多,但它们在GPU中的数量很多,每一个SP可以处理一部分数据,很多个SP就能处理大量的数据啦。

不必在意GPU具体是怎么设计的,我们关心的问题只有两个:

1. GPU如何保存我们的数据?

2. 如何让GPU处理数据?

对于第一个问题,答案很简单:GPU有自己的内存,我们需要把数据传到GPU上。值得注意的是,Vulkan中抽象出了两种不同的内存:一种是VkBuffer, 这种内存基本上就是我们平时所说的主机内存,是一个连续的二进制块;另一种是VkImage, 仔细观察上面这张图,其中有一个名为Texture Unit的块(texture的意思是纹理,日常使用的jpg/png图片就可以作为纹理)。把纹理与普通的内存区分开来出于实际需要,目前只要知道二者有区别即可。

对于第二个问题,GPU通过队列(Queue)管理命令,用户通过调用形如vkCmdXXX的函数将命令传到队列,之后GPU会依次执行这些命令。队列类似于日常生活中的排队,先到先得,先传送到GPU的命令会先执行。队列有很多种,不同的队列有不同的功能,例如计算队列、图形队列等等。GPU中至少有一个队列,多个功能相同的队列构成了一个队列族(Queue Family). 

上面这些,正如“地图”的第二层所画的那样:

2. 查找可用的物理设备

以后,我们会输出各种信息,为了少写点代码,定义宏Log用于输出信息:

#define Log(message) std::cout << "[INFO] " << message << std::endl

例如,Log("Here is a bug"); 会被替换为 std::cout << "[INFO] " << "Here is a bug" << std::endl;.

另外还定义了一个功能相似的宏Error, 在程序出错时输出错误信息并终止程序:

#define Error(message) std::cerr << "[ERROR] " << message << std::endl; exit(-1)

之前我们我们实现了几个名为createXXX的函数,用于创建某个特定的对象,但物理设备显然不需要新建,只需要选择合适的物理设备即可,因此接下来我们要实现的函数为selectPhysicalDevice, 用于选择满足特定要求的设备。

除了GPU外,Vulkan还支持其它类型的物理设备,我们默认“物理设备”一词指的是GPU.

首先在VulkanApp类中添加以下成员表示物理设备:

VkPhysicalDevice mPhysicalDevice;  // 物理设备

使用vkEnumeratePhysicalDevices查找所有可选的物理设备:

VkResult vkEnumeratePhysicalDevices(
    VkInstance instance,
    uint32_t& pPhysicalDeviceCount,
    VkPhysicalDevice& pPhysicalDevices);

这个函数会将一个VkPhysicalDevice数组返回给用户,其中:

instance: 上一篇文章中创建的实例对象mInstance;

pPhysicalDeviceCount: 数组的大小;

pPhysicalDevices: 数组的首地址。当此参数为空指针nullptr时,此函数会向pPhysicalDeviceCount指向的位置写入设备的数量。

从而,查找物理设备的代码如下:

/* 查找所有可选的物理设备 */
uint32_t physicalDeviceCount = 0;
vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, nullptr);
vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, physicalDevices.data());

此函数的第一个参数是上一篇博客中创建的实例对象mInstance, 后面两个参数分别是两个指针,分别指向物理设备的数量和一个

首先,将函数vkEnumeratePhysicalDevices的最后一个参数设置为nullptr, 从而physicalDeviceCount会获取到可用的设备数量;随后,使用vector分配相应大小的内存;最后,将内存的首地址传入函数,此函数会向内存写入数据。

以上代码展示了从Vulkan函数中获取一个数组的套路:

  1. 获取大小
  2. 分配相应大小的内存
  3. 将分配好内存的首地址传入函数,由函数向相应位置写入数据。

从Vulkan的函数中获取数组基本上都类似于上面这种写法,以后再遇到时不再另外说明。

3. 物理设备的属性与特性

上一节中我们获得了可用物理设备的数组, 可以通过VkPhysicalDevice查找物理设备的属性(Property)与特性(Feature)。

属性与特性的区别

属性是物理设备的一些性质,例如显卡的名称、版本号等;而特性是物理设备支持的功能。如果用人作类比,身高、体重是人的属性,会写代码、会弹钢琴是人的特性。

定义结构体VkPhysicalDeviceProperties,并使用特定的函数获取物理设备的属性与特性信息:

for (VkPhysicalDevice physicalDevice : physicalDevices) {
	/* 获取显卡的属性 */
	VkPhysicalDeviceProperties physicalDeviceProperties;
	vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);
	/* 获取显卡的特性 */
	VkPhysicalDeviceFeatures physicalDeviceFeatures;
	vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);

	/* 输出所有可选的设备名称 */
	Log("device name: " << physicalDeviceProperties.deviceName);
}

代码的最后加了一条输出信息,现在运行代码,应该能看到所有可选设备的名称。

一个悲伤的故事

本人最初的游戏本下岗后,为了戒掉游戏瘾,买了台不带独显、只带有一块集显的笔记本,这样的电脑是玩不动3A大作的。如果这块集显不支持Vulkan, 那我也没办法继续学下去了,这也是标题为“零号显卡”的原因——电脑中只有一块可选的集显。

好吧,我现在有点后悔了。

4. 物理设备的内存和队列

类似地,我们也可以查找物理设备支持的内存和队列。

 

以下代码可以获取物理设备的内存信息:

/* 获取显卡的内存信息 */
VkPhysicalDeviceMemoryProperties physicalDeviceMemoryProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &physicalDeviceMemoryProperties);

Log("device supports " << physicalDeviceMemoryProperties.memoryTypeCount << " types of memory:");
for (int i = 0; i < physicalDeviceMemoryProperties.memoryTypeCount; i++) {
	auto flags = physicalDeviceMemoryProperties.memoryTypes[i].propertyFlags;
	if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
		Log("    device local memory");
	}
}

上述代码只是个演示,由于目前了解物理设备的内存信息并没有什么用处,所以在此不过多介绍,感兴趣的朋友可自行查找文档。

 

获取队列就比较重要了。之前提到过,相同功能的队列集合构成一个队列族(Queue Family), 需要查找物理设备支持哪些功能的队列族(例如计算功能、图形功能等等)。以下是获取队列族信息的代码:

/* 获取队列族的信息 */
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilies.data());

有了之前获取物理设备的经验,相信以上代码对你来说一定是小菜一碟。其中,结构体VkQueueFamilyProperties描述了一个队列族的信息,其定义如下:

typedef struct VkQueueFamilyProperties {
    VkQueueFlags queueFlags;  // 队列族的功能
    uint32_t queueCount;  // 队列族的队列个数
    uint32_t timestampValidBits;
    VkExtent3D minImageTransferGranularity;
} VkQueueFamilyProperties;

其中:

queueFlags:描述了队列族的功能,如果支持某项功能,相应的位置就会被设置为1。例如,如果队列族支持图形功能,那么queueFlags & VK_QUEUE_GRAPHICS_BIT的结果就不为零;

queueCount: 此队列族中队列的个数;

另外两个成员不用管它们。

5. 选择合适的物理设备

物理设备需要支持下列功能:

  1. 支持交换链(Swapchain)扩展(至于什么是交换链,之后的文章会加以说明)
  2. 支持几何着色器(Geometry Shader) (同上,之后的文章会加以说明)
  3. 支持图形队列功能;
  4. 支持显示功能。

5.1. 检查交换链扩展

首先在VulkanApp类中添加成员mRequiredExtensions, 表示需要的扩展:

const vector<const char*> mRequiredExtensions = {
	VK_KHR_SWAPCHAIN_EXTENSION_NAME,  // 等价于字符串"VK_KHR_swapchain"
};

之后,获取设备支持的扩展信息:

/* 获取物理设备支持的扩展信息 */
uint32_t extensionCount = 0;
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data());

接下来,检查mRequiredExtensions中的字符串是否都在availableExtensions中(结构体VkExtensionProperties有一个成员extensionName,是一个指向扩展名称字符串的指针)。下面使用了<cstring>中的strcmp函数比较字符串是否相等,代码如下:

for (const char* requiredExtensionName : mRequiredExtensions) {
	bool isSupported = false;
	for (const auto& availableExtension : availableExtensions) {
		if (strcmp(requiredExtensionName, availableExtension.extensionName) == 0) {
			isSupported = true;
			Log("extension " << requiredExtensionName << " is supported");
			break;
		}
	}
	if (isSupported == false) {
		Log("extension " << requiredExtensionName << " is not supported");
	}
}

以上代码没什么技术含量,或许你可以实现个更好的。

5.2. 检查是否支持几何着色器

这一信息包含在物理设备的特性内,之前介绍过如何获取物理设备的特性信息,只要检查结构体VkPhysicalDeviceFeatures中的成员geometryShader(类型为VkBool32)是否为True即可:

/* 2. 检查设备是否支持几何着色器 */
VkPhysicalDeviceFeatures physicalDeviceFeatures;
vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);
if (physicalDeviceFeatures.geometryShader) {
	Log("geometry shader is supported");
}
else {
	Log("geometry shader is not supported");
}

5.3. 检查是否支持图形功能

我们需要找到一个支持图形功能的队列族,并记录下此队列族的索引(之后会用到)。为此,首先在VulkanApp类中定义一个成员变量,用于记录图形队列族的索引:

int mGraphicsQueueFamilyIndex;  // 支持图形功能的队列族索引

上一节介绍过如何获取队列族的信息。接下来,只要依次检查每一个队列族,即检查结构体VkQueueFamilyProperties中的queueFlags位,直到找到合适的队列族:

for (int i = 0; i < queueFamilyCount; i++) {
	/*  5.3. 检查是否支持图形功能 */
	if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
		Log("graphics is supported");
		mGraphicsQueueFamilyIndex = i;  // 保留队列族的索引
	}
	else {
		Log("graphics is not supported");
	}
}

5.4. 检查是否支持显示功能

类似于图形功能,我们需要找到一个支持显示功能(与VkSurfaceKHR相关)的队列族,并记录下此队列族的索引。首先在VulkanApp中定义一个成员变量记录索引:

int mPresentQueueFamilyIndex;  // 支持显示功能的队列族索引

使用函数vkGetPhysicalDeviceSurfaceSupportKHR检查:

for (int i = 0; i < queueFamilyCount; i++) {
	/*  5.3. 检查是否支持图形功能 */
	......

	/* 5.4. 检查是否支持显示功能 */
	VkBool32 isPresentSupport = false;
	vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, mSurface, &isPresentSupport);
	if (isPresentSupport) {
		mPresentQueueFamilyIndex = i; 
		Log("find present queue family index " << i);
	}
	else {
		Log("present is not supported");
	}
}

vkGetPhysicalDeviceSurfaceSupportKHR需要传入的参数有:物理设备(VkPhysicalDevice), 队列族的索引和上一节创建的表面对象(mSurface), 最后一个参数是指向VkBool32的指针,函数向其中写入检查的结果。

 

如果待选的物理设备通过了以上四重考验,那么我们就可以选择此设备:

mPhysicalDevice = physicalDevice;

6. 到目前为止的完整代码

现在运行代码,应该能看到以下输出:

值得注意的是,在这里,图形与显示选择的是同一个队列族,都是0号队列族。当然,它们也可以分属不同的队列族,这会影响到后面的一部分代码,到时候再说。

下一节,我们将研究逻辑设备(VkDevice),或者简称为设备,它是对物理设备的抽象。之后可以看到,大多数API都需要一个设备作为参数。

写到这里,我遇到了MSVC编译器的一个小bug:某行形如/* XXX */的中文注释,使得编译器在预处理阶段错误地删去了此注释后面的部分代码。出错的原因与文件的编码格式相关,有一种解决方案是在注释的前后空2个空格,例如/*  XXX  */.

到目前为止的完整代码
 #define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include <iostream>
#include <vector>
using std::vector;
#include <cstring>

#define Log(message) std::cout << "[INFO] " << message << std::endl
#define Error(message) std::cerr << "[ERROR] " << message << std::endl; exit(-1)

static void if_fail(VkResult result, const char* message);


class VulkanApp {
public:
	VulkanApp() {
		glfwInit();  // 初始化glfw库
		glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);  // 禁用OpenGL相关的API
		glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);  // 禁止调整窗口大小

		createInstance();
		createSurface();

		selectPhysicalDevice();
	}

	~VulkanApp() {
		vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
		vkDestroyInstance(mInstance, nullptr);

		glfwDestroyWindow(mWindow);
		glfwTerminate();
	}

	void Run() {
		while (!glfwWindowShouldClose(mWindow)) {
			glfwPollEvents();
		}
	}

private:
	const vector<const char*> mRequiredLayers = {
		"VK_LAYER_KHRONOS_validation"
	};
	const vector<const char*> mRequiredExtensions = {
		VK_KHR_SWAPCHAIN_EXTENSION_NAME,  // 等价于字符串"VK_KHR_swapchain"
	};
	VkInstance mInstance;  // 实例
	VkPhysicalDevice mPhysicalDevice;  // 物理设备
	int mGraphicsQueueFamilyIndex = -1;  // 支持图形功能的队列族索引
	int mPresentQueueFamilyIndex = -1;  // 支持显示功能的队列族索引

	int mWidth = 800;  // 窗口宽度
	int mHeight = 600;  // 窗口高度
	GLFWwindow* mWindow = nullptr;  // glfw窗口指针
	VkSurfaceKHR mSurface;

	void createInstance() {
		/* 填充VkApplicationInfo结构体 */
		VkApplicationInfo appInfo{
			VK_STRUCTURE_TYPE_APPLICATION_INFO,  // .sType
			nullptr,  // .pNext
			"I don't care",  // .pApplicationName
			VK_MAKE_VERSION(1, 0, 0),  // .applicationVersion
			"I don't care",  // .pEngineName
			VK_MAKE_VERSION(1, 0, 0),  // .engineVersion
			VK_API_VERSION_1_0,  // .apiVersion
		};

		/* 获取glfw要求支持的扩展 */
		uint32_t glfwExtensionCount = 0; 
		const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

		/* 输出glfw所需的扩展 */
		std::cout << "[INFO] glfw needs the following extensions:\n";
		for (int i = 0; i < glfwExtensionCount; i++) {
			std::cout << "    " << glfwExtensions[i] << std::endl;
		}

		/* 填充VkInstanceCreateInfo结构体 */
		VkInstanceCreateInfo instanceCreateInfo{
			VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,  // .sType
			nullptr,  // .pNext
			0,  // .flags
			&appInfo,  // .pApplicationInfo
			mRequiredLayers.size(),  // .enabledLayerCount
			mRequiredLayers.data(),  // .ppEnabledLayerNames
			glfwExtensionCount,  // .enabledExtensioncount
			glfwExtensions,  // .ppEnabledExtensionNames
		};

		/* 如果创建实例失败,终止程序 */
		if_fail(
			vkCreateInstance(&instanceCreateInfo, nullptr, &mInstance),
			"failed to create instance"
		);
	}

	void createSurface() {
		mWindow = glfwCreateWindow(mWidth, mHeight, "Vulkan App", nullptr, nullptr);  // 创建glfw窗口
		if (mWindow == nullptr) {
			std::cerr << "failed to create window\n";
			exit(-1);
		}

		/* 创建VkSurfaceKHR对象 */
		if_fail(
			glfwCreateWindowSurface(mInstance, mWindow, nullptr, &mSurface),
			"failed to create surface"
		);
	}

	void selectPhysicalDevice() {
		/* 查找所有可选的物理设备 */
		uint32_t physicalDeviceCount = 0;
		vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, nullptr);
		vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
		vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, physicalDevices.data());

		mPhysicalDevice = VK_NULL_HANDLE;

		for (VkPhysicalDevice physicalDevice : physicalDevices) {
			/* 1. 检查物理设备是否支持扩展 */
			/* 获取物理设备支持的扩展信息 */
			uint32_t extensionCount = 0;
			vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
			vector<VkExtensionProperties> availableExtensions(extensionCount);
			vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data());

			bool isAllRequiredExtensionsSupported = true;  // 检查此物理设备是否支持所有的扩展
			for (const char* requiredExtensionName : mRequiredExtensions) {
				bool isSupported = false;
				for (const auto& availableExtension : availableExtensions) {
					if (strcmp(requiredExtensionName, availableExtension.extensionName) == 0) {
						isSupported = true;
						break;
					}
				}
				if (isSupported == false) {
					isAllRequiredExtensionsSupported = false;
					break;
				}
			}
			if (isAllRequiredExtensionsSupported) {
				Log("all required extensions are supported");
			}
			else {
				continue;
			}

			/* 2. 检查物理设备是否支持几何着色器 */
			VkPhysicalDeviceFeatures physicalDeviceFeatures;
			vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);
			if (physicalDeviceFeatures.geometryShader) {
				Log("geometry shader is supported");
			}
			else {
				continue;
			}

			/* 获取队列族的信息 */
			uint32_t queueFamilyCount = 0;
			vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
			vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
			vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilies.data());

			for (int i = 0; i < queueFamilyCount; i++) {
				/*  5.3. 检查是否支持图形功能 */
				if (mGraphicsQueueFamilyIndex < 0 && (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) {
					Log("find graphics queue family index " << i);
					mGraphicsQueueFamilyIndex = i;  // 保留队列族的索引
				}

				/*  5.4. 检查是否支持显示功能  */
				if (mPresentQueueFamilyIndex < 0) {
					VkBool32 isPresentSupport = false;
					vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, mSurface, &isPresentSupport);
					if (isPresentSupport) {
						mPresentQueueFamilyIndex = i;
						Log("find present queue family index " << i);
					}
					else {
						Log("present is not supported");
					}
				}
			}

			if (mGraphicsQueueFamilyIndex >= 0 && mPresentQueueFamilyIndex >= 0) {
				mPhysicalDevice = physicalDevice;

				/*  获取物理设备的属性  */
				VkPhysicalDeviceProperties physicalDeviceProperties;
				vkGetPhysicalDeviceProperties(mPhysicalDevice, &physicalDeviceProperties);
				Log("select physical device: " << physicalDeviceProperties.deviceName);
			}
		}

		/* 如果没找到合适的物理设备 */
		if (mPhysicalDevice == VK_NULL_HANDLE) {
			Error("can't find suitable physical device");
		}
	}
};


int main() {
	VulkanApp app;
	app.Run();
}

static void if_fail(VkResult result, const char* message) {
	if (result != VK_SUCCESS) {
		std::cerr << "[error] " << message << std::endl;
		exit(-1);
	}
}

 

posted @ 2024-01-30 00:20  overxus  阅读(412)  评论(0编辑  收藏  举报