【Vulkan学习记录-基础篇-1】用Vulkan画一个三角形

本文是Vulkan学习系列的第一篇,介绍了如何在Windows环境中使用Vulkan API从零开始绘制一个三角形。首先讲解了初始化窗口、检查校验层支持、创建Instance和设置校验层回调,接着详细阐述了创建物理设备、Surface、逻辑设备和资源的过程。最后,文章展示了如何执行渲染并显示图像,以及如何销毁资源。本文适合有OpenGL或Direct3D基础,对Vulkan感兴趣的读者阅读。
摘要由CSDN通过智能技术生成

好久没有更新过博客了,上半年一直忙着找实习的事情,不过现在已经入职一段时间了,也可以抽出时间来继续整理一些内容,所以最近会尽量变得勤快一点来写博客。

Vulkan是新一代的图形API,具有跨平台、高性能的优势,它强调减少对驱动的依赖性,和传统的图形API(例如OpenGL、Direct3D)相比,它需要程序员自己在程序方面做以往驱动做的事情,因此Vulkan的代码量会比传统的图形API多很多,学习起来也相对的困难一点。我自己也是处于摸索阶段,所以难免会有不正确的地方,希望读者能不吝斧正。

不过学习Vulkan至少需要有OpenGL或者Direct3D的使用经验,需要对某一些概念有一些基本的认识(swapchain、shader、pipeline等),如果没有可能不太适合阅读。

这一篇就介绍一下如何在Vulkan中画一个三角形,以此来熟悉Vulkan API的一些基本操作。

我所使用的环境是:Windows7+VS 2019+GLFW+Vulkan SDK1.1.106.0

下面来逐步进行介绍。

1-初始化窗口

使用GLFW来创建一个窗口非常容易,只需要:

	// 1. Init Window
	glfwInit();
	glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
	glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
	window = glfwCreateWindow( width , height , "Vulkan window", nullptr, nullptr);

第二行:GLFW本身被设计为会在Init时创建一个OpenGL的Context,但是我们这里是在使用Vulkan,因此就需要把这个过程给取消掉
第三行:禁止窗口拉伸(窗口拉伸会涉及到重新创建SwapChain,这个后续再考虑)

2-检查是否支持校验层

Vulkan的设计理念是减少对驱动的依赖,因此在默认情况下只提供了非常有限的错误检查,程序员需要高度谨慎地对待自己所写的每一行程序,所以如果向API传入错误的参数往往不会有错误的反馈,而是会像正常情况一样处理,这可能会直接导致程序的崩溃。因此在写程序时犯的一些小错误可能会导致非常严重的后果,但是却不能被及时的排查。
不过幸运的是Vulkan SDK提供了一个名叫校验层的东西,它可以用来做参数检查、内存泄漏监测、API调用详细记录、线程安全检查等非常有用的工作,但是在默认的情况下都是不开启的。如果想要启用,就必须先检查当前环境是否支持校验层:

	// 2. check validation layer support 
	uint32_t layerCount;
	vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
	std::vector<VkLayerProperties> availableLayers(layerCount);
	vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
	
	std::vector<const char*> validationLayerNames = { "VK_LAYER_LUNARG_core_validation" };
	for (auto validationLayerName : validationLayerNames)
	{
		bool support = false;
		for (auto supportedLayerName : availableLayers)
		{
			if (strcmp(supportedLayerName.layerName, validationLayerName) == 0)
			{
				support = true;
				break;
			}
		}
		if (support == false)
		{
			std::cout << validationLayerName << " is not supported . " << std::endl;
			throw std::runtime_error(" not all validation layer is supported . ");
		}
	}

首先先获取当前设备环境下所能够支持的所有Layer,然后将需要开启的Layer的名cheng添加在validationLayerNames中,随后检查这些需要的validationLayer是否都被包含在能够支持的所有Layer中。
那么到底需要在validationLayerNames中填入哪些名称呢?
在这里插入图片描述

根据自己的需求来选择其中的一种或多种Layer填入即可,比如如果只需要检查Vulkan API中创建的Object是否有被及时地销毁而没有造成内存泄漏,那么就可以只添加VK_LAYER_LUNARG_object_tracker

3-创建Instance并设置校验层回调函数

如果当前的环境的确支持校验层,那么怎么样才能将其开启并提供一种获取校验层所检测出来的错误的方法呢?这个过程可以在创建Instance时完成。Instance存储了每一个独立的应用程序所具有的独立的状态,在官方的文档中有这样一幅图:
在这里插入图片描述
这是Vulkan的一个基本的架构,通过一个名叫Loader的东西将应用程序和Vulkan的各个Layer连接起来,注意这里所说的Layer通常都是被用来做校验层,然后再和底层的驱动连接。在Vulkan中,驱动层会比传统的图形API轻量很多,因为所有的错误检测都被转移到了校验层,而传统图形API都是在驱动中做错误检测。
在创建Instance时,这个Loader会随之初始化,然后Loader就会加载并初始化底层的图形驱动(通常由GPU硬件提供 ),因此对于任何一个Vulkan应用程序,我们都需要先创建Instance:

3.1-设置应用程序信息

	// 3.1 fill the application info
	VkApplicationInfo appInfo = {};
	appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
	appInfo.pApplicationName = "Hello Triangle";
	appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
	appInfo.pEngineName = "No Engine";
	appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
	appInfo.apiVersion = VK_API_VERSION_1_0;
	appInfo.pNext = NULL;

Instance记录了每个应用程序独立的信息,因此我们需要先填充这样一个记录了应用程序基本信息的结构体,大部分的成员都可以直接望文生义,其中需要注意的是pNext,在有的时候可能不只是需要一个Info,可能需要多种不同的Info结构,通过这样一个指针就可以将所有的这些结构体链接起来,但是在这里不需要,直接设置为NULL即可。

3.2-获取应用程序所要求必须有的扩展
所谓扩展,就是指Vulkan中实现的一些可选的功能,它们可能会添加一些新的函数、结构体,也有可能会修改已有函数的功能。对于每个不同的应用程序,它们在不同的平台上都有可能会要求不同的扩展,在创建Instance时会启用所有的这些扩展,因此我们需要先获取这个信息:

	// 3.2 get required extensions 
	uint32_t glfwExtensionCount = 0;
	const char** glfwExtensions;
	glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

在glfw中可以通过一个简单的函数调用完成这个过程。

3.3-添加校验层回调扩展
启用校验层只需要将所需要的Layer传递到之后的InstanceCreateInfo中即可,但是仅仅启用校验层还是不够的,我们需要的是能够将校验层的错误信息显示出来,因此我们需要有一个回调函数,它专门用于接收校验层的信息并以某种方式输出。但是要想启用这样的一个回调机制,必须先启用相应的扩展:

	// 3.3 add validation layer required extension 
	std::vector<const char*> required_extensions(glfwExtensions , glfwExtensions + glfwExtensionCount);
	required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);

先将glfwExtensions这个名称字符串数组中的所有元素拷贝到一个Vector中,然后再往vector中添加启用校验层回调的扩展名,这个名称可以由VK_EXT_DEBUG_UTILS_EXTENSION_NAME宏来获取。

3.4-填充InstanceCreateInfo并创建instance

	// 3.4 fill the instance create info and create
	VkInstance instance;
	VkInstanceCreateInfo instanceCreateInfo = {};
	instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
	instanceCreateInfo.pApplicationInfo = &appInfo;
	instanceCreateInfo.enabledExtensionCount = static_cast<uint32_t>(required_extensions.size());
	instanceCreateInfo.ppEnabledExtensionNames = required_extensions.data();
	instanceCreateInfo.enabledLayerCount = validationLayerNames.size();
	instanceCreateInfo.ppEnabledLayerNames = validationLayerNames.data();

	if (vkCreateInstance(&instanceCreateInfo, nullptr, &instance) != VK_SUCCESS) {
		throw std::runtime_error("failed to create instance!");
	}

创建一个object往往需要先填充它的createInfo,enabledExtensionCount和ppEnabledExtensionNames决定了应用程序需要启用哪些扩展,而enabledLayerCount和ppEnabledLayerNames决定了应用程序所启用的校验层。
注意vkCreateInstance函数的第二个参数传递的是一个指向内存分配器的指针,它用于自定义内存分配机制,这里我们不需要,直接设置为NULL即可。

3.5-设置回调函数
首先先实现一下输出校验层错误信息的回调函数:

static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
	VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
	VkDebugUtilsMessageTypeFlagsEXT messageType,
	const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
	void* pUserData) {

	std::cout << "validation layer: " << pCallbackData->pMessage << std::endl;

	return VK_FALSE;
}

函数的参数解释:
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity:校验层检测结果的严重程度,它有四个取值:

取值 含义
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT 正常的诊断信息
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 诸如创建资源的通知类信息
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT 警告信息,不一定是错误,但是极有可能是一个应用程序中的BUG
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT 严重的错误信息,可能会导致应用程序的崩溃

可以根据这个参数来决定是否要将校验层返回的信息输出

VkDebugUtilsMessageTypeFlagsEXT messageType:校验层检测行为的类型,它有三个取值:

取值 含义
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT 与规范或者性能不相关的行为
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT 不符合规范或者有可能是一个潜在的错误的行为
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT 可能的未被优化的影响性能的行为

const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData:指向具体的回调信息结构体,该结构体有三个成员:

成员 含义
pMessage 回调的具体信息字符串
pObjects 与该信息有关的Vulkan Object数组
objectCount 数组中元素的个数

void* pUserData:指向用户自定义的数据,可以在设置回调函数时指定

然后是具体的设置回调函数:

	// 3.5 set up debug messenger
	VkDebugUtilsMessengerCreateInfoEXT messengerCreateInfo = {};
	messengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
	messengerCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
	messengerCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
	messengerCreateInfo.pfnUserCallback = debugCallback;
	messengerCreateInfo.pUserData = nullptr;

	auto func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
	if (func == NULL)
	{
		throw std::runtime_error("get vkCreateDebugUtilsMessengerEXT fault.");
	}
	VkDebugUtilsMessengerEXT debugMessenger;
	if (func(instance, &messengerCreateInfo, NULL, &debugMessenger) != VK_SUCCESS )
	{
		throw std::runtime_error("set debug messenger fault . ");
	}

首先是填充VkDebugUtilsMessengerCreateInfoEXT,messageSeverity和messageType决定了该回调函数能够接受的严重性和行为类型,pfnUserCallback决定了具体的回调函数,pUserData是用户自定义的数据。

随后我们需要使用vkCreateDebugUtilsMessengerEXT函数来指定回调函数,这个函数属于扩展中的函数,因此它并不被Vulkan默认加载,需要在运行时手动进行加载,使用vkGetInstanceProcAddr来获取该函数的定义。指定回调时需要创建一个VkDebugUtilsMessengerEXT,以便后续对这个回调操作的撤销。
值得注意的是vkCreateDebugUtilsMessengerEXT函数的第三个参数依旧是自定义分配器,这里直接设置为NULL即可。

至此如果设置的准确无误,那么应该能够看到控制台下有输出信息:
在这里插入图片描述

4-创建物理设备

4.1 获取所有的物理设备
物理设备指的就是配置在计算上的用于图形显示、渲染的GPU设备,一台计算机可能配置的有多个GPU,我们需要选择其中一个能够满足需求的GPU

// 4.1 enumerate all physical device 
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
if (deviceCount == 0)
{
	throw std::runtime_error("failed to find physical device .");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

这段代码比较简单,就是先获取物理设备的总数,然后再分配一个总数大小的Vector,再将所有的物理设备句柄存到这个Vector中。

4.2 选择合适的物理设备
需要对GPU的类型和支持的特性做选择,比如我们需要独立显卡和支持geometryShader的GPU,就可以这样写:

	// 4.2 choose a suitable physical device 
	for (const auto& device : devices)
	{
		VkPhysicalDeviceProperties deviceProperties;
		VkPhysicalDeviceFeatures deviceFeatures;
		vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
		vkGetPhysicalDeviceProperties(device, &deviceProperties);

		if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
			deviceFeatures.geometryShader)
		{
			physicalDevice = device;
			break;
		}
	}
	
	if (physicalDevice == VK_NULL_HANDLE)
	{
		throw std::runtime_error("no suitable device.");
	}

很明显,VkPhysicalDeviceProperties表示显卡的类型,而VkPhysicalDeviceFeatures表示显卡所支持的特性。

5-创建Surface

如果直接使用Vulkan控制GPU进行渲染,那么最终渲染的结果是根本看不到的,因为它没有和应用程序窗口连接在一起,Surface就提供了连接的功能,我们先根据窗口的句柄创建一个Surface,再在后面的阶段让它把绘制的结果和窗口联系在一起:

	// 5. create surface 
	VkSurfaceKHR surface;
	VkWin32SurfaceCreateInfoKHR surfaceCreateInfo = {};
	surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
	surfaceCreateInfo.hwnd = glfwGetWin32Window(window);
	surfaceCreateInfo.hinstance = GetModuleHandle(nullptr);

	if (vkCreateWin32SurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface))
	{
		throw std::runtime_error(" failed to create window surface .");
	}

Vulkan是与平台不相关的,因此这里的实现根据操作系统的不同会略有区别,具体可以参考官方的文档。

6.准备创建逻辑设备和队列

6.1-选择合适的队列族
Vulkan中所有的操控GPU的操作,都被抽象为一个任务,任务主要有图像渲染类、计算类、内存转移类这几种类型,在执行操作时都需要将任务添加到一个队列中,然后队列将任务传递给GPU,队列也是唯一能够完成这项工作的组件。
每一个队列都来自一个队列族,一个队列族定义了能够接受的命令类型集合,比如有的队列族只能做计算调用,那么它里面的所有队列就不能接受内存转移类的任务。在之后创建逻辑设备和设备队列的时候,都需要用到这个队列族,因此这个阶段需要先选择合适的队列族:

	// 6.1 find a queueFamily which can be a graphics queue and a present queue
	uint32_t queueFamilyCount = 0;
	vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, NULL);
	std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
	vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilies.data());

	int i = 0;
	int queue_ind = -1;

	for (const auto& queueFamily : queueFamilies) {
		VkBool32 presentSupport = false;
		vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, surface, &presentSupport);
		if (queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT && presentSupport)
		{
			queue_ind = i;
		}
		i++;
	}

	if (queue_ind == -1) {
		throw std::runtime_error("No suitable queue .");
	}

注意将图像呈现到窗口上也是一种队列的特性,并不是所有的队列都能支持这项任务,因此需要选择那些支持图形渲染工作并且能够将渲染的图像呈现到窗口上的队列。
vkGetPhysicalDeviceSurfaceSupportKHR可以判定根据给定的队列族编号来判定该队列族下的队列是否支持将图像呈现到窗口上。

6.2-填充VkDeviceQueueCreateInfo

	VkDeviceQueueCreateInfo queueCreateInfo = {};
	queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
	queueCreateInfo.queueFamilyIndex = queue_ind;
	queueCreateInfo.queueCount = 1;
	float queuePriority = 1.0f;
	queueCreateInfo.pQueuePriorities = &queuePriority;

这里我们填充创建一个队列所需要的信息,这些参数都很好理解。其中queuePriority的取值是0-1的浮点数,它表示在命令调度的时候相应队列的优先级。

值得注意的是这里并没有随之创建队列,因为创建队列在创建逻辑设备时同时完成的,在创建逻辑设备时需要提供一个VkDeviceQueueCreateInfo来同时创建队列,所以我们将6.1和6.2这两步放在一个阶段内,它们都是为了创建逻辑设备和队列做准备的。

7-创建逻辑设备并使用逻辑设备创建资源

7.1 检查物理设备是否支持需要的扩展
逻辑设备是作为用户操控物理设备的一个接口,也就是说通过逻辑设备来创建资源,本质上还是在用物理设备来创建,只是我们并不能直接去操控物理设备。在创建逻辑设备之前,还需要对物理设备先做一个关于扩展的支持性检查。前面在创建Instance时已经做过了一次跟扩展相关的工作,那么这里为什么还需要处理一个跟扩展有关的问题呢?
此前在Instance中检测了应用程序所必须要求的扩展并且添加了校验层回调的扩展,但是这都是在处理应用程序层面上的扩展,影响因素可能是应用程序所处的运行环境,VulkanSDK的版本等(个人猜测)。
而现在我们需要做的是检测物理设备的扩展,影响因素是GPU本身的功能,这里主要就检测一项——创建swapchain的扩展,并不是所有的GPU都能够支持创建swapchain然后将渲染的结果呈现到屏幕上,因为有一些GPU是专门给服务器设计的,它们并不需要将图形渲染的结果反馈到屏幕上,自然也就不会支持创建swapchain了。

	// 7.1 check whether the physical device support the required extensions 
	const std::vector<const char*> requireExtensions = {
		VK_KHR_SWAPCHAIN_EXTENSION_NAME
	};

	uint32_t availableExtensionCount;
	vkEnumerateDeviceExtensionProperties(physicalDevice, NULL, &availableExtensionCount, NULL);
	std::vector<VkExtensionProperties> availableExtensions(availableExtensionCount);
	vkEnumerateDeviceExtensionProperties(physicalDevice, NULL, &availableExtensionCount, availableExtensions.data());

	std::set<std::string> requireExtensionNames(requireExtensions.begin(), requireExtensions.end());
	for (const auto& extension : availableExtensions)
	{
		requireExtensionNames.erase(extension.extensionName);
	}

	if (!requireExtensionNames.empty()) {
		throw std::runtime_error("extension not fulfill");
	}

这段程序先获取物理设备所支持的所有扩展,然后在需要的扩展中删掉被物理设备支持的扩展,然后最后判定需要的扩展是否为空,如果不为空说明该物理设备并不支持我们的需求。
VK_KHR_SWAPCHAIN_EXTENSION_NAME宏就是支持创建swapchain的扩展名。

7.2 创建逻辑设备和队列

	// 7.2 create logical device 
	VkDevice logicalDevice;
	VkPhysicalDeviceFeatures deviceFeatures = {};
	VkDeviceCreateInfo deviceCreateInfo = {};
	deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
	deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
	deviceCreateInfo.queueCreateInfoCount = 1;
	deviceCreateInfo.pEnabledFeatures = &deviceFeatures;
	deviceCreateI
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值