好久没有更新过博客了,上半年一直忙着找实习的事情,不过现在已经入职一段时间了,也可以抽出时间来继续整理一些内容,所以最近会尽量变得勤快一点来写博客。
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
下面来逐步进行介绍。
使用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,这个后续再考虑)
Vulkan的设计理念是减少对驱动的依赖,因此在默认情况下只提供了非常有限的错误检查,程序员需要高度谨慎地对待自己所写的每一行程序,所以如果向API传入错误的参数往往不会有错误的反馈,而是会像正常情况一样处理,这可能会直接导致程序的崩溃。因此在写程序时犯的一些小错误可能会导致非常严重的后果,但是却不能被及时的排查。
不过幸运的是Vulkan SDK提供了一个名叫校验层的东西,它可以用来做参数检查、内存泄漏监测、API调用详细记录、线程安全检查等非常有用的工作,但是在默认的情况下都是不开启的。如果想要启用,就必须先检查当前环境是否支持校验层:
// 2. check validation layer support
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
std::vector 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
如果当前的环境的确支持校验层,那么怎么样才能将其开启并提供一种获取校验层所检测出来的错误的方法呢?这个过程可以在创建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 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(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.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 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表示显卡所支持的特性。
如果直接使用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.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 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.1 检查物理设备是否支持需要的扩展
逻辑设备是作为用户操控物理设备的一个接口,也就是说通过逻辑设备来创建资源,本质上还是在用物理设备来创建,只是我们并不能直接去操控物理设备。在创建逻辑设备之前,还需要对物理设备先做一个关于扩展的支持性检查。前面在创建Instance时已经做过了一次跟扩展相关的工作,那么这里为什么还需要处理一个跟扩展有关的问题呢?
此前在Instance中检测了应用程序所必须要求的扩展并且添加了校验层回调的扩展,但是这都是在处理应用程序层面上的扩展,影响因素可能是应用程序所处的运行环境,VulkanSDK的版本等(个人猜测)。
而现在我们需要做的是检测物理设备的扩展,影响因素是GPU本身的功能,这里主要就检测一项——创建swapchain的扩展,并不是所有的GPU都能够支持创建swapchain然后将渲染的结果呈现到屏幕上,因为有一些GPU是专门给服务器设计的,它们并不需要将图形渲染的结果反馈到屏幕上,自然也就不会支持创建swapchain了。
// 7.1 check whether the physical device support the required extensions
const std::vector requireExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
uint32_t availableExtensionCount;
vkEnumerateDeviceExtensionProperties(physicalDevice, NULL, &availableExtensionCount, NULL);
std::vector availableExtensions(availableExtensionCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, NULL, &availableExtensionCount, availableExtensions.data());
std::set 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;
deviceCreateInfo.enabledExtensionCount = requireExtensions.size();
deviceCreateInfo.ppEnabledExtensionNames = requireExtensions.data();
if (vkCreateDevice(physicalDevice, &deviceCreateInfo, NULL, &logicalDevice) != VK_SUCCESS)
{
throw std::runtime_error("failed to create logical device.");
}
这里的VkPhysicalDeviceFeatures deviceFeatures就是此前在选择物理设备时用到的Features,创建逻辑设备时,我们需要指明要使用物理设备所支持的特性中的哪一些特性,这里暂时不需要使用什么特性,所以可以先不管。
VkDeviceCreateInfo中填入有关队列和扩展的信息,这样在创建逻辑设备时,就启用了需要的扩展以及创建了队列。
7.3 获取队列
// 7.3 retrieve device queue by logical device
VkQueue graphicsQueue;
vkGetDeviceQueue(logicalDevice, queue_ind, 0, &graphicsQueue);
————————————————————————————————————————————
在看后续的程序之前,先把几个不太容易理解、容易混淆的概念解释一下:
VkMemory、VkImage、VkImageView、Attachment、VkFrameBuffer
VkMemory:很简单,就是指内存中一块连续的字节序列
VkImage:在VkMemory的基础上添加了一定的图像格式,即解析VkMemory所指内存的方式,由此我们可以通过一个像素(或者图素)来访问这个内存中的某一位置,而不需要通过指定字节的方式来访问这块内存。
VkImageView:选取VkImage中的一部分或者对VkImage中的格式作一定的改变以适应不同的使用环境(兼容不同的接口)
Attachment:在渲染中用到的RenderTargets(color、depth/stencil等)或者一些输入数据(这里仅限于同一个pass内的前一个subpass的RenderTarget,关于RenderPass和SubPass稍后解释,注意Attachment并不包括输入的 Texture/Buffer/Sampler等),只有Image所包含的带有具体图像格式的内存才能够被用来作为Attachment,一般的没有固定格式的buffer(比如D3D中的Constant Buffer)是不可以作为Attachment的
VkFrameBuffer:绑定一个或多个VkImageView,在进行渲染时让这些VkImageView作为Attachment
Pipeline、RenderPass、RenderSubPass
Pipeline:定义了渲染管线的各个阶段的状态,比如输入的VertexData,可编程阶段使用的Shader(VertexShader、PixelShader),光栅化的一些配置,混合方式等。任何一种状态如果要发生变化,就必须新创建一个pipeline。
RenderSubPass:指定执行一次pipeline时的Attachment,并且会指定在渲染之前对Attachment的操作(保留原有值或者全部填充为一个指定的颜色),在渲染之后对渲染结果的处理策略(写回到内存或者丢弃)。
RenderPass:一个集合,它包含:一系列的Attachment、RenderSubPass、SubPassDependencies。RenderSubPass所指定的Attachment均来自RenderPass这个集合中提供的Attachment,同一个RenderPass内的所有RenderSubPass的执行顺序是不固定的,有可能并行执行,也有可能是不可预知的一个顺序,如果需要指定顺序,就需要用到SubPassDependencies。
Pipeline和SubPass之间是一一对应的关系,在执行一个SubPass时,需要绑定一个pipeline。但是一次完整的渲染是开始于RenderPass,在一个RenderPass的执行过程中,不可以再启动一个其他的RenderPass,必须要让这个RenderPass中的所有Subpass执行完才行,RenderPass结束才意味着这一次渲染结束。
那么现在最令人费解的是,为什么要有SubPass这个概念?为什么不让RenderPass来直接和Pipeline一一对应,而非要让一个RenderPass中包含多个不同的Subpass?直接让RenderPass来指定Attachment,然后如果有需要就创建多个RenderPass不就可以了吗?
现在设想一下,如果需要实现延迟渲染,只用RenderPass,如果整个场景都是不透明的物体,那么我们至少需要两个RenderPass:第一个用于填充G-Buffer,第二个用于作光照计算得到最终的颜色。那么执行的流程是:
第一个RenderPass进行渲染->将渲染结果写回到RenderTarget(G-Buffer)的内存中->第二个RenderPass读G-Buffer->第二个RenderPass进行渲染->将渲染结果写回到RenderTarget(Color Attachment)中
可以看到中间有两步是要向RAM中写数据和读数据的,这里可以优化,第二个RenderPass如果能直接读取第一个RenderPass的结果,比如把第一个RenderPass的结果放在GPU芯片上的内存中或者直接合并两个Pass的操作,那么就省去了跨级存储器读写的高昂代价。
这就是SubPass存在的意义,Vulkan中对于一个RenderPass内的Subpass,如果某一个SubPass需要前一个SubPass的渲染结果并且第二个SubPass中的任何一个像素只需读取前一个SubPass中同一像素位置所存储的数据时(不允许访问其他位置的像素),就会对此做一个与内存操作有关的优化。
所以如果我们做类似于延迟渲染这样的多个Pass之间不会有像素级的随机存取操作时,就可以采用多个SubPass来提升性能。而如果是像SSAO、BLOOM这种需要用到多个不同位置像素的数据时,就要采用多个RenderPass。
对于这些概念如果理解的还不是特别清楚也没关系,在本程序中我们只会用到一个RenderPass,并且它只有一个Subpass,本程序也不会用到SubPassDependencies。
更复杂的情况后续再考虑。
7.4 创建swapchain
创建一个swapchain,需要有三个基本的信息:
1.swapchain的容量,即swapchain中所能包含图像的最小和最大数量,图像的大小
2.swapchain中图像的数据存储格式和色彩空间
3.swapchain中图像的呈现方式
首先我们要查询GPU所能够支持的这些属性,然后在从中选择:
// 7.4 create swap chain by logical device
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector formats;
std::vector presentModes;
};
SwapChainSupportDetails details;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &details.capabilities);
// 7.4.1 choose the surface format and present mode
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, NULL);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, details.formats.data());
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, NULL);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, details.presentModes.data());
}
if (formatCount == 0 || presentModeCount == 0) {
throw std::runtime_error(" no suitable format or present mode ");
}
int format_ind = -1;
for (const auto& availableFormat : details.formats)
{
format_ind++;
if (availableFormat.format == VK_FORMAT_R8G8B8A8_UNORM
&& availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
{
break;
}
}
int present_mode_ind = -1;
for (const auto& availablePresentMode : details.presentModes) {
present_mode_ind++;
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
break;
}
}
// 7.4.2 choose the extent
VkExtent2D actualExtent;
actualExtent.width = std::fmax(details.capabilities.minImageExtent.width,
std::fmin(details.capabilities.maxImageExtent.width, width));
actualExtent.height = std::fmax(details.capabilities.minImageExtent.height,
std::fmin(details.capabilities.maxImageExtent.height, height));
uint32_t imageCount = details.capabilities.minImageCount + 1;
if (details.capabilities.maxImageCount > 0 && imageCount > details.capabilities.maxImageCount) {
imageCount = details.capabilities.maxImageCount;
}
这段程序选择图像数据存储格式为R8G8B8A8_UNORM,色彩空间为SRGB非线性色彩空间。
选择图像大小和图像数量比较直接,需要注意这里推荐使用的是设备支持图像数量最小值+1,这是为了避免在有的时候可能会因为驱动的内部一些操作造成需要等待一段时间才能获取swapchain中的另一个图像来进行渲染,造成卡顿。
关于PresentMode,它有四种可能的取值:
取值 | 含义 |
---|---|
VK_PRESENT_MODE_IMMEDIATE_KHR | 应用程序呈递的图像会被立即显示到屏幕上 |
VK_PRESENT_MODE_FIFO_KHR | 呈现机制类似于队列,先到的图像会被先显示,然后移出队列,在队列满时程序需要等待屏幕刷新 |
VK_PRESENT_MODE_FIFO_RELAXED_KHR | 与前一种方式唯一的不同之处在于当队列为空时,图像到达后不会等待屏幕刷新直接被显示 |
VK_PRESENT_MODE_MAILBOX_KHR | 跟第二种方式不同的是当队列满时,新的图像会直接取代队列中已有的元素而不等待 |
// 7.4.3 fill the VkSwapchainCreateInfoKHR and create .
VkSwapchainCreateInfoKHR swapChainCreateInfo = {};
swapChainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapChainCreateInfo.surface = surface;
swapChainCreateInfo.minImageCount = imageCount;
swapChainCreateInfo.imageFormat = details.formats[format_ind].format;
swapChainCreateInfo.imageColorSpace = details.formats[format_ind].colorSpace;
swapChainCreateInfo.imageExtent = actualExtent;
swapChainCreateInfo.imageArrayLayers = 1;
swapChainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
swapChainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapChainCreateInfo.preTransform = details.capabilities.currentTransform;
swapChainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
swapChainCreateInfo.presentMode = details.presentModes[present_mode_ind];
swapChainCreateInfo.clipped = VK_TRUE;
swapChainCreateInfo.oldSwapchain = VK_NULL_HANDLE;
if (vkCreateSwapchainKHR(logicalDevice, &swapChainCreateInfo, NULL, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("swap chain create fault ");
}
然后就是创建swapchain了,所需要填入的信息有:
成员 | 含义 |
---|---|
surface | 5中创建的surface对象 |
minImageCount | 7.4.1中获取的最小图像数量 |
imageFormat | 7.4.1中获取的图像存储格式 |
imageColorSpace | 7.4.1 中获取的色彩空间 |
imageExtent | 7.4.1中获取的图像大小 |
imageArrayLayers | 每一张图像的层数,一般都设置为1,除非是在做具有立体视觉的程序 |
imageUsage | 图像的具体用途,这里是直接把渲染好的图像直接呈现到屏幕上,因此是用于颜色绑定 |
imageSharingMode | VK_SHARING_MODE_EXCLUSIVE-图像必须在Graphics Queue渲染之后传递给Prensent Queue VK_SHARING_MODE_CONCURRENT-GraphicsQueue和PresentQUeue是同一个,无须经过传递 |
preTransform | 图像在交换链中需要经过的变换(例如旋转90度之类的),如果不需要额外的变换就设置为swapChainSupport.capabilities.currentTransform |
compositeAlpha | 应用程序的窗口是否需要和其他窗口进行颜色的混合,大部分情况是不需要的,直接设置为VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR |
presentMode | 7.4.1中获取的呈现模式 |
clipped | 是否忽略掉窗口中那些被其他窗口所遮挡的像素 |
oldSwapchain | 与swapchain的重建有关,暂时忽略 |
7.5 创建swapchain ImageView
在swapchain被创建好后,swapchain种会存放的有我们指定的个数、大小的Image,和Direct3D中一样,如果要想绑定一个资源到管线上,必须使用一个view,view规定了对image的访问权限和操作方式,所以现在需要创建和图像个数相同的view:
//7.5 create swapchain ImageView
std::vector swapChainImages;
vkGetSwapchainImagesKHR(logicalDevice, swapChain, &imageCount, NULL);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(logicalDevice, swapChain, &imageCount, swapChainImages.data());
std::vector swapChainImageViews;
swapChainImageViews.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImages.size(); i++) {
VkImageViewCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapChainCreateInfo.imageFormat;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(logicalDevice, &createInfo, NULL, &swapChainImageViews[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create image view.");
}
}
说明其中个别成员的含义:
createInfo.components:用于色阶的映射,比如可以将view中的rgba全部都映射到image中的red色阶,这里使用VK_COMPONENT_SWIZZLE_IDENTITY表示不做任何的改变。
createInfo.subresourceRange.aspectMask:用途,这里只是做颜色使用,如果是作为深度,就改为VK_IMAGE_ASPECT_DEPTH_BIT
7.6 创建Shader Module
Vulkan中对于着色器采用加载字节码的形式,我们需要先写好shader,然后将其编译为字节码,再经过Vulkan API来读取创建Shader Module。比如在这个程序中会先写好2个Shader文件:
shader.vert
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(location = 0) out vec3 fragColor;
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
vec3 colors[3] = vec3[](
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
fragColor = colors[gl_VertexIndex];
}
shader.frag:
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
这两个Shader程序比较简单,值得注意的是location的含义,VertexShader的输出和FragmentShader的输入Location要一一对应,FragmentShader的输出Location是在随后Subpass指定的Attachments数组的下标,表示FragmentShader的输出将写入到的Attachment的编号。
然后在这两个目录所在的文件下新建一个批处理文件compile.bat,编辑这个批处理文件,输入:
E:/VulkanSDK/1.1.108.0/Bin32/glslangValidator.exe -V shader.vert
E:/VulkanSDK/1.1.108.0/Bin32/glslangValidator.exe -V shader.frag
pause
然后在该目录下就应该会得到frag.spv和vert.spv两个字节码文件,这两个文件可以被Vulkan API直接读取加载。
//7.6 create shader module
auto loadShaderByteCode = [](const std::string& fileName) {
std::ifstream loadFile(fileName, std::ios::ate | std::ios::binary);
if (!loadFile.is_open())
{
throw std::runtime_error("failed to open file!");
}
size_t fileSize = (size_t)loadFile.tellg();
std::vector buffer(fileSize);
loadFile.seekg(0);
loadFile.read(buffer.data(), fileSize);
loadFile.close();
return buffer;
};
auto vertShaderCode = loadShaderByteCode("vert.spv");
auto fragShaderCode = loadShaderByteCode("frag.spv");
auto createShaderModule = [](const std::vector& code) {
VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast(code.data());
VkShaderModule shaderModule;
if (vkCreateShaderModule(logicalDevice, &createInfo, NULL, &shaderModule) != VK_SUCCESS)
{
throw std::runtime_error("failed to create shader module .");
}
return shaderModule;
};
VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);
这段程序比较简单,不须多做说明。
7.7 创建pipeline
7.7.1 准备ShaderStage
// 7.7.1 prepare shader stage
VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo , fragShaderStageInfo };
ShaderStage指的是管线中的所有可编程着色器,需要用到几个就创建几个,然后整合到一个数组中,最后一起在创建管线时传递过去即可。
7.7.2 准备VertexInputState
//7.7.2 prepare vertex input state
VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = NULL;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = NULL;
这个描述了输入顶点的格式和属性,这里先暂时忽略,我们先通过Shader硬编码的方式获取输入的顶点数据,在之后的文章中会对此做修改。
7.7.3 准备input assembly state
//7.7.3 prepare input assembly state
VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
Topology:规定了顶点所构成图元的类型,这里使用的是三角形List
PrimitiveRestartEnable:如果设置为VK_TRUE则表示在Strip类型的图元中,可以通过一个特殊的索引来打断当前的Strip并开启一个新的Strip
7.7.4 准备viewport和scissor state
//7.7.4 prepare viewport and scissor state
VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = width;
viewport.height = height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
VkRect2D scissor = {};
scissor.offset = { 0 , 0 };
scissor.extent = swapChainCreateInfo.imageExtent;
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
关于viewport和scissor看这副图就应该能明白了,viewport是指定最后看到的视口区域,绘制的结果会被变换到这个区域中,而scissor表示的是绘制的区域。
7.7.5 准备 rasteriazation state
//7.7.5 prepare rasterization state
VkPipelineRasterizationStateCreateInfo rasterizer = {};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f;
rasterizer.depthBiasClamp = 0.0f;
rasterizer.depthBiasSlopeFactor = 0.0f;
偷一下懒,这些参数我就不解释了……用过D3D的应该都懂……有一些不明白的去查查文档吧……
7.7.6 准备 multisampling state
//7.7.6 prepare multisample state
VkPipelineMultisampleStateCreateInfo multisampling = {};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f;
multisampling.pSampleMask = nullptr;
multisampling.alphaToCoverageEnable = VK_FALSE;
multisampling.alphaToOneEnable = VK_FALSE;
本程序暂时先不开启多重采样,在后面的文章会修改这里。
7.7.7 准备 color blend state
//7.7.7 prepare color blend state
VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
colorBlendAttachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;
当colorBlending中的logicOpEnable为False的时候,会使用第一个结构体中提供的混合方式进行颜色混合,如果为True,就会通过它所指定的四个混合常数和logicOp指定的混合操作符进行混合。
在第一个结构体中的混合逻辑为:
if (blendEnable) {
finalColor.rgb = (srcColorBlendFactor * newColor.rgb) (dstColorBlendFactor * oldColor.rgb);
finalColor.a = (srcAlphaBlendFactor * newColor.a) (dstAlphaBlendFactor * oldColor.a);
} else {
finalColor = newColor;
}
finalColor = finalColor & colorWriteMask;
7.7.8 创建 RenderPass
//7.7.8 create render pass
VkAttachmentDescription colorAttachment = {};
colorAttachment.format = swapChainCreateInfo.imageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorAttachmentRef = {};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
VkRenderPass renderPass;
VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 0;
renderPassInfo.pDependencies = NULL;
if (vkCreateRenderPass(logicalDevice, &renderPassInfo, NULL, &renderPass) != VK_SUCCESS) {
throw std::runtime_error("failed to create render pass!");
}
来仔细看一下这几个结构体的含义:
VkAttachmentDescription :
成员 | 含义 |
---|---|
format | 作为Attachment的Image的格式 |
samples | Attachment的MSAA采样个数 |
loadOp | (Attachment为 Color/Depth Attachment时有效)在渲染前,对该Attachment的操作:VK_ATTACHMENT_LOAD_OP_LOAD-保持该Attachment的内容;VK_ATTACHMENT_LOAD_OP_CLEAR-将其清空为某一常数;VK_ATTACHMENT_LOAD_OP_DONT_CARE-不关心该Attachment的内容 |
storeOp | (Attachment为 Color/Depth Attachment时有效)在渲染后,对该Attachment的操作:VK_ATTACHMENT_STORE_OP_STORE-渲染的结果会立刻存回内容中,后续可读取;VK_ATTACHMENT_STORE_OP_DONT_CARE-不关心渲染的结果如何处理 |
stencilLoadOp | Attachment为Stencil Attachment时有效,效果同loadOp |
stencilStoreOp | Attachment为Stencil Attachment时有效,效果同storeOp |
initialLayout | 在渲染前image所在的Layout |
finalLayout | 在渲染后image会被转移到的Layout |
根据image用途的不同,它会处于不同的Layout中,比较常见的Layout有:VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:用作ColorAttachment
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:会被Swapchain呈现
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:被用作一次内存转移命令的目标位置
VK_IMAGE_LAYOUT_UNDEFINED:不关心image处于哪个Layout
VkAttachmentReference:这个结构体会被Subpass直接包含
成员 | 含义 |
---|---|
attachment | 目标Attachment在RenderPass中所有attachment中的索引 |
layout | 在整个subpass中,目标attachment的Layout |
VkSubpassDescription:
成员 | 含义 |
---|---|
pipelineBindPoint | 指定管线的绑定类型 VK_PIPELINE_BIND_POINT_COMPUTE-作为计算管线;VK_PIPELINE_BIND_POINT_GRAPHICS -作为图形管线;VK_PIPELINE_BIND_POINT_RAY_TRACING_NV -作为光线追踪管线 |
colorAttachmentCount | SubPass中的colorAttachment的个数 |
pColorAttachments | SubPass中所有colorAttachment的AttachmentReference |
VkSubpassDescription的其他成员暂时不用到,先不解释。
总的来看,所有的Attachment被放在RenderPass中,Subpass通过AttachmentReference来作为它们各自Attachment在RenderPass中的索引。
7.7.9 创建pipeline layout
本程序暂时不需要配置pipeline layout,所有的参数都设置为空即可
//7.7.9 create pipeline layout
VkPipelineLayout pipelineLayout;
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pushConstantRangeCount = 0;
if (vkCreatePipelineLayout(logicalDevice, &pipelineLayoutInfo, NULL, &pipelineLayout) != VK_SUCCESS)
{
throw std::runtime_error("failed to create graphics pipeline . ");
}
7.7.10 创建pipeline
有了前面准备的这些object,现在已经能够创建pipeline了:
// 7.7.10 merge all the state and create graphics pipeline
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = NULL;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = NULL;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
pipelineInfo.layout = pipelineLayout;
VkPipeline graphicsPipeline;
if (vkCreateGraphicsPipelines(logicalDevice, VK_NULL_HANDLE, 1, &pipelineInfo, NULL, &graphicsPipeline)
!= VK_SUCCESS)
{
throw std::runtime_error("failed to create graphics pipeline");
}
vkDestroyShaderModule(logicalDevice, fragShaderModule, nullptr);
vkDestroyShaderModule(logicalDevice, vertShaderModule, nullptr);
主要就是把所有准备的信息合并,注意在这里将fragShaderModule和vertShaderModule都释放掉了,它们的字节码已经被管线所绑定,现在可以销毁。
7.7.11 创建FrameBuffer
在创建RenderPass时,创建了一些Attachment,准确地说,应该是一些Attachment Description,它们描述了RenderPass中所有Attachment的一些基本性质以及RenderPass对它们的处理策略,但是注意到,我们并没有在那里真正的绑定具体的某一个Image View作为Attachment,前面说了,Attachment是通过FrameBuffer来指定一些Image View得到的,也就是说真正的Attachment都存放在FrameBuffer中,而RenderPass更像是提供了Attachment的接口。
下面就来创建FrameBuffer,在本程序中,只会用到SwapChain中的所有Image作为ColorAttachment,然后每一帧通过Pipeline和RenderPass对SwapChain中的一个Image做渲染然后输出,最终通过Swapchain再显示到屏幕上,那么我们就只需要对每个SwapChain中的Image创建一个相应的FrameBuffer,然后在之后的渲染中,将FrameBuffer和RenderPass连接起来,就完成了Attachment的指定。
//7.7.11 create frame buffer
std::vector swapChainFrameBuffer;
swapChainFrameBuffer.resize(swapChainImageViews.size());
for (size_t i = 0; i < swapChainImageViews.size(); i++)
{
VkImageView attachments[] = {
swapChainImageViews[i]
};
VkFramebufferCreateInfo frameBufferInfo = {};
frameBufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
frameBufferInfo.renderPass = renderPass;
frameBufferInfo.width = width;
frameBufferInfo.height = height;
frameBufferInfo.layers = 1;
frameBufferInfo.pAttachments = attachments;
frameBufferInfo.attachmentCount = 1;
if (vkCreateFramebuffer(logicalDevice, &frameBufferInfo, NULL, &swapChainFrameBuffer[i]) != VK_SUCCESS)
{
throw std::runtime_error("create frame buffer fault . ");
}
}
7.7.12 创建CommandBuffer
Vulkan中所有渲染、内存转移等指令,都不能直接的通过函数调用来完成(DrawCall之类的),必须要将它们全部都放在CommandBuffer中,然后把CommandBuffer提交给GraphicsQueue
创建CommandBuffer需要用到CommandPool和VkCommandBufferAllocateInfo ,创建好CommandBuffer后,就需要往里面填充一系列的命令。
由于我们需要在每一帧指定不同的FrameBuffer,因此需要创建多个CommandBuffer,其个数和FrameBuffer的个数相同。
//7.7.12 create command buffer
VkCommandPool commandPool;
VkCommandPoolCreateInfo poolCreateInfo = {};
poolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolCreateInfo.queueFamilyIndex = queue_ind;
poolCreateInfo.flags = 0;
if (vkCreateCommandPool(logicalDevice, &poolCreateInfo, NULL, &commandPool) != VK_SUCCESS)
{
throw std::runtime_error("create command pool fault . ");
}
commandBuffer.resize(swapChainFrameBuffer.size());
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t)commandBuffer.size();
if (vkAllocateCommandBuffers(logicalDevice, &allocInfo, commandBuffer.data()) != VK_SUCCESS)
{
throw std::runtime_error("alloc command buffer fault . ");
}
for (size_t i = 0; i < commandBuffer.size(); i++)
{
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0;
if (vkBeginCommandBuffer(commandBuffer[i], &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("failed to begin recording command buffer . ");
}
VkRenderPassBeginInfo renderPassBeginInfo = {};
VkClearValue clearColor = { 0.0f , 0.0f , 0.0f , 1.0f };
renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassBeginInfo.renderPass = renderPass;
renderPassBeginInfo.framebuffer = swapChainFrameBuffer[i];
renderPassBeginInfo.renderArea.offset = { 0 , 0 };
renderPassBeginInfo.renderArea.extent = swapChainCreateInfo.imageExtent;
renderPassBeginInfo.clearValueCount = 1;
renderPassBeginInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffer[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffer[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
vkCmdDraw(commandBuffer[i], 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffer[i]);
if (vkEndCommandBuffer(commandBuffer[i]) != VK_SUCCESS)
{
throw std::runtime_error("failed to record command buffer . ");
}
}
VkCommandBufferAllocateInfo中的level有两个取值:
VK_COMMAND_BUFFER_LEVEL_PRIMARY:创建的CommandBuffer可以被提交给Queue,但是不能被其他的CommandBuffer调用
VK_COMMAND_BUFFER_LEVEL_SECONDARY:创建的CommandBuffer不可以被直接提交给Queue,但是能够被其他的CommandBuffer调用
然后是非常重要的命令填充部分:
在开始和结束填充命令时,需要调用vkBeginCommandBuffer和vkEndCommandBuffer。然后对于每一个RenderPass,需要在开始和结束时分别调用vkCmdBeginRenderPass和vkCmdEndRenderPass,在调用vkCmdBeginRenderPass时需要指定一个VkCommandBufferBeginInfo,它指定了FrameBuffer、RenderPass以及渲染区域等信息。
在中间,需要绑定Pipeline以及写入DrawCall命令。
vkCmdDraw的后四个参数分别代表:顶点的数量、Instance的数量、起始顶点、起始Instance
经过这样一些步骤以后,CommandBuffer就存放了渲染的所有命令,之后在程序的主循环中,每一帧根据SwapChain所呈现的Image编号,递交相应的CommandBuffer到Queue中就行了。
7.7.13 创建两个Semaphore
在主函数递交命令渲染的过程中,我们需要不断重复做三件事情:
1.从SwapChain中获取一个Image(Index)
2.把这个Image作为Attachment,使用CommandBuffer来对其做渲染
3.将渲染后的Image返回给Swapchain来呈现到屏幕上
它们各自都只需要一个函数调用就可以完成:vkAcquireNextImageKHR、vkQueueSubmit、vkQueuePresentKHR
这三个函数都是异步执行的,然而在逻辑上它们应该有一个明确的先后次序,因此我们需要用一定的手段来做同步。这里使用Semaphore(信号量),学过操作系统的应该都明白,如果不是很清楚的话,就可以先把它理解为一个锁,用它可以在执行一个指令时加锁,等待它完成后(或者执行到一定程度时)才会解锁,然后后续的指令(或者指令中的某一步)都需要在这个锁解掉以后才能开始执行。
VkSemaphoreCreateInfo semaphoreInfo = {};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
if (vkCreateSemaphore(logicalDevice, &semaphoreInfo, NULL, &imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(logicalDevice, &semaphoreInfo, NULL, &renderFinishedSemaphore) != VK_SUCCESS)
{
throw std::runtime_error("failed to create semaphore . ");
}
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
//get image
uint32_t imageIndex;
uint64_t limit = (std::numeric_limits::max)();
vkAcquireNextImageKHR(logicalDevice, swapChain, limit, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
// submit command
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = { imageAvailableSemaphore };
VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer[imageIndex];
VkSemaphore signalSemaphores[] = { renderFinishedSemaphore };
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer . ");
}
// present image
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = { swapChain };
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = NULL;
vkQueuePresentKHR(graphicsQueue, &presentInfo);
}
这里的同步策略是:imageAvailableSemaphore负责让vkQueueSubmit执行Pipeline时,当执行到VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT所代表的写入渲染结果到Attachment时停下,等待vkAcquireNextImageKHR完成才能继续。
renderFinishedSemaphore负责让vkQueuePresentKHR在渲染命令结束后才开始执行。
具体的细节看一下程序吧,这里就不解释了,比较好理解。
//clean up
for (auto framebuffer : swapChainFrameBuffer) {
vkDestroyFramebuffer(logicalDevice, framebuffer, nullptr);
}
vkDestroySemaphore(logicalDevice, renderFinishedSemaphore, nullptr);
vkDestroySemaphore(logicalDevice, imageAvailableSemaphore, nullptr);
vkDestroyCommandPool(logicalDevice, commandPool, nullptr);
vkDestroyPipeline(logicalDevice, graphicsPipeline, nullptr);
vkDestroyPipelineLayout(logicalDevice, pipelineLayout, nullptr);
vkDestroyRenderPass(logicalDevice, renderPass, nullptr);
for (auto imageView : swapChainImageViews) {
vkDestroyImageView(logicalDevice, imageView, nullptr);
}
vkDestroySwapchainKHR(logicalDevice, swapChain, nullptr);
vkDestroyDevice(logicalDevice, nullptr);
auto destroyFunc = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (destroyFunc != nullptr) {
destroyFunc(instance, debugMessenger, NULL);
}
else {
throw std::runtime_error(" not find function PFN_vkDestroyDebugUtilsMessengerEXT . ");
}
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
前面每一个通过Create创建的资源都需要在这里被销毁,如果开启了校验层,可以帮助检测没有被销毁的物体,比如去掉其中一个DestroySemephore,校验层就会有输出:
写这篇文章花了我很长时间,一些概念可能我现在理解的也不太到位,有一些内容如果感到比较含糊的话也没关系,因为这个程序本身就不太能够体现一些东西的用法,随后我也会继续学习加深理解,一起学习一起进步吧~
不过如果发现一些错误,请明确指出,然后我会再进行修改。
如果读者对有些函数的调用的参数不太明白什么意思的话,可以去查查文档:Vulkan Documentation,以及我参考的教程:Vulkan Tutorial
整个程序的全部源码在这里:
#define GLFW_INCLUDE_VULKAN
#define GLFW_EXPOSE_NATIVE_WIN32
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
GLFWwindow* window;
VkInstance instance;
std::vector validationLayerNames = { "VK_LAYER_LUNARG_object_tracker" };
const float width = 800;
const float height = 600;
VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;
VkQueue graphicsQueue;
VkQueue presentQueue;
VkDevice logicalDevice;
VkRenderPassCreateInfo renderPassInfo = {};
VkSwapchainKHR swapChain;
std::vector commandBuffer;
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;
}
int main()
{
// 1. Init Window
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(width, height, "Vulkan window", nullptr, nullptr);
// 2. check validation layer support
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
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 . ");
}
}
// 3. Create Instance
// 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;
// 3.2 get required extensions
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
// 3.3 add validation layer required extension
std::vector required_extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
// 3.4 fill the instance create info and create
VkInstanceCreateInfo instanceCreateInfo = {};
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instanceCreateInfo.pApplicationInfo = &appInfo;
instanceCreateInfo.enabledExtensionCount = static_cast(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!");
}
// 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 . ");
}
// 4. create physical device
// 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 devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
// 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.");
}
// 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 .");
}
// 6. prepare for creating device queue
// 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 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 .");
}
// 6.2 fill 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;
// 7. create logical device and use it to create some resources
// 7.1 check whether the physical device support the required extensions
const std::vector requireExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
uint32_t availableExtensionCount;
vkEnumerateDeviceExtensionProperties(physicalDevice, NULL, &availableExtensionCount, NULL);
std::vector availableExtensions(availableExtensionCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, NULL, &availableExtensionCount, availableExtensions.data());
std::set requireExtensionNames(requireExtensions.begin(), requireExtensions.end());
for (const auto& extension : availableExtensions)
{
requireExtensionNames.erase(extension.extensionName);
}
if (!requireExtensionNames.empty()) {
throw std::runtime_error("extension not fulfill");
}
// 7.2 create logical device
VkPhysicalDeviceFeatures deviceFeatures = {};
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
deviceCreateInfo.queueCreateInfoCount = 1;
deviceCreateInfo.pEnabledFeatures = &deviceFeatures;
deviceCreateInfo.enabledExtensionCount = requireExtensions.size();
deviceCreateInfo.ppEnabledExtensionNames = requireExtensions.data();
if (vkCreateDevice(physicalDevice, &deviceCreateInfo, NULL, &logicalDevice) != VK_SUCCESS)
{
throw std::runtime_error("failed to create logical device.");
}
// 7.3 retrieve device queue by logical device
vkGetDeviceQueue(logicalDevice, queue_ind, 0, &graphicsQueue);
// 7.4 create swap chain by logical device
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector formats;
std::vector presentModes;
};
SwapChainSupportDetails details;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &details.capabilities);
// 7.4.1 choose the surface format and present mode
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, NULL);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, details.formats.data());
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, NULL);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, details.presentModes.data());
}
if (formatCount == 0 || presentModeCount == 0) {
throw std::runtime_error(" no suitable format or present mode ");
}
int format_ind = -1;
for (const auto& availableFormat : details.formats)
{
format_ind++;
if (availableFormat.format == VK_FORMAT_R8G8B8A8_UNORM
&& availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
{
break;
}
}
int present_mode_ind = -1;
for (const auto& availablePresentMode : details.presentModes) {
present_mode_ind++;
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
break;
}
}
// 7.4.2 choose the extent
VkExtent2D actualExtent;
actualExtent.width = std::fmax(details.capabilities.minImageExtent.width,
std::fmin(details.capabilities.maxImageExtent.width, width));
actualExtent.height = std::fmax(details.capabilities.minImageExtent.height,
std::fmin(details.capabilities.maxImageExtent.height, height));
uint32_t imageCount = details.capabilities.minImageCount + 1;
if (details.capabilities.maxImageCount > 0 && imageCount > details.capabilities.maxImageCount) {
imageCount = details.capabilities.maxImageCount;
}
// 7.4.3 fill the VkSwapchainCreateInfoKHR and create .
VkSwapchainCreateInfoKHR swapChainCreateInfo = {};
swapChainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapChainCreateInfo.surface = surface;
swapChainCreateInfo.minImageCount = imageCount;
swapChainCreateInfo.imageFormat = details.formats[format_ind].format;
swapChainCreateInfo.imageColorSpace = details.formats[format_ind].colorSpace;
swapChainCreateInfo.imageExtent = actualExtent;
swapChainCreateInfo.imageArrayLayers = 1;
swapChainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
swapChainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapChainCreateInfo.preTransform = details.capabilities.currentTransform;
swapChainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
swapChainCreateInfo.presentMode = details.presentModes[present_mode_ind];
swapChainCreateInfo.clipped = VK_TRUE;
swapChainCreateInfo.oldSwapchain = VK_NULL_HANDLE;
if (vkCreateSwapchainKHR(logicalDevice, &swapChainCreateInfo, NULL, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("swap chain create fault ");
}
//7.5 create swapchain ImageView
std::vector swapChainImages;
vkGetSwapchainImagesKHR(logicalDevice, swapChain, &imageCount, NULL);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(logicalDevice, swapChain, &imageCount, swapChainImages.data());
std::vector swapChainImageViews;
swapChainImageViews.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImages.size(); i++) {
VkImageViewCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapChainCreateInfo.imageFormat;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(logicalDevice, &createInfo, NULL, &swapChainImageViews[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create image view.");
}
}
//7.6 create shader module
auto loadShaderByteCode = [](const std::string& fileName) {
std::ifstream loadFile(fileName, std::ios::ate | std::ios::binary);
if (!loadFile.is_open())
{
throw std::runtime_error("failed to open file!");
}
size_t fileSize = (size_t)loadFile.tellg();
std::vector buffer(fileSize);
loadFile.seekg(0);
loadFile.read(buffer.data(), fileSize);
loadFile.close();
return buffer;
};
auto vertShaderCode = loadShaderByteCode("vert.spv");
auto fragShaderCode = loadShaderByteCode("frag.spv");
auto createShaderModule = [](const std::vector& code) {
VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast(code.data());
VkShaderModule shaderModule;
if (vkCreateShaderModule(logicalDevice, &createInfo, NULL, &shaderModule) != VK_SUCCESS)
{
throw std::runtime_error("failed to create shader module .");
}
return shaderModule;
};
VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);
// 7.7 create pipeline
// 7.7.1 prepare shader stage
VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo , fragShaderStageInfo };
//7.7.2 prepare vertex input state
VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = NULL;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = NULL;
//7.7.3 prepare input assembly state
VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
//7.7.4 prepare viewport and scissor state
VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = width;
viewport.height = height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
VkRect2D scissor = {};
scissor.offset = { 0 , 0 };
scissor.extent = swapChainCreateInfo.imageExtent;
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
//7.7.5 prepare rasterization state
VkPipelineRasterizationStateCreateInfo rasterizer = {};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f;
rasterizer.depthBiasClamp = 0.0f;
rasterizer.depthBiasSlopeFactor = 0.0f;
//7.7.6 prepare multisample state
VkPipelineMultisampleStateCreateInfo multisampling = {};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f;
multisampling.pSampleMask = nullptr;
multisampling.alphaToCoverageEnable = VK_FALSE;
multisampling.alphaToOneEnable = VK_FALSE;
//7.7.7 prepare color blend state
VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
colorBlendAttachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;
//7.7.8 create render pass
VkAttachmentDescription colorAttachment = {};
colorAttachment.format = swapChainCreateInfo.imageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorAttachmentRef = {};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
VkRenderPass renderPass;
VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 0;
renderPassInfo.pDependencies = NULL;
if (vkCreateRenderPass(logicalDevice, &renderPassInfo, NULL, &renderPass) != VK_SUCCESS) {
throw std::runtime_error("failed to create render pass!");
}
//7.7.9 create pipeline layout
VkPipelineLayout pipelineLayout;
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pushConstantRangeCount = 0;
if (vkCreatePipelineLayout(logicalDevice, &pipelineLayoutInfo, NULL, &pipelineLayout) != VK_SUCCESS)
{
throw std::runtime_error("failed to create graphics pipeline . ");
}
// 7.7.10 merge all the state and create graphics pipeline
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = NULL;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = NULL;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
pipelineInfo.layout = pipelineLayout;
VkPipeline graphicsPipeline;
if (vkCreateGraphicsPipelines(logicalDevice, VK_NULL_HANDLE, 1, &pipelineInfo, NULL, &graphicsPipeline)
!= VK_SUCCESS)
{
throw std::runtime_error("failed to create graphics pipeline");
}
vkDestroyShaderModule(logicalDevice, fragShaderModule, nullptr);
vkDestroyShaderModule(logicalDevice, vertShaderModule, nullptr);
//7.7.11 create frame buffer
std::vector swapChainFrameBuffer;
swapChainFrameBuffer.resize(swapChainImageViews.size());
for (size_t i = 0; i < swapChainImageViews.size(); i++)
{
VkImageView attachments[] = {
swapChainImageViews[i]
};
VkFramebufferCreateInfo frameBufferInfo = {};
frameBufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
frameBufferInfo.renderPass = renderPass;
frameBufferInfo.width = width;
frameBufferInfo.height = height;
frameBufferInfo.layers = 1;
frameBufferInfo.pAttachments = attachments;
frameBufferInfo.attachmentCount = 1;
if (vkCreateFramebuffer(logicalDevice, &frameBufferInfo, NULL, &swapChainFrameBuffer[i]) != VK_SUCCESS)
{
throw std::runtime_error("create frame buffer fault . ");
}
}
//7.7.12 create command buffer
VkCommandPool commandPool;
VkCommandPoolCreateInfo poolCreateInfo = {};
poolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolCreateInfo.queueFamilyIndex = queue_ind;
poolCreateInfo.flags = 0;
if (vkCreateCommandPool(logicalDevice, &poolCreateInfo, NULL, &commandPool) != VK_SUCCESS)
{
throw std::runtime_error("create command pool fault . ");
}
commandBuffer.resize(swapChainFrameBuffer.size());
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t)commandBuffer.size();
if (vkAllocateCommandBuffers(logicalDevice, &allocInfo, commandBuffer.data()) != VK_SUCCESS)
{
throw std::runtime_error("alloc command buffer fault . ");
}
for (size_t i = 0; i < commandBuffer.size(); i++)
{
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0;
if (vkBeginCommandBuffer(commandBuffer[i], &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("failed to begin recording command buffer . ");
}
VkRenderPassBeginInfo renderPassBeginInfo = {};
renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassBeginInfo.renderPass = renderPass;
renderPassBeginInfo.framebuffer = swapChainFrameBuffer[i];
renderPassBeginInfo.renderArea.offset = { 0 , 0 };
renderPassBeginInfo.renderArea.extent = swapChainCreateInfo.imageExtent;
VkClearValue clearColor = { 0.0f , 0.0f , 0.0f , 1.0f };
renderPassBeginInfo.clearValueCount = 1;
renderPassBeginInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffer[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffer[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
vkCmdDraw(commandBuffer[i], 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffer[i]);
if (vkEndCommandBuffer(commandBuffer[i]) != VK_SUCCESS)
{
throw std::runtime_error("failed to record command buffer . ");
}
}
// 7.7.13 create semaphore
VkSemaphoreCreateInfo semaphoreInfo = {};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
if (vkCreateSemaphore(logicalDevice, &semaphoreInfo, NULL, &imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(logicalDevice, &semaphoreInfo, NULL, &renderFinishedSemaphore) != VK_SUCCESS)
{
throw std::runtime_error("failed to create semaphore . ");
}
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
uint32_t imageIndex;
uint64_t limit = (std::numeric_limits::max)();
vkAcquireNextImageKHR(logicalDevice, swapChain, limit, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = { imageAvailableSemaphore };
VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer[imageIndex];
VkSemaphore signalSemaphores[] = { renderFinishedSemaphore };
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer . ");
}
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = { swapChain };
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = NULL;
vkQueuePresentKHR(graphicsQueue, &presentInfo);
}
//clean up
for (auto framebuffer : swapChainFrameBuffer) {
vkDestroyFramebuffer(logicalDevice, framebuffer, nullptr);
}
// vkDestroySemaphore(logicalDevice, renderFinishedSemaphore, nullptr);
vkDestroySemaphore(logicalDevice, imageAvailableSemaphore, nullptr);
vkDestroyCommandPool(logicalDevice, commandPool, nullptr);
vkDestroyPipeline(logicalDevice, graphicsPipeline, nullptr);
vkDestroyPipelineLayout(logicalDevice, pipelineLayout, nullptr);
vkDestroyRenderPass(logicalDevice, renderPass, nullptr);
for (auto imageView : swapChainImageViews) {
vkDestroyImageView(logicalDevice, imageView, nullptr);
}
vkDestroySwapchainKHR(logicalDevice, swapChain, nullptr);
vkDestroyDevice(logicalDevice, nullptr);
auto destroyFunc = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (destroyFunc != nullptr) {
destroyFunc(instance, debugMessenger, NULL);
}
else {
throw std::runtime_error(" not find function PFN_vkDestroyDebugUtilsMessengerEXT . ");
}
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}