Ch1-2 初始化流程
为什么第一章的标题不是“从零到三角形”?因为步骤太多了!所以我分了两章来讲。
初始化一个Vulkan应用程序所必备的步骤已足以算得上繁多,在这一节先对所需步骤进行总结,解释名词概念,并定义各步骤中将要书写的函数及自建类型之名称。
单例类
之前在VKBase.h中添加了一个叫vulkan的命名空间,现在该命名空间中定义一个单例类graphicsBase,该类将被用于管理Vulkan中那些最基础的对象和行为。
class graphicsBase { //静态变量 static graphicsBase singleton; //-------------------- graphicsBase() = default; graphicsBase(graphicsBase&&) = delete; ~graphicsBase() { /*待Ch1-4填充*/ } public: //静态函数 //该函数用于访问单例 static graphicsBase& Base() { return singleton; } }; inline graphicsBase graphicsBase::singleton;
-
定义移动构造器(删除也算进行了定义),且没有定义复制构造器、复制赋值、移动赋值时,上述四个函数将全部无法使用,由此构成单例类。
初始化流程
一个呈现图像的Vulkan应用程序需经历以下的步骤以初始化:
1.创建Vulkan实例
2.创建debug messenger(若编译选项为DEBUG)
3.创建window surface
4.选择物理设备并创建逻辑设备,取得队列
5.创建交换链
什么是Vulkan实例?
应用程序必须显式地告诉操作系统,说明其需要使用Vulkan的功能,这一步是由创建Vulkan实例(VkInstance)来完成的。
Vulkan实例的底层是一系列Vulkan运行所必需的用于记录状态和信息的变量。
什么是debug messenger?
Debug messenger用于获取验证层所捕捉到的debug信息。若没有这东西,Vulkan编程可谓寸步难行。
什么是window surface?
Vulkan是平台无关的API,你必须向其提供一个window surface(VkSurfaceKHR),以和平台特定的窗口对接。
什么是物理设备和逻辑设备?
物理设备即图形处理器,通常为GPU。Vulkan中所谓的物理设备虽称物理(physical),但并非必须是实体设备。
VkPhysicalDevice类型指代物理设备,从这类handle只能获取物理设备信息。VkDevice类型是逻辑设备的handle,逻辑设备是编程层面上用来与物理设备交互的对象,关于分配设备内存、创建Vulkan相关对象的命令大都会被提交给逻辑设备。
什么是队列?
队列(VkQueue)类似于线程,命令被提交到队列执行,Vulkan官方标准中将队列描述为命令执行引擎的接口:
Vulkan queues provide an interface to the execution engines of a device.
Vulkan核心功能中规定队列支持的操作类型包括图形、计算、数据传送、稀疏绑定四种,图形和计算队列必定也支持数据传送。一族功能相同的队列称为队列族。
任何支持Vulkan的显卡驱动确保你能找到至少一个同时支持图形和计算操作的队列族。
什么是交换链?
在将一张图像用于渲染或其他类型的写入时,已渲染好的图像可以被呈现引擎读取,如此交替呈现在窗口中的数张图像的集合即为交换链。
创建Vulkan实例的步骤
创建Vulkan实例的步骤依序为:
1.确定所需的实例级别层及扩展,不检查是否可用
2.用vkCreateInstance(...)创建Vulkan实例
什么是层和扩展?
层和扩展在Vulkan的核心功能之外,提供了对特定需求、特定平台的支持,还包括特定厂商提供的功能。“扩展”一词顾名思义,而层与扩展的主要区别在于,层有显著的作用范围,比如验证层会检查几乎所有Vulkan相关的函数。此外,有些扩展需要特定的层才能使用。
层只有实例级别的(Vulkan1.0中定义过设备级别的层,但这一概念之后被废弃,Vulkan1.0版本中亦不存在设备级别独占的层),而扩展则分实例级别和设备级别,前者与特定设备无关,可能作用于所有Vulkan相关的内容,而后者则作用于与特定设备相关的内容。
为什么不在创建Vulkan实例前检查层和扩展的可用性?
如果你读过一些其他的Vulkan教程,你可能有读到在创建Vulkan实例前,会先检查层和扩展是否可用,我选择跳过这一步。
vkCreateInstance(...)在创建Vulkan实例时本就会检查层和扩展的可用性,该函数在无法满足所需层时返回VK_ERROR_LAYER_NOT_PRESENT,在无法满足所需扩展时返回VK_ERROR_EXTENSION_NOT_PRESENT。因此,我建议根据vkCreateInstance(...)的返回值,仅在创建Vulkan实例失败时检查层和扩展的可用性。
于是在graphicsBase中加入以下内容:
private: //单例类对象是静态的,未设定初始值亦无构造函数的成员会被零初始化 VkInstance instance; std::vector<const char*> instanceLayers; std::vector<const char*> instanceExtensions; //该函数用于向instanceLayers或instanceExtensions容器中添加字符串指针,并确保不重复 static void AddLayerOrExtension( std::vector<const char*>& container, const char* name) { for (auto& i : container) if (!strcmp(name, i))//strcmp(...)在字符串匹配时返回0 return; //如果层/扩展的名称已在容器中,直接返回 container.push_back(name); } public: //Getter VkInstance Instance() const { return instance; } const std::vector<const char*>& InstanceLayers() const { return instanceLayers; } const std::vector<const char*>& InstanceExtensions() const { return instanceExtensions; } //以下函数用于创建Vulkan实例前 void AddInstanceLayer(const char* layerName) { AddLayerOrExtension(instanceLayers, layerName); } void AddInstanceExtension(const char* extensionName) { AddLayerOrExtension(instanceExtensions, extensionName); } //该函数用于创建Vulkan实例 VkResult CreateInstance(VkInstanceCreateFlags flags = 0) { /*待Ch1-3填充*/ } //以下函数用于创建Vulkan实例失败后 VkResult CheckInstanceLayers(std::span<const char*> layersToCheck) { /*待Ch1-3填充*/ } void InstanceLayers(const std::vector<const char*>& layerNames) { instanceLayers = layerNames; } VkResult CheckInstanceExtensions(std::span<const char*> extensionsToCheck, const char* layerName = nullptr) const { /*待Ch1-3填充*/ } void InstanceExtensions(const std::vector<const char*>& extensionNames) { instanceExtensions = extensionNames; }
-
各种flags会之后在Ch1-3再做说明。
-
我打算让CheckInstanceLayers(...)和CheckInstanceExtensions(...)将传入的span中不可用的层和扩展设置为nullptr,然后与原本保存的instanceLayers和instanceExtensions比对,即可确定哪些层和扩展不可用。返回值则指示在获取可用层或扩展列表时是否发生错误。
于是剧本流程是这样的:
1.在创建Vulkan实例前,用AddInstanceLayer(...)和AddInstanceExtension(...)向对应的vector中添加指向层和扩展名称的指针。
2.然后尝试用CreateInstance(...)创建Vulkan实例。
3.若创建Vulkan实例失败,若vkCreateInstance(...)返回VK_ERROR_LAYER_NOT_PRESENT,从InstanceLayers()复制一份instanceLayers,用CheckInstanceLayers(...)检查可用性,若不可用的仅为非必要的层,创建一份去除该层后的vector,用InstanceLayers(...)复制给instanceLayers。返回VK_ERROR_EXTENSION_NOT_PRESENT的情况亦类似。然后重新尝试创建Vulkan实例。
创建debug messenger的步骤
创建了Vulkan实例后,即可创建debug messenger,以便检查初始化流程中的所有其他步骤。
创建debug messenger可以粗略地说是只有一步,在graphicsBase中加入以下内容:
private: VkDebugUtilsMessengerEXT debugMessenger; //以下函数用于创建debug messenger VkResult CreateDebugMessenger() { /*待Ch1-3填充*/ }
创建window surafce的步骤
在创建了glfw窗口和Vulkan实例后,用glfwCreateWindowSurface(...)创建window surface。这一步会写在之前的InitializeWindow(...)中。
如果你是自行用WindowsAPI创建的窗口,用vkCreateWin32SurfaceKHR(...)来创建窗口window surface,具体参考示例代码中的WinGeneral.hpp。
创建了window surface后当然需要将其存起来以备使用,于是在graphicsBase中加入以下内容:
private: VkSurfaceKHR surface; public: //Getter VkSurfaceKHR Surface() const { return surface; } //该函数用于选择物理设备前 void Surface(VkSurfaceKHR surface) { if (!this->surface) this->surface = surface; }
-
如果你有多个窗口(应用程序可以有子窗口),当然也会有多个window surface,以及多个交换链,本套教程对此不作考虑。
创建逻辑设备的步骤
创建逻辑设备的步骤依序为:
1.获取物理设备列表
2.检查物理设备是否满足所需的队列族类型,从中选择能满足要求的设备并顺便取得队列族索引
3.确定所需的设备级别扩展,不检查是否可用
4.用vkCreateDevice(...)创建逻辑设备,取得队列
5.取得物理设备属性、物理设备内存属性,以备之后使用
-
不检查扩展是否可用的原因同前文不检查实例级别层和扩展是否可用的原因相似,vkCreateDevice(...)本就会对扩展是否可用进行检查。
-
第5步的内容是以创建逻辑设备成功,确定将要使用的物理设备不再变动为前提的,若你对物理设备的性能有要求,也可以在第2步前自行取得各个物理设备的属性,进行甄选。
根据情况,一共需要三种类型的队列:图形、呈现、计算。
呈现队列并非是Vulkan核心功能中规定的队列类型。几乎可以肯定,GPU必定会有一个同时支持图形和呈现的队列族(该说法来自谷歌的搜索结果,我认为是准确的,考虑到没找到与此相悖的报告),但标准中并没有作此规定,因此保险起见,我将其单独列出来。
-
如果你的程序没有图形界面(比如,仅仅用于对图像做某种处理的控制台程序),那么呈现队列非必须。
-
如果你不需要GPU计算或间接渲染(将CPU上的一些计算扔到GPU上然后再从计算结果做渲染),那么计算队列非必须。
-
如果你只打算搞计算(GPU是高度并行的计算设备)而不搞渲染,那么图形队列非必须。
于是在graphicsBase中加入以下内容:
private: VkPhysicalDevice physicalDevice; VkPhysicalDeviceProperties physicalDeviceProperties; VkPhysicalDeviceMemoryProperties physicalDeviceMemoryProperties; std::vector<VkPhysicalDevice> availablePhysicalDevices; VkDevice device; //有效的索引从0开始,因此使用特殊值VK_QUEUE_FAMILY_IGNORED(为UINT32_MAX)为队列族索引的默认值 uint32_t queueFamilyIndex_graphics = VK_QUEUE_FAMILY_IGNORED; uint32_t queueFamilyIndex_presentation = VK_QUEUE_FAMILY_IGNORED; uint32_t queueFamilyIndex_compute = VK_QUEUE_FAMILY_IGNORED; VkQueue queue_graphics; VkQueue queue_presentation; VkQueue queue_compute; std::vector<const char*> deviceExtensions; //该函数被DeterminePhysicalDevice(...)调用,用于检查物理设备是否满足所需的队列族类型,并将对应的队列族索引返回到queueFamilyIndices,执行成功时直接将索引写入相应成员变量 VkResult GetQueueFamilyIndices(VkPhysicalDevice physicalDevice, bool enableGraphicsQueue, bool enableComputeQueue, uint32_t (&queueFamilyIndices)[3]) { /*待Ch1-3填充*/ } public: //Getter VkPhysicalDevice PhysicalDevice() const { return physicalDevice; } const VkPhysicalDeviceProperties& PhysicalDeviceProperties() const { return physicalDeviceProperties; } const VkPhysicalDeviceMemoryProperties& PhysicalDeviceMemoryProperties() const { return physicalDeviceMemoryProperties; } VkPhysicalDevice AvailablePhysicalDevice(uint32_t index) const { return availablePhysicalDevices[index]; } uint32_t AvailablePhysicalDeviceCount() const { return uint32_t(availablePhysicalDevices.size()); } VkDevice Device() const { return device; } uint32_t QueueFamilyIndex_Graphics() const { return queueFamilyIndex_graphics; } uint32_t QueueFamilyIndex_Presentation() const { return queueFamilyIndex_presentation; } uint32_t QueueFamilyIndex_Compute() const { return queueFamilyIndex_compute; } VkQueue Queue_Graphics() const { return queue_graphics; } VkQueue Queue_Presentation() const { return queue_presentation; } VkQueue Queue_Compute() const { return queue_compute; } const std::vector<const char*>& DeviceExtensions() const { return deviceExtensions; } //该函数用于创建逻辑设备前 void AddDeviceExtension(const char* extensionName) { AddLayerOrExtension(deviceExtensions, extensionName); } //该函数用于获取物理设备 VkResult GetPhysicalDevices() { /*待Ch1-3填充*/ } //该函数用于指定所用物理设备并调用GetQueueFamilyIndices(...)取得队列族索引 VkResult DeterminePhysicalDevice(uint32_t deviceIndex = 0, bool enableGraphicsQueue, bool enableComputeQueue = true) { /*待Ch1-3填充*/ } //该函数用于创建逻辑设备,并取得队列 VkResult CreateDevice(VkDeviceCreateFlags flags = 0) { /*待Ch1-3填充*/ } //以下函数用于创建逻辑设备失败后 VkResult CheckDeviceExtensions(std::span<const char*> extensionsToCheck, const char* layerName = nullptr) const { /*待Ch1-3填充*/ } void DeviceExtensions(const std::vector<const char*>& extensionNames) { deviceExtensions = extensionNames; }
-
检查硬件是否支持光追管线的方式与检查是否支持图形和计算管线的方式不同,这里暂不作考虑。
于是剧本流程依序是这样的:
1.用GetPhysicalDevices()获取可用物理设备的列表。
2.然后,用DeterminePhysicalDevice(...)判断将要使用的物理设备是否满足要求,如满足则将其记录为所用设备。这一步需要判断物理设备是否支持window surface,正是因此创建window surface一步须在此之前。
3.然后,用AddDeviceExtension(...)向deviceExtensions中添加指向各扩展名称的指针。
4.然后尝试用CreateDevice(...)创建逻辑设备,若创建成功,该函数会取得队列。
5.若创建逻辑设备失败,尝试用DeterminePhysicalDevice(...)更换物理设备。到这一步为止仍没有检查扩展,如果你首先选用的是性能较好的物理设备,且优先看重性能(而非支持的扩展),那么这一步与下一步可互换。
6.若遍历所有物理设备后仍无法创建逻辑设备且vkCreateDevice(...)返回VK_ERROR_EXTENSION_NOT_PRESENT,从DeviceExtensions()复制一份deviceExtensions,嵌套循环中用DeterminePhysicalDevice(...)遍历物理设备(会有代码防止反复取得队列族索引),用CheckDeviceExtensions(...)检查扩展可用性,对于某物理设备,若不可用的仅为非必要的扩展(不过为应对没有某个扩展而书写两套代码可能会很麻烦),创建一份去除该扩展后的vector,用DeviceExtensions(...)复制给deviceExtensions。然后重新尝试创建逻辑设备。
上述流程优先考虑在最佳情况下获得最快的初始化速度,因而显得有些繁琐(最佳情况下不需要检查扩展,最糟糕的情况为遍历所有物理设备一次后,因全数无法满足所需扩展,再次遍历并对每个设备检查扩展可用性),如果你的应用程序用到了显卡未必支持但对于程序运行非必要的扩展,且程序对显卡性能没有要求,可以先检查扩展(最佳情况下需要检查扩展一次,最糟糕的情况下需要遍历所有物理设备一次并对每个设备检查扩展可用性一次)。
创建交换链的步骤
创建交换链的步骤依序为:
1.填写一大堆信息
2.创建交换链并取得交换链图像,为交换链图像创建image view
什么是image view?
Vulkan中,VkImage引用一片物理设备内存,将该片内存上的数据用作图像,而VkImageView指定图像的使用方式,比如,一个6层的二维图像,可以用作6层的二维图像数组,也可用作天空盒。
书写创建交换链的函数时,重要的一点是要考虑重建交换链,于是在graphicsBase中加入以下内容:
private: std::vector <VkSurfaceFormatKHR> availableSurfaceFormats; VkSwapchainKHR swapchain; std::vector <VkImage> swapchainImages; std::vector <VkImageView> swapchainImageViews; //保存交换链的创建信息以便重建交换链 VkSwapchainCreateInfoKHR swapchainCreateInfo = {}; //该函数被CreateSwapchain(...)和RecreateSwapchain()调用 VkResult CreateSwapchain_Internal() { /*待Ch1-4填充*/ } public: //Getter const VkFormat& AvailableSurfaceFormat(uint32_t index) const { return availableSurfaceFormats[index].format; } const VkColorSpaceKHR& AvailableSurfaceColorSpace(uint32_t index) const { return availableSurfaceFormats[index].colorSpace; } uint32_t AvailableSurfaceFormatCount() const { return uint32_t(availableSurfaceFormats.size()); } VkSwapchainKHR Swapchain() const { return swapchain; } VkImage SwapchainImage(uint32_t index) const { return swapchainImages[index]; } VkImageView SwapchainImageView(uint32_t index) const { return swapchainImageViews[index]; } uint32_t SwapchainImageCount() const { return uint32_t(swapchainImages.size()); } const VkSwapchainCreateInfoKHR& SwapchainCreateInfo() const { return swapchainCreateInfo; } VkResult GetSurfaceFormats() { /*待Ch1-4填充*/ } VkResult SetSurfaceFormat(VkSurfaceFormatKHR surfaceFormat) { /*待Ch1-4填充*/ } //该函数用于创建交换链 VkResult CreateSwapchain(bool limitFrameRate = true, VkSwapchainCreateFlagsKHR flags = 0) { /*待Ch1-4填充*/ } //该函数用于重建交换链 VkResult RecreateSwapchain() { /*待Ch1-4填充*/ }
-
我定义SetSurfaceFormat(...)意在指定HDR这类特殊的色彩空间,在此之前需手动调用GetSurfaceFormats()来取得surface的可用格式。
-
若因为不需要指定surface格式或色彩空间而没有手动调用GetSurfaceFormats(),让CreateSwapchain(...)将调用它。
于是剧本流程依序是这样的:
1.若需要指定交换链的图像格式和色彩空间,用GetSurfaceFormats()来取得surface的可用格式到availableSurfaceFormats,用SetSurfaceFormat(...)验证并指定图像格式和色彩空间。
2.用CreateSwapchain(...)创建交换链。
3.在之后的运行中,窗口大小改变时(可通过GLFW的回调函数,或WindowsAPI中的message得知),调用RecreateSwapchain()。
若要使用最新版Vulkan
如果你想使用Vulkan的最新版本,须在创建Vulkan实例前,取得当前运行环境所支持的最新Vulkan版本,在graphicsBase中加入以下内容:
private: uint32_t apiVersion = VK_API_VERSION_1_0; public: //Getter uint32_t ApiVersion() const { return apiVersion; } VkResult UseLatestApiVersion() { /*待Ch1-3填充*/ }
代码汇总
本节中总共定义了如下内容:
class graphicsBase { uint32_t apiVersion = VK_API_VERSION_1_0; VkInstance instance; VkPhysicalDevice physicalDevice; VkPhysicalDeviceProperties physicalDeviceProperties; VkPhysicalDeviceMemoryProperties physicalDeviceMemoryProperties; std::vector<VkPhysicalDevice> availablePhysicalDevices; VkDevice device; uint32_t queueFamilyIndex_graphics = VK_QUEUE_FAMILY_IGNORED; uint32_t queueFamilyIndex_presentation = VK_QUEUE_FAMILY_IGNORED; uint32_t queueFamilyIndex_compute = VK_QUEUE_FAMILY_IGNORED; VkQueue queue_graphics; VkQueue queue_presentation; VkQueue queue_compute; VkSurfaceKHR surface; std::vector <VkSurfaceFormatKHR> availableSurfaceFormats; VkSwapchainKHR swapchain; std::vector <VkImage> swapchainImages; std::vector <VkImageView> swapchainImageViews; VkSwapchainCreateInfoKHR swapchainCreateInfo = {}; std::vector<const char*> instanceLayers; std::vector<const char*> instanceExtensions; std::vector<const char*> deviceExtensions; VkDebugUtilsMessengerEXT debugMessenger; //静态变量 static graphicsBase singleton; //-------------------- graphicsBase() = default; graphicsBase(graphicsBase&&) = delete; ~graphicsBase() { /*待Ch1-4填充*/ } Non-const函数 VkResult CreateSwapchain_Internal() { /*待Ch1-4填充*/ } VkResult GetQueueFamilyIndices(VkPhysicalDevice physicalDevice, bool enableGraphicsQueue, bool enableComputeQueue, uint32_t (&queueFamilyIndices)[3]) { /*待Ch1-3填充*/ } VkResult CreateDebugMessenger() { /*待Ch1-3填充*/ } public: //Getter uint32_t ApiVersion() const { return apiVersion; } VkInstance Instance() const { return instance; } VkPhysicalDevice PhysicalDevice() const { return physicalDevice; } const VkPhysicalDeviceProperties& PhysicalDeviceProperties() const { return physicalDeviceProperties; } const VkPhysicalDeviceMemoryProperties& PhysicalDeviceMemoryProperties() const { return physicalDeviceMemoryProperties; } VkPhysicalDevice AvailablePhysicalDevice(uint32_t index) const { return availablePhysicalDevices[index]; } uint32_t AvailablePhysicalDeviceCount() const { return uint32_t(availablePhysicalDevices.size()); } VkDevice Device() const { return device; } uint32_t QueueFamilyIndex_Graphics() const { return queueFamilyIndex_graphics; } uint32_t QueueFamilyIndex_Presentation() const { return queueFamilyIndex_presentation; } uint32_t QueueFamilyIndex_Compute() const { return queueFamilyIndex_compute; } VkQueue Queue_Graphics() const { return queue_graphics; } VkQueue Queue_Presentation() const { return queue_presentation; } VkQueue Queue_Compute() const { return queue_compute; } VkSurfaceKHR Surface() const { return surface; } const VkFormat& AvailableSurfaceFormat(uint32_t index) const { return availableSurfaceFormats[index].format; } const VkColorSpaceKHR& AvailableSurfaceColorSpace(uint32_t index) const { return availableSurfaceFormats[index].colorSpace; } uint32_t AvailableSurfaceFormatCount() const { return uint32_t(availableSurfaceFormats.size()); } VkSwapchainKHR Swapchain() const { return swapchain; } VkImage SwapchainImage(uint32_t index) const { return swapchainImages[index]; } VkImageView SwapchainImageView(uint32_t index) const { return swapchainImageViews[index]; } uint32_t SwapchainImageCount() const { return uint32_t(swapchainImages.size()); } const VkSwapchainCreateInfoKHR& SwapchainCreateInfo() const { return swapchainCreateInfo; } const std::vector<const char*>& InstanceLayers() const { return instanceLayers; } const std::vector<const char*>& InstanceExtensions() const { return instanceExtensions; } const std::vector<const char*>& DeviceExtensions() const { return deviceExtensions; } //Const函数 VkResult CheckInstanceLayers(std::span<const char*> layersToCheck) const { /*待Ch1-3填充*/ } VkResult CheckInstanceExtensions(std::span<const char*> extensionsToCheck, const char* layerName = nullptr) const { /*待Ch1-3填充*/ } VkResult CheckDeviceExtensions(std::span<const char*> extensionsToCheck, const char* layerName = nullptr) const { /*待Ch1-3填充*/ } //Non-const函数 void AddInstanceLayer(const char* layerName) { instanceLayers.push_back(layerName); } void AddInstanceExtension(const char* extensionName) { instanceExtensions.push_back(extensionName); } void AddDeviceExtension(const char* extensionName) { deviceExtensions.push_back(extensionName); } VkResult UseLatestApiVersion() { /*待Ch1-3填充*/ } VkResult CreateInstance(VkInstanceCreateFlags flags = 0) { /*待Ch1-3填充*/ } void Surface(VkSurfaceKHR surface) { if (!this->surface) this->surface = surface; } VkResult GetPhysicalDevices() { /*待Ch1-3填充*/ } VkResult DeterminePhysicalDevice(uint32_t deviceIndex = 0, bool enableGraphicsQueue, bool enableComputeQueue = true) { /*待Ch1-3填充*/ } VkResult CreateDevice(VkDeviceCreateFlags flags = 0) { /*待Ch1-3填充*/ } VkResult GetSurfaceFormats() { /*待Ch1-4填充*/ } VkResult SetSurfaceFormat(VkSurfaceFormatKHR surfaceFormat) { /*待Ch1-4填充*/ } VkResult CreateSwapchain(bool limitFrameRate = true, VkSwapchainCreateFlagsKHR flags = 0) { /*待Ch1-4填充*/ } void InstanceLayers(const std::vector<const char*>& layerNames) { instanceLayers = layerNames; } void InstanceExtensions(const std::vector<const char*>& extensionNames) { instanceExtensions = extensionNames; } void DeviceExtensions(const std::vector<const char*>& extensionNames) { deviceExtensions = extensionNames; } VkResult RecreateSwapchain() { /*待Ch1-4填充*/ } //静态函数 static graphicsBase& Base() { return singleton; } }; inline graphicsBase graphicsBase::singleton;