Vulkan学习苦旅02:看不见的窗口(创建VkInstance与VkSurfaceKHR)

  在上一篇博客中,我们对Vulkan有了初步的了解。今天,我们将会了解到“地图”顶层的内容。 

  如图所示,“地图”的顶层有两个模块: Instance和SurfaceKHR. 其中,Instance表示应用程序的一个实例,它用于保存一些状态,我们可以在一个应用程序中创建多个实例,但目前我们只创建一个实例;而SurfaceKHR与图像在屏幕上的显示有关。

Vulkan并不仅仅是图形API,它还可以实现纯计算的任务,此类任务并不需要将结果显示到窗口上,因此,显示功能是Vulkan的扩展功能,而不是核心功能。

  接下来将正式创建Instance和SurfaceKHR对象,并在此过程中了解到创建Vulkan对象的一些套路。现在,让我们新建一个应用程序类吧。

1. VulkanApp类

  VulkanApp类的框架如下:

class VulkanApp {
public:
    VulkanApp() {}
    ~VulkanApp() {}
    void Run() {}
private:
    // TODO: 此处添加变量和函数
};

在VulkanApp类中,构造函数用于创建各种Vulkan对象,在析构函数中,按照与创建顺序相反的顺序销毁这些Vulkan对象。此外,还有一个Run方法,用于一遍又一遍地在屏幕上绘制窗口。接下来,我们将会在这个类中定义一系列的私有成员变量和私有成员函数(即写在private的下方).

  在main函数中,按照以下方式使用VulkanApp类:

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

2. 导入glfw库

  对于不同的操作系统,其窗口系统有着不同的实现,如果我们希望代码在不同操作系统上运行,就需要针对不同的操作系统使用不同的编窗口API,例如使用Xlib或Xcb接口。或者,我们可以使用别人封装好的跨平台库,glfw正是这样的一个库。

  使用glfw库前,需要引入相应的头文件:

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

glfw最早是为OpenGL(也是一种图形API,是Vulkan的老前辈)设计的,为了启用Vulkan相关的代码,需要定义GLFW_INCLUDE_VULKAN这个宏。之后,与Vulkan相关的头文件都会通过glfw.h被引入,无需额外引入Vulkan相关的头文件。

在构造函数的起始部分初始化glfw,并进行相应的设置:

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

其中:

glfwInit():初始化glfw库,使用glfw库之前需要先初始化这个库;

glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API):前面曾提到,glfw库最初是为OpenGL量身定做的,所以需要通过此函数调用,告诉glfw不要创建OpenGL相关的内容;

glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE):在Vulkan中,当用户调节窗口的大小时,需要手动完成一系列的设置,例如改变底层的窗口大小等等,因此我们暂时禁止调整窗口大小,以简化代码。

3. 创建实例(Instance)

  Vulkan中第一个创建的对象是实例对象,之后创建的所有对象都直接或间接地依赖它。函数vkCreateInstance用于创建一个实例,其原型为:

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

Vulkan中的函数以小写字母vk开头,例如这里的vkCreateInstance.

各参数的含义如下:

pCreateInfo: 是一个指向结构体的指针。多数Vulkan对象的创建都需要填充一个结构体。在这里,如果我们想创建一个实例,就需要向VkInstanceCreateInfo结构体中填入相应的信息,最后通过指向此结构体的指针将结构体传入函数;

pAllocator: 与内存管理有关,用户可以自定义内存分配的方式。在本系列文章中,只要遇到此参数,我们就将其设置为nullptr;

  pInstance: 指向创建好的实例。

函数的返回值表示函数执行的状态,是一个类型为VkResult的枚举值。如果创建成功,则返回VK_SUCCESS;否则会返回其它类型的值。

 

  创建VkInstance的关键在于填充VkInstanceCreateInfo结构体,其定义如下:

typedef sturct VkInstanceCreateInfo {
    VKStructureType  sType;
    const void*  pNext;
    VkInstanceCreateFlags  flags;
    const VkApplicationInfo*  pApplicationInfo;
    uint32_t  enableLayerCount;
    const char* const*  ppEnabledLayerNames;
    uint32_t  enableExtensionCount;
    const char* const*  ppEnabledExtensionNames;
} VkInstanceCreateInfo;

各成员的含义如下:

sType: 几乎所有的XXXCreateInfo结构体,其第一个成员都是sType,它是一个枚举类型的变量,表示此结构体的类型。例如,在这里将sType设置为VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,表示此结构体的类型是VkInstanceCreateInfo;

pNext: 有了它,方便向函数传入结构体链表,在本系列文章的绝大部分情况下,都将其设置为nullptr;

flags: 保留,供以后使用。Vulkan的很多结构体都有这个位,与XXXCreateInfo相关的flags成员大多保留,通常设置为0.

pApplicationInfo: 指向VkApplication结构体,此结构体用于记录应用程序的名称、版本号等;

enableLayerCount:启用层(Layer)的个数;

ppEnabledLayerNames:一个字符串数组的首地址,需要启用的层名称记录在一个字符串数组中;

enableExtensionCount:启用扩展(Extension)的个数;

ppEnabledExtensionNames:一个字符串数组的首地址,需要启用的扩展名称记录在一个字符串数组中。

 

  结构体中的层和扩展可能会让人困惑,它们是什么呢?

  层(Layer)提供了调试、日志、性能分析等功能。例如,有的层可以检查函数的参数是否合法,这对于调试代码很有用,但是一旦程序调试完成,这样的检查只会降低性能。因此,可以只在调试阶段启用一些具有特定功能的层。

  可以将需要的层名称以字符串的形式保存在一个vector中,为此,首先引入相应的头文件:

#include <vector>
using std::vector;  // 由于vector在接下来经常会用到,这样可以每次少写个std::

在VulkanApp中定义字符串的vector成员变量mRequiredLayers, 并在其中写下我们需要启用的扩展:

const vector<const char*> mRequiredLayers = {
	"VK_LAYER_KHRONOS_validation"
};

 

  扩展(Extension)提供了额外的功能,例如在VSCode中安装的各种扩展。在这里,我们至少需要启用glfw需要的扩展,通过以下代码获取glfw所需的扩展:

uint32_t glfwExtensionCount = 0; 
const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

 

VulkanApp类中定义成员变量VkInstance mInstance表示创建的实例:

class VulkanApp {
private:
    ...
    VkInstance mInstance;  // Vulkan实例
};

所有的Vulkan对象都以Vk(大写字母V和小写字母k)开头,例如此处的VkInstance.

接下来,定义成员函数createInstance创建一个实例,此函数的具体实现如下:

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(vkCreateInstance(&instanceCreateInfo, nullptr, &mInstance) != VK_SUCCESS) {
		exit(-1);
    }
}

我们经常需要向结构体传递数组,传递的是数组的大小和首地址。有以下两种方式:

1. 直接使用数组,例如上面的glfwExtensions数组;

2. 使用vector:使用.size()传入数组大小,使用.data()传入数组首指针,例如上面的mRequiredLayers.

由于VkApplicationInfo这个结构体没有什么重要的东西,在这里就不另花篇幅介绍这个结构体了。

VkInstanceCreateInfo结构体中填充必要的信息,最后调用vkCreateInstance创建结构体,这就是创建一个VkInstance对象的全部步骤。

Vulkan创建对象的一般步骤

在Vulkan中,VkXXX类型的对象一般按如下方式创建:

1. VkXXX会有个与之关联的、名为VKXXXCreateInfo的结构体, 首先填充这个结构体;

2. 有个名为vkCreateXXX的函数,向函数中传入上一步填充的结构体(传入指向结构体的指针),创建这个对象;

3. 检查上一步中函数的返回值是否为VK_SUCCESS, 如果是,则表示VkXXX对象创建成功;否则需要作进一步处理,我们这里简单粗暴地使用exit(-1)终止程序。

  后面,我们会反复使用类似的步骤创建各种Vulkan对象,总有一天你会看吐的。对于每个vkCreateXXX函数,都需要检查对象是否创建成功,为了方便,将检查返回值的过程封装为函数if_fail

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

  当Vulkan对象创建失败时,输出错误信息message并终止程序,为了使用输出,请将#include<iostream>添加到代码的开头部分。有了这个函数,上面检查vkCreateInstance是否创建成功的代码可改写为:

if_fail(
    vkCreateInstance(&instanceCreateInfo, nullptr, &mInstance),
    "failed to create instance"
);

  不要忘记在析构函数中销毁mInstance对象:

~VulkanDemo() {
    vkDestroyInstance(mInstance, nullptr);  // 销毁mInstance

    glfwDestroyWindow(mWindow);
    glfwTerminate();
}

以后,当创建完一个对象后,我们都会立即在析构函数中添加销毁此对象的代码。

 

  在函数vkDestroyInstance中,第二个参数与内存分配有关,与vkCreateInstance中的参数pAllocator含义相同,在接下来的文章中,此类参数全部设置为nullptr.

vkCreateXXX有一个与之对应的函数vkDestroyXXX,用于销毁某一对象。

4. 创建表面(Surface)

  接下来需要创建一个glfw窗口,它是对不同系统中窗口的抽象,创建完成后会得到一个GLFWwindow*类型的窗口指针。为了创建窗口,需要提供窗口的宽和高,我们将这些值定义为VulkanApp的私有成员变量:

class VulkanApp {
public:
    ...
private:
    int mWidth;
    int mHeight;
    GLFWwindow* mWindow;
};

例如mWidth的命名仅仅是个人偏好,前面的m表示这个变量是类的一个成员(member), 你可以换成自己喜欢的命名方式,例如m_width等。

  定义函数createSurface用于创建表面,我们首先在这个函数中创建一个glfw窗口:

void createSurface() {
	mWindow = glfwCreateWindow(mWidth, mHeight, "Vulkan App", nullptr, nullptr);  // 创建glfw窗口
}

glfwCreateWindow用于新建一个窗口,前两个参数是窗口的宽和高;第三个参数是窗口标题;最后两个参数不用管它们,全部设置为空指针nullptr即可。

  不要忘记在析构函数中销毁glfw窗口:

~VulkanApp() {
    glfwDestroyWindow(mWindow);  // 销毁glfw窗口
    glfwTerminate();  // 终止glfw
}

 

  接下来就可以创建Surface对象了,在VulkanApp类中定义一个成员变量:

class VulkanApp {
private:
    ...
    VkSurfaceKHR mSurface;  // 表面
};

谁是KHR?

  可能有的朋友会疑惑:好好的VkSurface, 为什么后面会有个KHR呢?前面提到过,显示功能是Vulkan的一个扩展功能,如果在网上搜索Vulkan,你会发现Vulkan与一个名为Khronos Group的组织密切相关,相信聪明的你已经猜到KHR是谁了吧?

  

  接着借助glfw提供的函数创建VkSurfaceKHR对象:

void createSurface() {
    mWindow = glfwCreateWindow(mWidth, mHeight, u8"快显示出三角形", nullptr, nullptr);  // 创建glfw窗口

    /* 创建VkSurfaceKHR对象 */
    if_fail(
	    glfwCreateWindowSurface(mInstance, mWindow, nullptr, &mSurface),  // 创建表面需要Vulkan实例和glfw窗口。
	    "failed to create surface"
    );
}

5. 收尾工作

  最后在函数Run中添加以下代码:

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

这是程序的主循环,以后我们会在这个循环中添加绘制各种物体的代码。glfwWindowShouldClose检查当前窗口是否关闭,例如当我们点击窗口的关闭按钮后,这个函数就会返回true,从而结束循环。

  目前,所有的代码放在末尾,如果此时运行代码,是可以看到一个空白窗口的:

  另外,还能在输出中看到glfw要求启用的扩展(博主是在Windows上运行的,所以需要VK_KHR_win32_surface):

  在下一节中,我们将会研究物理设备,即与GPU相关的代码。

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

创建VkInstance和VkSurfaceKHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

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


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();
	}

	~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"
	};
	VkInstance mInstance;  // 实例

	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"
		);
	}
};


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-27 23:11  overxus  阅读(573)  评论(0编辑  收藏  举报