此文仅记录我对Vulkan的学习心得和记录,欢迎技术交流,非专业处请多指教
从技术层面上讲,Vulkan相比于OpenGL更贴近于硬件底层,它更容易实现资源的读取。 同时,Vulkan也比OpenGL更专注于图形渲染,而把上下文Context的创建和配置(如内存的分配,命令的存储和执行以及渲染管线等等)交给了用户程序。
本文主要关于Vulkan渲染环境的创建,即创建 Window,Surface和Context。在OpenGL中,一个Context对应一个线程,而Vulkan由于支持对线程,因此,我们能够为每个线程配置相应的Context。
Vulkan和 OpenGL的渲染环境创建都可分为如下几步:
1 创建窗口
2 获取连接的设备
3 创建Surface
4 创建上下文
一: 创建窗口
创建窗口是与平台有关的事,与图形渲染API关系不是很大,对于 Windows可能需要MFC来创建一个窗口句柄,对于Linux/Unix需要用XLib来获取一个x window。而在Android平台下, 直接可以通过Java传递的Surface获取一个 Native 窗口。
以Android平台(因为作者是做移动开发的,所以后续都会以移动平台为例)作为参考,
ANativeWindow *window = ANativeWindow_fromSurface(jenv, surface);
二: 获取连接的设备
获取窗口后,我们需要与设备连接,这个设备也就是我们的GPU, 在opengl里 它叫做display, 而在vulkan里,它是Device。
对于OpenGL,
EGLDisplay _display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
然后, eglInitialize(_display, &majorVersion, &minorVersion); 这就算是创建设备了。
对于Vulkan:
获取设备Device,首先必须满足两个条件,第一它必须支持 图形渲染, 第二,它必须支持Presenting,即对 图像显示 到窗口的支持。
Device能够为我们提供队列树,每个队列树都包含了多个具有相似功能的队列,我们往这些队列中提交命令, 设备会按序对这些队列进行处理。
1 创建Vulkan实例 Instance
Vulkan的Api有个特点是,如果我们想创建某个对象,必须提供这个对象的创建信息。
首先,我们需要指定实例创建信息:
实例创建信息被定义在结构体中, 如下
typedef struct VkInstanceCreateInfo{ VkStructureType sType; const void* pNext; VkInstanceCreateFlags flags; const VkApplicationInfo* pApplicationInfo; uint32_t enabledLayerCount; const char* const* ppEnabledLayerNames; uint32_t enabledExtensionCount; const char* const* ppEnabledExtensionNames; }VkInstanceCreateInfo;
其中 sType 可指定为 VK_STRUCTURE_TYPE_APPLICATION_INFO;
而 apiVersion 可以为 VK_MAKE_VERSION(1, 0,2); VK_MAKE_VERSION 为vulkan提供制作版本号的宏。
实例支持的扩展与层 可以通过在cmd中输入vulkaninfo命令查看:
如我的PC 独立显卡为 GForce 860, 其实例扩展有:
VK_KHR_surface, VK_KHR_win32_surface, VK_EXT_debug_report
支持的层有:
(它们的名字 都以 VK_LAYER_LUNARG开头)
api_dump
device_limits
draw_state
image
mem_tracker
param_checker
screenshot
swapchain
threading
vktrace
由于Vulkan是跨平台的API,所以它必须定义扩展来支持不同的平台硬件,而且Vulkan还兼容了所有OpenGL的扩展。另外,Vulkan提供了一种分层架构,使得诸如正确性验证和调试信息等可以按需加载。
有了实例创建信息,我们就可以创建实例了:
VKResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
其中,VkAllocationCallbacks 可以暂时为NULL
2 创建所需设备:
设备是允许客户端与物理设备(GPU)唯一的交互接口。 设备主要通过提供队列来向GPU提交命令, vulkan支持多线程实际上是因为它能允许客户端同步/异步操作多个队列来 实现的,相同性能的队列都存在一个队列树中。如果,我们需要做图形渲染,那么我们必须选择能够支持图形渲染的队列树。
首先,我们可以通过实例来列举出电脑上安装的物理设备(即显卡,确切的说应该是GPU):
VkResult vkEnumeratePhysicalDevices(VkInstance instance, uint32_t pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices);
如果pPhysicalDevices是NULL, 那么就会返回物理设备的个数,根据个数,我们分配pPhysicalDeivces的大小,再次通过该函数返回具体的物理设备:
(以我的电脑为例)
我们还可以得到物理设备的物理属性:
void vkGetPhysicalDeviceProperties(VkPhysicalDevice physicalDevice, VkPhysicalDeviceProperties* pProperties);
其中pProperties是一个结构体,获取值如下:
其结构体表示如下:
typedef struct VkPhysicalDeviceProperties{ uint32_t apiVersion; uint32_t driverVersion; uint32_t vendorID; uint32_t deviceID; VkPhysicalDeviceType deviceType; char deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE]; uint8_t pipelineCacheUUID[VK_UUID_SIZE]; VkPhysicalDeviceLimits limits; VkPhysicalDeviceSparseProperties sparseProperties; } VkPhysicalDeviceProperties;
可以看出我的GPU是 DISCRETE_GPU,即独立显卡。
物理设备只是提供我们对于电脑上设备的相关信息,它不会直接影响我们的操作。
其次,我们需要根据物理设备和实例创建设备:
根据惯例,创建设备需要设备创建信息:
typedef struct VkDeviceCreateInfo{ VkStructureType sType; const void* pNext; VkDeviceCreateFlags flags; uint32_t queueCreateInfoCount; const VkDeviceQueueCreateInfo* pQueueCreateInfo; uint32_t enabledLayerCount; const char* const* ppEnabledLayerNames; uint32_t enabledExtensionCount; const char* const* ppEnabledExtensionNames; const VkPhysicalDeviceFeatures* pEnabledFeatures; }VkDeviceCreateInfo;
可以看出,
1它可以指定扩展和层 :
扩展:
层:
其层与实例支持的层差不多。
2 它需要队列创建信息,这是因为我们在创建设备后就已经为该设备创建了队列。
VkDeviceQueueCreateInfo定义如下:
typedef struct VkDeviceQueueCreateInfo{ VkStructureType sType; const void* pNext; VkDeviceQueueCreateFlags flags; uint32_t queueFamilyIndex; uint32_t queueCount; const float* pQueuePriorites; }VkDeviceQueueCreateInfo;
其中queueFamilyIndex是我们需要确认的在物理设备中获取的队列树数组中支持图形渲染的队列树的索引。
pQueuePriorities是 该队列的优先级,取值0.0~1.0, 如果我们只创建一个队列,那么可以只指定一个值,一般为0.0。 如果我们创建3个队列,队列优先级越来越高,那么指定的数组为{a, b, c}, 0.0<a<b<c<1.0;
我们可以通过获取物理设备上队列树的属性来找到支持图形渲染的队列树:
void vkGetPhysicalDeviceFamilyProperties(VkPhysicalDevice physicalDevice,
uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties* pQueueFamilyProperties);
按照惯例,如果pQueueFamilyProperties 为NULL,返回的是可找到属性的个数, 然后分配内存,获取pQueueFamilyProperties的值:
这里,我们只关心queueFlags, 它实际上是uint32_t的类型,其值是以下枚举值:
typdef enum VkQueueFlagBits{ VK_QUEUE_GRAPHICS_BIT = 0x00000001; // 表示队列树种队列支持图形操作 VK_QUEUE_COMPUTE_BIT = 0x00000002; //表示队列中队列支持计算操作 VK_QUEUE_TRANSFER_BBIT = 0x00000004;//支持转移操作 VK_QUEUE_SPARSE_BINDING_BIT = 0x00000008; //支持离散内存管理操作 }VkQueueFlagBits;
所以对于 第i个队列树属性,如果其queueFlags为VK_QUEUE_GRAPHIS_BIT, 那么queueFamilyIndex 便为i。
最后,有了设备创建信息后,我们可以创建设备了:
VkResult vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice);
三 创建Surface
对于OpenGL,
1 选择合适的Surface配置信息
首先,指定配置属性 EGLConfig config
EGLint attribList[], 这里面可以指定rgba通道大小,Depth/Stencil大小,采样buffer数目等。
然后, eglChooseConfig(_display, attribList, &config, 1, &numConfigs);
最后, 选择与窗口前后buffer的配置并创建Surface
EGLint winAttribList[] = {
EGL_RENDER_BUFFER, EGL_BACK_BUFFER,
EGL_NONE
};
_surface = eglCreateWindowSurface(_display, config, (EGLNativeWindowType) _window, winAttribList);
对于Vulkan,
1 创建Surface
查看Vukan1.0 Quick Reference: (这里以Android端为例)
2 配置SwapChain的信息,准备SwapChain
OpenGL里,我们可以看到,我们在准备Surface时,指定了图像像素格式,深度以及front/back buffers等属性。 这些属性其实都与渲染图显示到窗口有关。
在Vulkan里, Device有一个专门的扩展来管理窗口配置和窗口显示,叫VK_KHR_SwapChian。关于SwapChian 我会在下文着重研究
四 创建上下文:
所谓上下文,就是一个应用程序的环境相关的全局信息。
对于OpenGL:
OpenGL是一个经过多层封装的图形API,我们只能通过其最上层的接口,来提交渲染信息,完成渲染。 而关于 这些信息存储的创建、分配,以及提交命令的执行,渲染管线我们都不可见, 而这些不可见的信息,都包含在了这个上下文中。
1 创建上下文
EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION,num, EGL_NONE};
_context = eglCreateContext(_display, config, shaderdContext, contextAttribs);
其中,shaderdContext可以是其它渲染线程的上下文,也可以为EGL_NO_CONTEXT
2使用当前上下文
当我们想在某个线程中执行渲染,那么我们需要使用该线程下的上下文,即使其上下文成为当前。
eglMakeCurrent(_display, _surface, _surface, _context);
其中两个_surface分别是读写surface,读写surface也可以是同一个surface。
对于 Vulkan,
除了最后draw方法,即绘制图形的过程没有暴露给用户,其它的都是用户可配置的。因此,它不像OpenGL中那样,有一个简单的Context。而是由一个可配置的Context,即包含了Commandbuffer, pipeline, renderPass等, 对于多线程来说,我们需要为每个线程配置一个Context。
总结:
本文通过Opengl,来分析从窗口创建、到创建渲染环境,来探究Vulkan渲染环境创建的研究。 通过对比OpenGL和 Vulkan的配置方式,发现它们的异同。 对于Vulkan来说,首先需要创建实例,然后通过该实例,创建设备,同时为设备创建可支持图形渲染和显示的队列, 最后,通过实例和设备,创建Surface和 SwapChain。而不同之处在于,Vulkan没有一句创建Context的语句,而是需要用户自己配置。
在下文中,我会学习研究SwapChain以及配置Vulkan上下文。