vulkan_vulkan下载 - CSDN
vulkan 订阅
Vulkan是一个跨平台的2D和3D绘图应用程序接口(API),最早由科纳斯组织(Khronos Group) [1]  在2015年游戏开发者大会(GDC)上发表。科纳斯最先把VulkanAPI称为“次世代OpenGL行动”(next generation OpenGL initiative)或“glNext”, [2]  但在正式宣布Vulkan之后这些名字就没有再使用了。就像OpenGL,Vulkan针对实时3D程序(如电子游戏)设计,Vulkan并计划提供高性能和低CPU管理负担(overhead),这也是Direct3D12和AMD的Mantle的目标。Vulkan兼容Mantle的一个分支,并使用了Mantle的一些组件。 [2-3] 展开全文
Vulkan是一个跨平台的2D和3D绘图应用程序接口(API),最早由科纳斯组织(Khronos Group) [1]  在2015年游戏开发者大会(GDC)上发表。科纳斯最先把VulkanAPI称为“次世代OpenGL行动”(next generation OpenGL initiative)或“glNext”, [2]  但在正式宣布Vulkan之后这些名字就没有再使用了。就像OpenGL,Vulkan针对实时3D程序(如电子游戏)设计,Vulkan并计划提供高性能和低CPU管理负担(overhead),这也是Direct3D12和AMD的Mantle的目标。Vulkan兼容Mantle的一个分支,并使用了Mantle的一些组件。 [2-3]
信息
外文名
Vulkan
开发商
Khronos Group [1]
中文名
Vulkan
Vulkan特性
同 OpenGL® 一样,Vulkan™ 也由 Khronos 集团开发。它是 AMD Mantle 的后续版本,继承了前者强大的低开销架构,使软件开发人员能够全面获取 Radeon™ GPU 与多核 CPU 的性能、效率和功能。 [4]  相对于 OpenGL,Vulkan™ 大幅降低了 CPU 在提供重要特性、性能 和影像质量时的“API 开销” [2]  (CPU 在分析游戏的硬件需求时所执行的后台工作),而且可以使用通常通过 OpenGL 无法访问的 GPU 硬件特性。 [4]  独特的跨 OS 支持Vulkan™ 能够支持深入硬件底层的控制,为 Windows® 7、Windows® 8.1、Windows® 10 和 Linux® [4]  带来更快的性能和更高的影像质量。Vulkan™ API 还提供超高的 OS 兼容性、渲染特性和硬件效率。 [4]  自动兼容 GCN 架构只有基于GCN架构 的Radeon™ 显卡拥有强大的“异步计算”功能,使显卡得以并行处理3D几何图形与计算工作量。例如,当游戏需要同时计算复杂照明与渲染人物时,这种功能就找到了用武之地。这些任务并不需要在Radeon™ 显卡上串行运行,因此可以节约时间、提升整体帧速率。设计Vulkan应用的游戏开发者可以在所有近期版本的Windows和Linux系统中利用这种独特硬件特性。 [4] 
收起全文
精华内容
参与话题
  • Vulkan教程(官方教程翻译版)

    万次阅读 2018-09-07 17:43:17
    欢迎来到Vulkan示例教程 本教程以章节的方式一步一步指导你创建一个简单的Vulkan程序。 每个教程章节都对应一个示例程序,可以阅读这个示例程序,并可在实际编码中进行测试。 教程目录 介绍 实例化 枚举所有...

    翻译记录:

    • 2018.9.7更新到“绘制立方体”章节
    • 只剩下“Vulkan1.1版本变化”未翻译

    欢迎来到Vulkan示例教程

    本教程以章节的方式一步一步指导你创建一个简单的Vulkan程序。
    每个教程章节都对应一个示例程序,可以阅读这个示例程序,并可在实际编码中进行测试。

    教程目录

    目标

    本教程的最后一节会有一个显示如下图案的程序:

    这里写图片描述

    展开全文
  • Vulkan资源包

    2020-07-19 23:32:57
    vulkan资料大礼包,包括1-Vulkan-Tutorial_English.pdf Vulkan® 1.1.88 - A Specification (with all registered Vulkan extensions).pdf Vulkan® 1.1.88 - A Specification.pdf,学习vulkan不二资料
  • vulkan中文教程

    2020-07-30 23:31:48
    vulkan在国内几乎没有什么资料可以参考,本着分享的想法,为广大想要学习vulkan的道友们提供一点帮助,拿走不谢 声明,这不是本人自己翻译的,也是从别人那里下载的
  • Vulkan入门流程

    千次阅读 2019-03-19 22:13:29
    Vulkan是Khronos Group(OpenGL标准的维护组织)开发的一个新API,它提供了对现代显卡的一个更好的抽象,与OpenGL和Direct3D等现有api相比,Vulkan可以更详细的向显卡描述你的应用程序打算做什么,从而可以获得更好的...

    Vulkan是Khronos Group(OpenGL标准的维护组织)开发的一个新API,它提供了对现代显卡的一个更好的抽象,与OpenGL和Direct3D等现有api相比,Vulkan可以更详细的向显卡描述你的应用程序打算做什么,从而可以获得更好的性能和更小的驱动开销。Vulkan的设计理念与Direct3D 12和Metal基本类似,但Vulkan作为OpenGL的替代者,它设计之初就是为了跨平台实现的,可以同时在Windows、Linux和Android开发。甚至在Mac OS系统上,Khronos也提供了Vulkan的SDK,虽然这个SDK底层其实是使用MoltenVK实现的。

    MoltenVK实际上是一个将Vulkan API映射到Metal API的一个框架,Vulkan这里只是相当于一个抽象层。
    然而,为了得到一个更好的性能,Vulkan引入了一个非常冗余的API。相比于OpenGL驱动帮我们做了大量的工作,Vulkan与图像api相关的每一个细节,都需要从头设置,包括初始帧缓冲区的创建与缓冲、纹理内存的管理等等。因此,哪怕只画一个三角形,我们都要写数倍于OpenGL的代码。
    而Google在Android 7.0后提供了对Vulkan的支持,并且提供了一系列工具链与Validation Layers(后面会进行说明)。在Android Studio中,只要将Shader代码放在src/main/shaders文件夹下面,项目编译时会自动被编译成.spv字节码,可以作为assets使用。
    由于Vulkan的使用非常冗长,这篇文章将主要介绍Vulkan API的一般使用模式以及画一个三角形所需要的基本元素,后续将对每一个章节进行详细介绍,并最终绘制出一个三角形。

    详细教程 Vulkan画三角形的各个步骤

    展开全文
  • Vulkan编程指南.pdf

    2020-07-30 23:31:29
    Vulkan编程指南高清版,内容可复制,非常实用.
  • Vulkan简介

    2020-03-21 17:21:12
    最近学习了一下Vulkan,通过这篇文章来对我所学的知识进行一个总结。 前言 Vulkan可以认为是Opengl版本的重写,它提供高性能和低CPU管理负担,天然支持多线程,能较好发挥多核CPU的性能,是一个能和DX12相提并论的...

    最近学习了一下Vulkan,通过这篇文章来对我所学的知识进行一个总结。

    在这里插入图片描述

    前言

    Vulkan可以认为是Opengl版本的重写,它提供高性能和低CPU负担,天然支持多线程,能较好发挥多核CPU的性能,是一个能和DX12相提并论的东西。同时Vulkan几乎支持所有平台,跨平台API具有非常好的优势。Vulkan把驱动层做的很薄,把很多权限交给开发者,使开发者能更精确地控制渲染流程和资源管理。把很多功能做成一种扩展的形式,当你需要的时候才把它加进来。有的扩展还可以在开发的时候加入来提高开发效率,发布的时候将其去掉提高运行效率。也可以把传统的Opengl当做C#、Vulkan当C++来理解吧。

    Vulkan概念介绍

    VkPhysicalDevice & VkDevice

    这是Vulkan独有的,VkPhysicalDevice是物理设备或者说显卡,程序启动前要找到一块支持Vulkan的显卡,比如是否支持GeometryShader,是否支持光追扩展等。VkDevice就是从PhysicalDevice中创建得到的一个虚拟Device,在代码中都是通过这的虚拟Device来控制显卡的。在这个虚拟的Device中也需要检查是否支持需要的Queue、数据格式等。

    Pipeline:

    传统API:
    1、可以认为只有一条全局管线,驱动会记录状态,比如开启了模板测试,后面的所有DrawCall就会开启模板测试。
    Vulkan:
    1、有一个叫VkPipeline的类型,每一个DC都可以对应一条管线,管线的设置项非常多,比如顶点数据输入信息、数据格式、Viewport、Shader、MultiSample、属于哪个RenderPass的第几个Subpass等等。
    2、管线创建后基本是不允许修改的,只有少量的信息可以修改,要修改这些信息也要在创建管线的时候提前声明。
    3、管线的创建可以用pipelineCache加速创建速度,缓存信息可以从文件读取。
    下面是创建管线的一些基本设置。
    		VkGraphicsPipelineCreateInfo pipelineInfo = {};
    		pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
    		pipelineInfo.stageCount = 2;
    		pipelineInfo.pStages = shaderStage;
    		pipelineInfo.pVertexInputState = &vertexInputInfo;
    		pipelineInfo.pInputAssemblyState = &inputAssembly;
    		pipelineInfo.pViewportState = &viewportState;
    		pipelineInfo.pRasterizationState = &rasterizer;
    		pipelineInfo.pMultisampleState = &multisampling;
    		pipelineInfo.pDepthStencilState = nullptr;
    		pipelineInfo.pColorBlendState = &colorBlending;
    		pipelineInfo.pDynamicState = nullptr;
    		pipelineInfo.layout = pipelineLayout;
    		pipelineInfo.renderPass = renderPass;
    		pipelineInfo.subpass = 0;
    		pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
    		pipelineInfo.basePipelineIndex = -1;
    
    		if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
    			throw std::runtime_error("failed to create graphics pipeline!");
    		}
    
    

    总结:每个DC可以有一个独立管线,减少DC之间的耦合,有利于并行。管线可以提前创建,运行时不能修改,如果要切换一些渲染状态, 可以直接绑定另一个Pipeline,状态切换消耗低,保证高效。不像传统API,不同DC间要大量设置状态,驱动要做检查。

    Buffer

    Buffers in Vulkan are regions of memory used for storing arbitrary data that can be read by the graphics card。Vulkan中的Buffer是一块用来存储显卡可以读取的的任意数据存储区。Buffer的类型有几种,比如有Device Local 的,在创建这种Buffer要是使用一个叫VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT的参数。这种存储区域是显卡本地的,而且CPU不能访问的。而CPU能访问的存储区域,显卡访问性能比上面的略差一点。这些存储区域主要是显存和主存的一小部分区域。当显存用完可以把显存的数据交换到主存。这些存储区域是开放的,像一个数组,由开发者自己管理。

    	//with cpu assessable memory
    	void createVertexBuffers() {
    		VkDeviceSize bufferSize = sizeof(vertices[0])*vertices.size();
    		//VK_MEMORY_PROPERTY_HOST_COHERENT_BIT使用这个bit可以保证显卡在使用数据之前,数据已经复制到vertexbfferMemory了
    		//不会因为cache导致数据还没过去(也可以flush一下)
    		createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
    			VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory);
    	
    		void* data;
    		vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data);
    		memcpy(data, vertices.data(), (size_t)bufferSize);
    		vkUnmapMemory(device, vertexBufferMemory);
    	}
    

    这是创建VertexBuffer的一点代码,这里使用的是HOST_VISIBLE的存储类型,就是CPU能访问的,这种的使用比较简单,直接map然后memcpy复制就行。

    Image

    Image在Vulkan中也是对应一种类型(VkImage),Image的用途非常多,具体如下所示。创建的时候可以指定Iamge大小,数据类型、数据布局、使用的颜色空间等。
    在这里插入图片描述
    其中值得一提的是数据布局,数据的不同布局会影响效率,比如你是一行一行读取Image,但是存储却是一列一列的,会降低命中率。下面是一些ImageLayout的选项。

    typedef enum VkImageLayout {
        VK_IMAGE_LAYOUT_UNDEFINED = 0,
        VK_IMAGE_LAYOUT_GENERAL = 1,
        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL = 2,
        VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL = 3,
        VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL = 4,
        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL = 5,
        VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL = 6,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL = 7,
        VK_IMAGE_LAYOUT_PREINITIALIZED = 8,
        VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL = 1000117000,
        VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL = 1000117001,
        VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL = 1000241000,
        VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL = 1000241001,
        VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL = 1000241002,
        VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL = 1000241003,
        VK_IMAGE_LAYOUT_PRESENT_SRC_KHR = 1000001002,
        VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR = 1000111000,
        VK_IMAGE_LAYOUT_SHADING_RATE_OPTIMAL_NV = 1000164003,
        VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT = 1000218000,
        VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL,
        VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL,
        VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
        VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
        VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL_KHR = VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL,
        VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL_KHR = VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL,
        VK_IMAGE_LAYOUT_BEGIN_RANGE = VK_IMAGE_LAYOUT_UNDEFINED,
        VK_IMAGE_LAYOUT_END_RANGE = VK_IMAGE_LAYOUT_PREINITIALIZED,
        VK_IMAGE_LAYOUT_RANGE_SIZE = (VK_IMAGE_LAYOUT_PREINITIALIZED - VK_IMAGE_LAYOUT_UNDEFINED + 1),
        VK_IMAGE_LAYOUT_MAX_ENUM = 0x7FFFFFFF
    } VkImageLayout;
    

    Descriptor Sets & Layouts

    Vulkan强调资源的复用,资源可以通过DescriptorSet绑定到Shader使用,一个Image绑定到上一个DC作为ColorAttachment,再绑定到当前DC做普通纹理使用。绑定的方式可以通过Multi-Set或者Multi-Binding。下面是使用Multi-Binding来绑定数据的Shader。这些资源布局和绑定和Pipeline类似都是预先创建好的,驱动不做验证,运行时Shader通过descriptorSet的信息寻找资源。

    layout (binding = 0, rgba32f) uniform readonly image2D samplerPositionDepth;
    layout (binding = 1, rgba8) uniform readonly image2D samplerNormal;
    layout (binding = 2, rgba32f) uniform readonly image2D ssaoNoise;
    layout (binding = 3, rgba8) uniform image2D resultImage;
    

    CommandBuffer

    传统API的渲染方式:
    1、bind&draw的模式,绑定一些东西然后Draw,再绑定一些东西再Draw,是同步的,非常适合串行。
    2、命令在驱动层记录和自动提交,开发者不知道命令什么时候提交到显卡计算。 如下图所示,驱动会根据它的策略判断是否应该提交命令,就有点像TCP,你的数据比协议头还小,是不会立刻发出去的。这样就会出现图中的问题,如果第一次Submit的任务在第二次Submit前就已经完成了,则有一段时间GPU不工作,就会产生气泡,GPU利用率不够高。

    在这里插入图片描述

    Vulkan:(multi-thread rendering, control submission)
    1、命令在CommandBuffer中记录。
    2、命令的记录和提交是分离的,在使用Vulkan的时候记录指令可以在MainLoop之前就记录好,在主循环中直接Submit即可。
    3、可以自定义提交的时机。在合适的时机提交命令可以提高CPU和GPU的利用率,减少一帧的计算时间。
    4、可以创建多个CommandBuffer,多线程记录命令。如下所示,左边表示开两个线程来记录命令,充分发挥多核CPU的性能,提交完后再处理Physics和AI,这时候GPU和CPU一起工作,在EndOfFrame的时候所有渲染工作已经完成了。

    在这里插入图片描述

    为什么多线程渲染那么重要?实际上很多游戏都不止一个线程,如果渲染指令的提交只能单线程就会很拉胯。当使用多线程高效去计算游戏各种对象下一帧的方位后,却只能用单线程渲染所有对象,这时候很自然地就会想到把渲染也多线程一下。

    应用实例:
    比如要为六个灯渲染各一张ShadowMap,在CPU多线程中可以像图中那样安排任务,其中橙色的代表指令提交。
    在这里插入图片描述

    总的来说,使用CommandBuffer可以更精确控制提交时机、提高运行效率。以前DrawCall会很严重限制帧率,十分消耗CPU资源,而现在则有所改观,使用多线程可以提高利用率,缩短计算时间,在高端电脑游戏中可以选择压榨性能,渲染更多物体,在手机上可以降低CPU的负担。

    GPU Queue

    传统API:
    没有Queue,渲染方式如图,按照时间顺序渲染。这样的坏处是每次只能使用显卡的一小部分功能。一般显卡会有Copy Engine(专门做数据的复制传输)、Graphic Engine(管线化用来做渲染)、Compute Engine(只是计算数据)等。图中在Stream textures阶段时只有Copy Engine在工作,而其他不工作。
    在这里插入图片描述
    Vulkan
    有Transfer、Graphic、Compute功能的队列,其中带Compute和Graphic的队列都有Transfer功能,有的队列同时支持Compute和Graphic,而有的只支持Compute。比如我的1660同时支持Compute和Graphic有16条Queue,在编程中是可以开启多条线程向多条队列提交渲染指令的,这些Queue并行计算,Vulkan也提供了一些同步原语可以对并行计算进行同步。其中,提交到同一条Queue的指令是按顺序执行的。使用Queue后,上面的流程可以如下设计,其中最上面的是纯粹离线计算,在Compute Queue算,中间的使用Graphic Queue计算。

    在这里插入图片描述
    总的来说,使用Queue可以充分利用GPU,可以进行异步计算,未来Compute Shader的应用会越来越多,把一些Graphic Queue上的计算工作抽出来到Compute Queue上计算,获得更好的性能表现。资源加载也类似。

    同步原语

    因为Vulkan任务提交和执行是异步的,使用多条Queue的时候计算也是并行的,不同的DC之间难免有依赖关系,需要进行一些同步操作。Vulkan提供了几种不同粒度的同步原语,包括event、fence、barrier、semaphore。Vulkan的一些API可以自动wait和signal指定的同步原语,在资源上可以使用barrier定义依赖关系。比如在DC1中一张Image是一个被写入的,在DC2中是只读的,而DC1和DC2是并行的,就有可能DC2执行,读到空数据,然后DC1再去写入,或者DC1和DC2同时读写之类的。这时候可以在DC2的RenderPass中对这个image定义一个Barrier。

    			VkImageMemoryBarrier imageMemoryBarrier = {};
    			imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    			// We won't be changing the layout of the image
    			imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
    			imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
    			imageMemoryBarrier.image = textureComputeTarget.image;
    			imageMemoryBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
    			imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; //注意这里
    			imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;  //注意这里
    			vkCmdPipelineBarrier(
    				drawCmdBuffers[i],
    				VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
    				VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
    				VK_FLAGS_NONE,
    				0, nullptr,
    				0, nullptr,
    				1, &imageMemoryBarrier);
    

    注意中间两行,大概就是说我要等到这个Image从可写变成可读再执行后面的操作。

    总结

    Vulkan把驱动层的一些任务交给开发者,使开发者能够以较低的消耗更深入地控制硬件,按照自己的需求来使用硬件,可以自定义内存管理,较好支持多线程。使用Vulkan的游戏应该尽量使用多线程,把一些复杂计算从Graphic queue转到Compute queue计算。由于本人能力有限,有错误的地方可以在评论中帮我指出。

    Reference:

    https://vulkan-tutorial.com/Introduction
    https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#introduction
    https://zhuanlan.zhihu.com/p/20712354
    https://zhuanlan.zhihu.com/p/73016473
    https://www.youtube.com/watch?v=H1L4iLIU9xU
    http://on-demand.gputechconf.com/gtc/2016/video/S6817.html
    https://github.com/SaschaWillems/Vulkan

    展开全文
  • 在本章,你将学到: Vulkan以及它背后的基本原理; 如何创建一个最简单的Vulkan应用程序;...在本章的最后,我们会完成一个简单的Vulkan应用程序,这个程序可以初始化Vulkan系统,查找可用的Vulkan设备并显示其...

    在本章,你将学到:

    • Vulkan以及它背后的基本原理;
    • 如何创建一个最简单的Vulkan应用程序;
    • 在本书其余部分将使用到的术语和概念。

    本章将介绍并解释Vulkan是什么。我们会介绍API背后的基本概念,包括初始化、对象生命周期、Vulkan实例以及逻辑和物理设备。在本章的最后,我们会完成一个简单的Vulkan应用程序,这个程序可以初始化Vulkan系统,查找可用的Vulkan设备并显示其属性和功能,最后彻底地关闭程序。

    1.1 引言

    Vulkan是一个用于图形和计算设备的编程接口。Vulkan设备通常由一个处理器和一定数量的固定功能硬件模块组成,用于加速图形和计算操作。通常,设备中的处理器是高度线程化的,所以在极大程度上Vulkan里的计算模型是基于并行计算的。Vulkan还可以访问运行应用程序的主处理器上的共享或非共享内存。Vulkan也会给开发人员提供这个内存。

    Vulkan是个显式的API,也就是说,几乎所有的事情你都需要亲自负责。驱动程序是一个软件,用于接收API调用传递过来的指令和数据,并将它们进行转换,使得硬件可以理解。在老的API(例如OpenGL)里,驱动程序会跟踪大量对象的状态,自动管理内存和同步,以及在程序运行时检查错误。这对开发人员非常友好,但是在应用程序经过调试并且正确运行时,会消耗宝贵的CPU性能。Vulkan解决这个问题的方式是,将状态跟踪、同步和内存管理交给了应用程序开发人员,同时将正确性检查交给各个层进行代理,而要想使用这些层必须手动启用。这些层在正常情况下不会在应用程序里执行。

    由于这些原因,Vulkan难以使用,并且在一定程度上很不稳定。你需要做大量的工作来保证Vulkan运行正常,并且API的错误使用经常会导致图形错乱甚至程序崩溃,而在传统的图形API里你通常会提前收到用于帮助解决问题的错误消息。以此为代价,Vulkan提供了对设备的更多控制、清晰的线程模型以及比传统API高得多的性能。

    另外,Vulkan不仅仅被设计成图形API,它还用作异构设备,例如图形处理单元(Graphics Processing Unit,GPU)、数字信号处理器(Digital Signal Processor,DSP)和固定功能硬件。功能可以粗略地划分为几类。Vulkan的当前版本定义了传输类别——用于复制数据;计算类别——用于运行着色器进行计算工作;图形类别——包括光栅化、图元装配、混合、深度和模板测试,以及图形程序员所熟悉的其他功能。

    Vulkan设备对每个分类的支持都是可选的,甚至可以根本不支持图形。因此,将图像显示到适配器设备上的API(这个过程叫作展示)不但是可选择的功能,而且是扩展功能,而不是核心API。

    1.2 实例、设备和队列

    Vulkan包含了一个层级化的功能结构,从顶层开始是实例,实例聚集了所有支持Vulkan的设备。每个设备提供了一个或者多个队列,这些队列执行应用程序请求的工作。

    Vulkan实例是一个软件概念,在逻辑上将应用程序的状态与其他应用程序或者运行在应用程序环境里的库分开。系统里的物理设备表示为实例的成员变量,每个都有一定的功能,包括一组可用的队列。

    物理设备通常表示一个单独的硬件或者互相连接的一组硬件。在任何系统里,都有一些数量固定的物理设备,除非这个系统支持重新配置,例如热插拔。由实例创建的逻辑设备是一个与物理设备相关的软件概念,表示与某个特定物理设备相关的预定资源,其中包括了物理设备上可用队列的一个子集。可以通过创建多个逻辑设备来表示一个物理设备,应用程序花大部分时间与逻辑设备交互。

    图1.1展示了这个层级关系。图1.1中,应用程序创建了两个Vulkan实例。系统里的3个物理设备能够被这两个实例使用。经过枚举,应用程序在第一个物理设备上创建了一个逻辑设备,在第二个物理设备创建了两个逻辑设备,在第三个物理设备上创建了一个逻辑设备。每个逻辑设备启用了对应物理设备队列的不同子集。在实际开发中,大多数Vulkan应用程序不会这么复杂,而会针对系统里的某个物理设备只创建一个逻辑设备,并且使用一个实例。图1.1仅仅用来展示Vulkan的复杂性。

    0101{60%}

    图1.1 Vulkan里关于实例、设备和队列的层级关系

    后面的小节将讨论如何创建Vulkan实例,如何查询系统里的物理设备,并将一个逻辑设备关联到某个物理设备上,最后获取设备提供的队列句柄。

    1.2.1 Vulkan实例

    Vulkan可以被看作应用程序的子系统。一旦应用程序连接了Vulkan库并初始化,Vulkan就会追踪一些状态。因为Vulkan并不向应用程序引入任何全局状态,所以所有追踪的状态必须存储在你提供的一个对象里。这就是实例对象,由VkInstance对象来表示。为了构建这个对象,我们会调用第一个Vulkan函数vkCreateInstance(),其原型如下。

    
     
    1. VkResult vkCreateInstance (
    2. const VkInstanceCreateInfo* pCreateInfo,
    3. const VkAllocationCallbacks* pAllocator,
    4. VkInstance* pInstance);

    该声明是个典型的Vulkan函数:把多个参数传入Vulkan,函数通常接收结构体的指针。这里,pCreateInfo是指向结构体VkInstanceCreateInfo的实例的指针。这个结构体包含了用来描述新的Vulkan实例的参数,其定义如下。

    
     
    1. typedef struct VkInstanceCreateInfo {
    2. VkStructureType sType;
    3. const void* pNext;
    4. VkInstanceCreateFlags flags;
    5. const VkApplicationInfo* pApplicationInfo;
    6. uint32_t enabledLayerCount;
    7. const char* const* ppEnabledLayerNames;
    8. uint32_t enabledExtensionCount;
    9. const char* const* ppEnabledExtensionNames;
    10. } VkInstanceCreateInfo;

    几乎每一个用于向API传递参数的Vulkan结构体的第一个成员都是字段sType,该字段告诉Vulkan这个结构体的类型是什么。核心API以及任何扩展里的每个结构体都有一个指定的结构体标签。通过检查这个标签,Vulkan工具、层和驱动可以确定结构体的类型,用于验证以及在扩展里使用。另外,字段pNext允许将一个相连的结构体链表传入函数。这样在一个扩展中,允许对参数集进行扩展,而不用将整个核心结构体替换掉。因为这里使用了核心的实例创建结构体,将字段sType设置为VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,并且将pNext设置为nullptr。

    字段flags留待将来使用,应该设置为0。下一个字段pApplicationInfo是个可选的指针,指向另一个描述应用程序的结构体。可以将它设置为nullptr,但是推荐填充为有用的信息。pApplicationInfo指向结构体VkApplicationInfo的一个实例,其定义如下。

    
     
    1. typedef struct VkApplicationInfo {
    2. VkStructureType sType;
    3. const void* pNext;
    4. const char* pApplicationName;
    5. uint32_t applicationVersion;
    6. const char* pEngineName;
    7. uint32_t engineVersion;
    8. uint32_t apiVersion;
    9. } VkApplicationInfo;

    我们再一次看到了字段sType和pNext。SType 应该设置为VK_STRUCTURE_TYPE_APPLICATION_INFO,并且可以将pNext设置为nullptr。pApplicationName是个指针,指向以nul为结尾的字符串[1],这个字符串用于包含应用程序的名字。applicationVersion是应用程序的版本号。这样就允许工具和驱动决定如何对待应用程序,而不用猜测[2]哪个应用程序正在运行。同样,pEngineName与engineVersion也分别包含了引擎或者中间件(应用程序基于此构建)的名字和版本号。

    最后,apiVersion包含了应用程序期望运行的Vulkan API的版本号。这个应该设置为你期望应用程序运行所需的Vulkan的绝对最小版本号——并不是你安装的头文件中的版本号。这样允许更多设备和平台运行应用程序,即使并不能更新它们的Vulkan实现。

    回到结构体VkInstanceCreateInfo,接下来是字段enabledLayerCount和ppEnabledLayerNames。这两个分别是你想激活的实例层的个数以及名字。层用于拦截Vulkan的API调用,提供日志、性能分析、调试或者其他特性。如果不需要层,只需要将enabledLayerCount设置为0,将ppEnabledLayerNames设置为nullptr。同样,enabledExtensionCount是你想激活的扩展的个数[3],ppEnabledExtensionNames是名字列表。如果我们不想使用任何的扩展,同样可以将这些字段分别设置为0和nullptr。

    最后,回到函数vkCreateInstance(),参数pAllocator是个指向主机内存分配器的指针,该分配器由应用程序提供,用于管理Vulkan系统使用的主机内存。将这个参数设置为nullptr会导致Vulkan系统使用它内置的分配器。在这里先这样设置。应用程序托管的主机内存将会在第2章中讲解。

    如果函数vkCreateInstance()成功,会返回VK_SUCCESS,并且会将新实例的句柄放置在变量pInstance里。句柄是用于引用对象的值。Vulkan句柄总是64位宽,与主机系统的位数无关。一旦有了Vulkan实例的句柄,就可以用它调用实例函数了。

    1.2.2 Vulkan物理设备

    一旦有了实例,就可以查找系统里安装的与Vulkan兼容的设备。Vulkan有两种设备:物理设备和逻辑设备。物理设备通常是系统的一部分——显卡、加速器、数字信号处理器或者其他的组件。系统里有固定数量的物理设备,每个物理设备都有自己的一组固定的功能。

    逻辑设备是物理设备的软件抽象,以应用程序指定的方式配置。逻辑设备是应用程序花费大部分时间处理的对象。但是在创建逻辑设备之前,必须查找连接的物理设备。需要调用函数vkEnumeratePhysicalDevices(),其原型如下。

    
     
    1. VkResult vkEnumeratePhysicalDevices (
    2. VkInstance instance,
    3. uint32_t* pPhysicalDeviceCount,
    4. VkPhysicalDevice* pPhysicalDevices);

    函数vkEnumeratePhysicalDevices()的第一个参数instance是之前创建的实例。下一个参数pPhysicalDeviceCount是一个指向无符号整型变量的指针,同时作为输入和输出。作为输出,Vulkan将系统里的物理设备数量写入该指针变量。作为输入,它会初始化为应用程序能够处理的设备的最大数量。参数pPhysicalDevices是个指向VkPhysicalDevice句柄数组的指针。

    如果你只想知道系统里有多少个设备,将pPhysicalDevices设置为nullptr,这样Vulkan将忽视pPhysicalDeviceCount的初始值,将它重写为支持的设备的数量。可以调用vkEnumerate PhysicalDevices()两次,动态调整VkPhysicalDevice数组的大小:第一次仅将pPhysicalDevices设置为nullptr(尽管pPhysicalDeviceCount仍然必须是个有效的指针),第二次将pPhysicalDevices设置为一个数组(数组的大小已经调整为第一次调用返回的物理设备数量)。

    如果调用成功,函数vkEnumeratePhysicalDevices()返回VK_SUCCESS,并且将识别出来的物理设备数量存储进pPhysicalDeviceCount中,还将它们的句柄存储进pPhysicalDevices中。代码清单1.1展示了一个例子:构造结构体VkApplicationInfo和VkInstanceCreateInfo,创建Vulkan实例,查询支持设备的数量,并最终查询物理设备的句柄。这是例子框架里面的vkapp::init的简化版本。

    代码清单1.1 创建Vulkan实例

    
     
    1. VkResult vkapp::init()
    2. {
    3. VkResult result = VK_SUCCESS;
    4. VkApplicationInfo appInfo = { };
    5. VkInstanceCreateInfo instanceCreateInfo = { };
    6.  
    7. // 通用的应用程序信息结构体
    8. appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    9. appInfo.pApplicationName = "Application";
    10. appInfo.applicationVersion = 1;
    11. appInfo.apiVersion = VK_MAKE_VERSION(1, 0, 0);
    12.  
    13. // 创建实例
    14. instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    15. instanceCreateInfo.pApplicationInfo = &appInfo;
    16.  
    17. result = vkCreateInstance(&instanceCreateInfo, nullptr, &m_instance);
    18.  
    19. if (result == VK_SUCCESS)
    20. {
    21. // 首先判断系统里有多少个设备
    22. uint32_t physicalDeviceCount = 0;
    23. vkEnumeratePhysicalDevices(m_instance, &physicalDeviceCount, nullptr);
    24.  
    25. if (result == VK_SUCCESS)
    26. {
    27. // 调整设备数组的大小,并获取物理设备的句柄
    28. m_physicalDevices.resize(physicalDeviceCount);
    29. vkEnumeratePhysicalDevices(m_instance,
    30. &physicalDeviceCount,
    31. &m_physicalDevices[0]);
    32. }
    33. }
    34. return result;
    35. }

    物理设备句柄用于查询设备的功能,并最终用于创建逻辑设备。第一次执行的查询是vkGet PhysicalDeviceProperties(),该函数会填充描述物理设备所有属性的结构体。其原型如下。

    
     
    1. void vkGetPhysicalDeviceProperties (
    2. VkPhysicalDevice physicalDevice,
    3. VkPhysicalDeviceProperties* pProperties);

    当调用vkGetPhysicalDeviceProperties()时,向参数physicalDevice传递vkEnumeratePhysical Devices()返回的句柄之一,向参数pProperties传递一个指向结构体VkPhysicalDeviceProperties实例的指针。VkPhysicalDeviceProperties是个大结构体,包含了大量描述物理设备属性的字段。其定义如下。

    
     
    1. typedef struct VkPhysicalDeviceProperties {
    2. uint32_t apiVersion;
    3. uint32_t driverVersion;
    4. uint32_t vendorID;
    5. uint32_t deviceID;
    6. VkPhysicalDeviceType deviceType;
    7. char deviceName
    8. [VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
    9. uint8_t pipelineCacheUUID[VK_UUID_SIZE];
    10. VkPhysicalDeviceLimits limits;
    11. VkPhysicalDeviceSparseProperties sparseProperties;
    12. } VkPhysicalDeviceProperties;

    字段apiVersion包含了设备支持的Vulkan的最高版本,字段driverVersion包含了用于控制设备的驱动的版本号。这是硬件生产商特定的,所以对比不同的生产商的驱动版本没有任何意义。字段vendorID与deviceID标识了生产商和设备,并且通常是PCI生产商和设备标识符[4]

    字段deviceName包含了可读字符串来命名设备。字段pipelineCacheUUID用于管线缓存,这会在第6章中讲到。

    除了刚刚列出的属性之外,结构体VkPhysicalDeviceProperties内嵌了VkPhysicalDeviceLimits和VkPhysicalDeviceSparseProperties,包含了物理设备的最大和最小限制,以及和稀疏纹理有关的属性。这两个结构体里有大量信息,这些字段会在讨论相关特性时介绍,在此不再详述。

    除了核心特性(有些有更高的限制或约束)之外,Vulkan还可能有一些物理设备支持的可选特性。如果设备宣传支持某个特性,它必须激活(非常像扩展)。但是一旦激活,这个特性就变成了API的“一等公民”,就像任何核心特性一样。为了判定物理设备支持哪些特性,调用vkGetPhysicalDeviceFeatures()。其原型如下。

    
     
    1. void vkGetPhysicalDeviceFeatures (
    2. VkPhysicalDevice physicalDevice,
    3. VkPhysicalDeviceFeatures* pFeatures);

    结构体vkPhysicalDeviceFeatures也非常大,并且Vulkan支持的每一个可选特性都有一个布尔类型的字段。字段太多,就不在此详细罗列了,但是本章最后展示的例子会读取特性集并输出其内容。

    1.2.3 物理设备内存

    在许多情况下,Vulkan设备要么是一个独立于主机处理器之外的一块物理硬件,要么工作方式非常不同,以独有的方式访问内存。Vulkan里的设备内存是指,设备能够访问到并且用作纹理和其他数据的后备存储器的内存。内存可以分为几类,每一类都有一套属性,例如缓存标志位以及主机和设备之间的一致性行为。每种类型的内存都由设备的某个堆(可能会有多个堆)进行支持。

    为了查询堆配置以及设备支持的内存类型,需要调用以下代码。

    
     
    1. void vkGetPhysicalDeviceMemoryProperties (
    2. VkPhysicalDevice physicalDevice,
    3. VkPhysicalDeviceMemoryProperties* pMemoryProperties);

    查询到的内存组织信息会存储进结构体 VkPhysicalDeviceMemoryProperties中,地址通过pMemoryProperties传入。结构体VkPhysicalDeviceMemoryProperties包含了关于设备的堆以及其支持的内存类型的属性。该结构体的定义如下。

    
     
    1. typedef struct VkPhysicalDeviceMemoryProperties {
    2. uint32_t memoryTypeCount;
    3. VkMemoryType memoryTypes[VK_MAX_MEMORY_TYPES];
    4. uint32_t memoryHeapCount;
    5. VkMemoryHeap memoryHeaps[VK_MAX_MEMORY_HEAPS];
    6. } VkPhysicalDeviceMemoryProperties;

    内存类型数量包含在字段memoryTypeCount里。可能报告的内存类型的最大数量是VK_MAX_MEMORY_TYPES定义的值,这个宏定义为32。数组memoryTypes包含memoryTypeCount个结构体VkMemoryType对象,每个对象都描述了一种内存类型。VkMemoryType的定义如下。

    
     
    1. typedef struct VkMemoryType {
    2. VkMemoryPropertyFlags propertyFlags;
    3. uint32_t heapIndex;
    4. } VkMemoryType;

    这是个简单的结构体,只包含了一套标志位以及内存类型的堆栈索引。字段flags描述了内存的类型,并由VkMemoryPropertyFlagBits类型的标志位组合而成。标志位的含义如下。

    • VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT意味着内存对于设备来说是本地的(也就是说,物理上是和设备连接的)。如果没有设置这个标志位,可以认为该内存对于主机来说是本地的。
    • VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT意味着以这种方式分配的内存可以被主机映射以及读写。如果没有设置这个标志位,那么内存不能被主机直接访问,只能由设备使用。
    • VK_MEMORY_PROPERTY_HOST_COHERENT_BIT意味着当这种内存同时被主机和设备访问时,这两个客户之间的访问保持一致。如果没有设置这个标志位,设备或者主机不能看到对方执行的写操作,直到显式地刷新缓存。
    • VK_MEMORY_PROPERTY_HOST_CACHED_BIT意味着这种内存里的数据在主机里面进行缓存。对这种内存的读取操作比不设置这个标志位通常要快。然而,设备的访问延迟稍微高一些,尤其当内存也保持一致时。
    • VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT意味着这种内存分配类型不一定立即使用关联的堆的空间,驱动可能延迟分配物理内存,直到内存对象用来支持某个资源。

    每种内存类型都指定了从哪个堆上使用空间,这由结构体VkMemoryType里的字段heapIndex来标识。这个字段是数组memoryHeaps (在调用vkGetPhysicalDeviceMemoryProperties()返回的结构体VkPhysicalDeviceMemoryProperties里面)的索引。数组memoryHeaps里面的每一个元素描述了设备的一个内存堆。结构体的定义如下。

    
     
    1. typedef struct VkMemoryHeap {
    2. VkDeviceSize size;
    3. VkMemoryHeapFlags flags;
    4. } VkMemoryHeap;

    同样,这也是个简单的结构体,包含了堆的大小(单位是字节)以及描述这个堆的标识符。在Vulkan 1.0里,唯一定义的标识符是VK_MEMORY_HEAP_DEVICE_LOCAL_BIT。如果定义了这个标识符,堆对于设备来说就是本地的。这对应于以类似方式命名的用于描述内存类型的标识符。

    1.2.4 设备队列

    Vulkan设备执行提交给队列的工作。每个设备都有一个或者多个队列,每个队列都从属于设备的某个队列族。一个队列族是一组拥有相同功能同时又能并行运行的队列。队列族的数量、每个族的功能以及每个族拥有的队列数量都是物理设备的属性。为了查询设备的队列族,调用vkGetPhysicalDeviceQueueFamilyProperties(),其原型如下。

    
     
    1. void vkGetPhysicalDeviceQueueFamilyProperties (
    2. VkPhysicalDevice physicalDevice,
    3. uint32_t* pQueueFamilyPropertyCount,
    4. VkQueueFamilyProperties* pQueueFamilyProperties);

    vkGetPhysicalDeviceQueueFamilyProperties()的运行方式在一定程度上和vkEnumeratePhysical Devices()类似,需要调用前者两次。第一次,将nullptr传递给pQueueFamilyProperties,并给pQueueFamilyPropertyCount传递一个指针,指向表示设备支持的队列族数量的变量。可以使用该值调整VkQueueFamilyProperties类型的数组的大小。接下来,在第二次调用中,将该数组传入pQueueFamilyProperties,Vulkan将会用队列的属性填充该数组。VkQueueFamilyProperties的定义如下。

    
     
    1. typedef struct VkQueueFamilyProperties {
    2. VkQueueFlags queueFlags;
    3. uint32_t queueCount;
    4. uint32_t timestampValidBits;
    5. VkExtent3D minImageTransferGranularity;
    6. } VkQueueFamilyProperties;

    该结构体里的第一个字段是queueFlags,描述了队列的所有功能。这个字段由VkQueueFlagBits类型的标志位的组合组成,其含义如下。

    • VK_QUEUE_GRAPHICS_BIT 如果设置了,该族里的队列支持图形操作,例如绘制点、线和三角形。
    • VK_QUEUE_COMPUTE_BIT如果设置了,该族里的队列支持计算操作,例如发送计算着色器。
    • VK_QUEUE_TRANSFER_BIT 如果设置了,该族里的队列支持传送操作,例如复制缓冲区和图像内容。
    • VK_QUEUE_SPARSE_BINDING_BIT 如果设置了,该族里的队列支持内存绑定操作,用于更新稀疏资源。

    字段queueCount表示族里的队列数量,该值可能是1。如果设备支持具有相同基础功能的多个队列,该值也可能更高。

    字段timestampValidBits表示当从队列里取时间戳时,多少位有效。如果这个值设置为0,那么队列不支持时间戳。如果不是0,那么会保证最少支持36位。如果设备的结构体VkPhysicalDeviceLimits里的字段timestampComputeAndGraphics是VK_TRUE,那么所有支持VK_QUEUE_GRAPHICS_BIT或者VK_QUEUE_COMPUTE_BIT的队列都能保证支持36位的时间戳。这种情况下,无须检查每一个队列。

    最后,字段minImageTimestampGranularity指定了队列传输图像时支持多少单位(如果有的话)。

    注意,有可能出现这种情形,设备报告多个明显拥有相同属性的队列族。一个族里的所有队列实质上都等同。不同族里的队列可能拥有不同的内部功能,而这些不能在Vulkan API里轻易表达。由于这个原因,具体实现可能选择将类似的队列作为不同族的成员。这对资源如何在队列间共享施加了更多限制,这可能允许具体实现接纳这些不同。

    代码清单1.2展示了如何查询物理设备的内存属性和队列族属性。需要在创建逻辑设备(在下一节会讲到)之前获取队列族的属性。

    代码清单1.2 查询物理设备的属性

    
     
    1. uint32_t queueFamilyPropertyCount;
    2. std::vector<VkQueueFamilyProperties> queueFamilyProperties;
    3. VkPhysicalDeviceMemoryProperties physicalDeviceMemoryProperties;
    4.  
    5. //获取物理设备的内存属性
    6. vkGetPhysicalDeviceMemoryProperties( m_physicalDevices[deviceIndex],
    7. &physicalDeviceMemoryProperties);
    8.  
    9. //首先查询物理设备支持的队列族的数量
    10. vkGetPhysicalDeviceQueueFamilyProperties( m_physicalDevices[0],
    11. &queueFamilyPropertyCount,
    12. nullptr);
    13.  
    14. //为队列属性结构体分配足够的空间
    15. queueFamilyProperties.resize(queueFamilyPropertyCount);
    16.  
    17. //现在查询所有队列族的实际属性
    18. vkGetPhysicalDeviceQueueFamilyProperties( m_physicalDevices[0],
    19. &queueFamilyPropertyCount,
    20. queueFamilyProperties.data());

    1.2.5 创建逻辑设备

    在枚举完系统里的所有物理设备之后,应用程序应该选择一个设备,并且针对该设备创建逻辑设备。逻辑设备代表处于初始化状态的设备。在创建逻辑设备时,可以选择可选特性,开启需要的扩展,等等。创建逻辑设备需要调用vkCreateDevice(),其原型如下。

    
     
    1. VkResult vkCreateDevice (
    2. VkPhysicalDevice physicalDevice,
    3. const VkDeviceCreateInfo* pCreateInfo,
    4. const VkAllocationCallbacks* pAllocator,
    5. VkDevice* pDevice);

    把与逻辑设备相对应的物理设备传给physicalDevice,把关于新的逻辑对象的信息传给结构体VkDeviceCreateInfo的实例pCreateInfo。VkDeviceCreateInfo的定义如下。

    
     
    1. typedef struct VkDeviceCreateInfo {
    2. VkStructureType sType;
    3. const void* pNext;
    4. VkDeviceCreateFlags flags;
    5. uint32_t queueCreateInfoCount;
    6. const VkDeviceQueueCreateInfo* pQueueCreateInfos;
    7. uint32_t enabledLayerCount;
    8. const char* const* ppEnabledLayerNames;
    9. uint32_t enabledExtensionCount;
    10. const char* const* ppEnabledExtensionNames;
    11. const VkPhysicalDeviceFeatures* pEnabledFeatures;
    12. } VkDeviceCreateInfo;

    字段sType应该设置为VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO。通常,除非你希望使用扩展,否则pNext应该设置为nullptr。Vulkan当前版本没有为字段flags定义标志位,所以将这个字段设置为0。

    接下来是队列创建信息。pQueueCreateInfos是指向结构体VkDeviceQueueCreateInfo的数组的指针,每个结构体VkDeviceQueueCreateInfo的对象允许描述一个或者多个队列。数组里的结构体数量由queueCreateInfoCount给定。VkDeviceQueueCreateInfo的定义如下。

    
     
    1. typedef struct VkDeviceQueueCreateInfo {
    2. VkStructureType sType;
    3. const void* pNext;
    4. VkDeviceQueueCreateFlags flags;
    5. uint32_t queueFamilyIndex;
    6. uint32_t queueCount;
    7. const float* pQueuePriorities;
    8. } VkDeviceQueueCreateInfo;

    字段sType设置成VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO。Vulkan当前版本没有为字段flags定义标志位,所以将这个字段设置为0。字段queueFamilyIndex指定了你希望创建的队列所属的族,这是个索引值,与调用vkGetPhysicalDeviceQueueFamilyProperties()返回的队列族的数组对应。为了在这个族里创建队列,将queueCount设置为你希望创建的队列个数。当然,设备在你选择的族中支持的队列数量必须不小于这个值。

    字段pQueuePriorities是个可选的指针,指向浮点数数组,表示提交给每个队列的工作的相对优先级。这些数字是个归一化的数字,取值范围是0.0~1.0。给高优先级的队列会分配更多的处理资源或者更频繁地调度它们。将pQueuePriorities设置为nullptr等同于为所有的队列都指定相同的默认优先级。

    请求的队列按照优先级排序,并且给它们指定了与设备相关的相对优先级。一个队列能够表示的离散的优先级数量是设备特定的参数。这个参数从结构体VkPhysicalDeviceLimits(调用vkGetPhysicalDeviceProperties()的返回值)里的字段discreteQueuePriorities得到。例如,如果设备只支持高低两种优先级的工作负载,这个字段就是2。所有设备最少支持两个离散的优先级。然而,如果设备支持任意的优先级,这个字段的数值就会非常大。不管discreteQueuePriorities的数值有多大,队列的相对优先级仍然是浮点数。

    回到结构体VkDeviceCreateInfo,字段enabledLayerCount、ppEnabledLayerNames、enabled ExtensionCount与ppEnabledExtensionNames用于激活层和扩展。本章后面会讲到这两个主题。现在将enabledLayerCount和enabledExtensionCount设置为0,将ppEnabledLayerNames和ppEnabed ExtensionNames设置为nullptr。

    VkDeviceCreateInfo的最后一个字段是pEnabledFeatures,这是个指向结构体VkPhysical DeviceFeatures的实例的指针,这个实例指明了哪些可选扩展是应用程序希望使用的。如果你不想使用任何可选的特性,只需要将它设置为nullptr。当然,这种方式下Vulkan就会相当受限,大量有意思的功能就不能使用了。

    为了判断某个设备支持哪些可选的特性,像之前讨论的那样调用vkGetPhysicalDeviceFeatures()即可。vkGetPhysicalDeviceFeatures()将设备支持的特性组写入你传入结构体VkPhysicalDeviceFeatures的实例。查询物理设备的特性并将结构体VkPhysicalDeviceFeatures原封不动地传给vkCreateDevice(),你会激活设备支持的所有可选特性,同时也不会请求设备不支持的特性。

    然而,激活所有支持的特性会带来性能影响。对于有些特性,Vulkan具体实现可能需要分配额外的内存,跟踪额外的状态,以不同的方式配置硬件,或者执行其他影响应用程序性能的操作。所以,激活不会使用的特性不是个好主意。你应该查询设备支持的特性,然后激活应用程序需要的特性。

    代码清单1.3展示了一个简单的例子,它查询设备支持的特性并设置应用程序需要的功能列表。此处需要支持曲面细分和几何着色器,如果设备支持,就激活多次间接绘制(multidraw indirect),代码接下来使用第一个队列的单一实例创建设备。

    代码清单1.3 创建一个逻辑设备

    
     
    1. VkResult result;
    2. VkPhysicalDeviceFeatures supportedFeatures;
    3. VkPhysicalDeviceFeatures requiredFeatures = {};
    4.  
    5. vkGetPhysicalDeviceFeatures( m_physicalDevices[0],
    6. &supportedFeatures);
    7.  
    8. requiredFeatures.multiDrawIndirect = supportedFeatures.multiDrawIndirect;
    9. requiredFeatures.tessellationShader = VK_TRUE;
    10. requiredFeatures.geometryShader = VK_TRUE;
    11.  
    12. const VkDeviceQueueCreateInfo deviceQueueCreateInfo =
    13. {
    14. VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, // sType
    15. nullptr, // pNext
    16. 0, // flags
    17. 0, // queueFamilyIndex
    18. 1, // queueCount
    19. nullptr // pQueuePriorities
    20. };
    21.  
    22. const VkDeviceCreateInfo deviceCreateInfo =
    23. {
    24. VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, // sType
    25. nullptr, // pNext
    26.  
    27. 0, // flags
    28. 1, // queueCreateInfoCount
    29. &deviceQueueCreateInfo, // pQueueCreateInfos
    30. 0, // enabledLayerCount
    31. nullptr, // ppEnabledLayerNames
    32. 0, // enabledExtensionCount
    33. nullptr, // ppEnabledExtensionNames
    34. &requiredFeatures // pEnabledFeatures
    35. };
    36.  
    37. result = vkCreateDevice( m_physicalDevices[0],
    38. &deviceCreateInfo,
    39. nullptr,
    40. &m_logicalDevice);

    在代码清单1.3运行成功并创建逻辑设备之后,启用的特性集合就存储在了变量requiredFeatures里。这可以留待以后用,选择使用某个特性的代码可以检查这个特性是否成功激活并优雅地回退。

    1.3 对象类型和函数约定

    事实上,Vulkan里面的所有东西都表示为对象,这些对象靠句柄引用。句柄可以分为两大类:可调度对象和不可调度对象。在极大程度上,这与应用程序无关,仅仅影响API的构造以及系统级别的组件,例如Vulkan加载器和层如何与这些对象互操作。

    可调度对象内部包含了一个调度表,其实就是函数表,在应用程序调用Vulkan时,各种组件据此判断执行哪一部分代码。这些类型的对象通常是重量级的概念,目前有实例(VkInstance)、物理设备(VkPhysicalDevice)、逻辑设备(VkDevice)、命令缓冲区(VkCommandBuffer)和队列(VkQueue)。其他剩余的对象都可以被视为不可调度对象。

    任何Vulkan函数的第一个参数总是个可调度对象,唯一的例外是创建和初始化实例的相关函数。

    1.4 管理内存

    Vulkan提供两种内存:主机内存和设备内存。通常,Vulkan API创建的对象需要一定数量的主机内存。Vulkan实现在这里存储对象的状态并实现这个API所需的数据。资源对象(例如缓冲区和图像)需要一定数量的设备内存。这就是用于存储资源里数据的内存。

    应用程序有可能为Vulkan具体的实现管理主机内存,但是要求应用程序管理设备内存。因此,需要创建设备内存管理子系统。可以查询创建的每个资源,得到用于支持它的内存的数量和类型。应用程序分配正确数量的内存并在使用资源对象前将它附加在这个对象上。

    对于高级API,例如OpenGL,这个功能由驱动程序代替应用程序执行。然而,有的应用程序需要大量的小资源,有的应用程序需要少量非常大的资源。有些应用程序在执行期间创建和销毁资源,而有的在初始化时创建所有的资源,直到程序结束才释放。

    这些情况下的分配策略可能相当不同,不存在万全之策。因为OpenGL驱动无法预测应用程序的行为,所以必须调整分配策略,以适应你的使用方式。另一方面,作为应用程序的开发者,你完全知道应用程序的行为。可以将资源分为长期和短期两组。可以将一起使用的资源放入几个池式分配的内存里。你可以决定应用程序使用哪种分配策略。

    需要特别注意的是,每次动态内存分配都会在系统上产生开销。因此,尽量少分配对象是非常重要的。推荐做法是,设备内存分配器要分配大块的内存。大量小的资源可以放置在少数几个设备内存块里面。关于设备内存分配器的例子会在第2章中讨论,到时会讨论内存分配里的很多细节。

    1.5 Vulkan里的多线程

    对多线程应用程序的支持是Vulkan设计中不可或缺的一部分。Vulkan通常会假设应用程序能够保证两个线程会在同一个时间修改同一个对象,这称为外部同步。在Vulkan里性能至上的部分(例如构建命令缓冲区)中,绝大部分Vulkan命令根本没有提供同步功能。

    为了具体定义各种Vulkan命令中和线程相关的请求,把防止主机同步访问的每一个参数标识为外部同步。在某些情况下,把对象的句柄或者其他的数据内嵌到数据结构体里,包括进数组里,或者通过间接方式传入指令中。那些参数也必须在外部同步。

    这么做的目的是Vulkan实现从来不需要在内部使用互斥量或者其他的同步原语来保护数据结构体。这意味着多线程程序很少由于跨线程引起卡顿或者阻塞。

    除了在跨线程使用共享对象时要求主机同步访问之外,Vulkan还包含了若干高级特性,专门用来允许多线程执行任务时互不阻塞。这些高级特性如下。

    • 主机内存分配可以通过如下方式进行:将一个主机内存分配结构体传入创建对象的函数。通过每个线程使用一个分配器,这个分配器里的数据结构体就不需要保护了。主机内存分配器在第2章中会讲到。
    • 命令缓冲区是从内存池中分配的,并且访问内存池是由外部同步的。如果应用程序对每个线程都使用单独的命令池,那么命令缓冲区就可以从池内分配空间,而不会互相造成阻塞。命令缓冲区和池将在第3章里讲到。
    • 描述符是从描述符池里的集合分配的。描述符代表了运行在设备上的着色器使用的资源。这将在第6章里讲到。如果每个线程都使用单独的池,描述符集就可以从池中分配,而不会彼此阻塞线程。
    • 副命令缓冲区允许大型渲染通道(必须包含在某个命令缓冲区里)里的内容并行产生,然后聚集起来,就像它们是从主命令缓冲区调用的一样。副命令缓冲区会在第13章里讲到。

    当你正在编写一个非常简单的单线程应用程序时,创建用于分配对象的内存池就显得冗余了。然而,随着应用程序使用的线程不断增多,为了提高性能,这些对象就必不可少了。

    在本书剩下的篇幅中,在讲解命令时,和多线程有关的额外需求都会明确指出来。

    1.6 数学概念

    计算机图形学和大多数异构计算应用程序都严重地依赖数学。大多数Vulkan设备都是基于极其强大的计算处理器的。在本书写作时,即使是很普通的移动处理器也提供了每秒几十亿次浮点运算(GFLOPS)的数据处理能力,而高端台式机和工作站的处理器又提供每秒几万亿次浮点运算(TFLOPS)的数据处理能力。因此,有趣的应用程序构建在数学密集型的着色器之上。另外,Vulkan处理管线中的一些固定功能构建在“硬连接”到设备和规范的数学概念之上。

    1.6.1 向量和矩阵

    在图形程序中最基本的“积木”之一就是向量。不管它代表位置、方向、颜色或者其他量,向量在图形学著作中会从头到尾使用到。向量的一种常用形式是齐次向量,这也是个向量,只不过比它所表示的数值多一个维度。这些向量用于存储投影坐标。用任何标量乘以一个齐次向量会产生一个新的向量,代表了相同的投影坐标。要投影一个点向量,需要每一个元素都除以最后一个元素,这样会产生具有xyz和1.0(如果是4个元素的向量)这类形式的向量。

    如果要将一个向量从一个坐标空间变换到另一个,需要将这个向量乘以一个矩阵。因为3D空间里的点由具有4个元素的齐次向量表示,所以变换矩阵就应该是4×4的矩阵。

    3D空间里的点由齐次向量表示,按照惯例,里面的4个元素分别是xyzw。对于一个点来说,成员w一般来说最开始是1.0,与投影变换矩阵相乘以后就改变了。在除以w之后,这个点就经历了所有的变换,完成了投影变换。如果变换矩阵里没有投影变换矩阵,w仍然是1.0,除以1.0对向量来说没有任何影响。如果向量经过透视变换,w就不等于1.0了,但是使用这个透视变换矩阵除以向量以后,w就由变成1.0了。

    同时,3D空间里的方向也由齐次向量来表示,只是w是0.0。如果用正确构造的4×4投影变换矩阵乘以方向向量,w值仍是0.0,这样不会对其他元素产生影响。只需要丢弃额外的元素,你就能像4D齐次3D点向量那样,让3D方向向量经历同样的变换,使它经过同样的旋转、缩放和其他的变换。

    1.6.2 坐标系

    Vulkan通过将端点或者拐角表示成3D空间里的点,来表示基本图元,例如线和三角形。这些基本单位称为顶点。输入Vulkan系统的3D坐标系空间(表示为w元素是1.0的齐次向量)里的顶点坐标,这些顶点坐标是相对于当前对象的原点的数值。这个坐标空间称为对象空间或者模型空间。

    一般情况下,管线里的第一个着色器会将这个顶点变换到观察空间中,也就是相对于观察者的位置。这个变换操作是通过用一个变换矩阵乘以这个顶点的位置向量实现的。这个矩阵通常称为对象-视图变换矩阵,或者模型-视图变换矩阵。

    有时候,需要顶点的绝对坐标,例如查找某个顶点相对于其他对象的距离。这个全局空间称为世界空间,是顶点位置相对于全局原点的位置。

    从观察坐标系出来后,把顶点位置变换到裁剪空间。这是Vulkan中几何处理部分的最后一个空间,也是当把顶点推送进3D应用程序使用的投影空间时,这些顶点变换进的空间。把这个空间称为裁剪空间是因为在这个空间里大多数实现都执行裁剪操作,也就是渲染的可见区域之外的图元部分都会被移除。

    从裁剪空间出来后,顶点位置通过除以w归一化。这样就产生了一个新的坐标空间,叫作标准化设备坐标(NDC)。而这个操作通常称为透视除法。在这个空间里,在xy两个方向上坐标系上的可见部分是−1.0~1.0,z方向上是0.0~1.0。这个区域之外的任何东西都会在透视除法之前被剔除掉。

    最终,顶点的标准化设备坐标由视口变换矩阵进行变换,这个变换矩阵描述了NDC如何映射到正在被渲染的窗口或者图像中。

    1.7 增强Vulkan

    尽管Vulkan的核心API的设计规范相当丰富,但绝不是包罗万象的。有些功能是可选的,而更多的是以层(修改或者增强了现有的行为)和扩展(增加了Vulkan的新功能)的形式使用的。两种增强机制在下面会讲到。

    1.7.1 层

    层是Vulkan中的一种特性,允许修改它的行为。通常,层完全或者部分拦截Vulkan,并增加新的功能,例如日志、追踪、诊断、性能分析等。层可以添加到实例层面,这样,它会影响整个Vulkan实例,也有可能影响由实例创建的每个设备。或者,层可以添加到设备层面中,这样,它仅仅会影响激活这个层的设备。

    为了查询系统里的实例可用的层,调用vkEnumerateInstanceLayerProperties(),其原型如下。

    
     
    1. VkResult vkEnumerateInstanceLayerProperties (
    2. uint32_t* pPropertyCount,
    3. VkLayerProperties* pProperties);

    如果pProperties是nullptr,那么pPropertyCount应该指向一个变量,用于接收Vulkan可用的层的数量。如果pProperties不是nullptr,那么它应该指向结构体VkLayerProperties类型的数组,会向这个数组填充关于系统里注册的层的信息。这种情况下,pPropertyCount指向的变量的初始值是pProperties 指向的数组的长度,并且这个变量会被重写成数组里由指令重写的条目数。

    数组pProperties 里的每个元素都是结构体VkLayerProperties的实例,其定义如下。

    
     
    1. typedef struct VkLayerProperties {
    2. char layerName[VK_MAX_EXTENSION_NAME_SIZE];
    3. uint32_t specVersion;
    4. uint32_t implementationVersion;
    5. char description[VK_MAX_DESCRIPTION_SIZE];
    6. } VkLayerProperties;

    每一个层都有个正式的名字,存储在结构体VkLayerProperties里的成员layerName中。每个层的规范都可能不断改进,进一步明晰,或者添加新功能,层实现的版本号存储在specVersion中。

    随着规范不断改进,具体实现也需要不断改进。具体实现的版本号存储在结构体VkLayer Properties的字段implementationVersion里。这样就允许改进性能,修正Bug,实现更丰富的可选特性集,等等。应用程序作者可能识别出某个层的特定实现,并选择使用它,只要这个实现的版本号超过了某个版本(例如,后一个版本有个已知的严重Bug需要修复)。

    最终,描述层的可读字符串存储在description中。这个字段的唯一目的是输出日志,或者在用户界面展示,仅仅用作提供信息。

    代码清单1.4演示了如何查询Vulkan系统支持的实例层。

    代码清单1.4 查询实例层

    
     
    1. uint32_t numInstanceLayers = 0;
    2. std::vector<VkLayerProperties> instanceLayerProperties;
    3.  
    4. //查询实例层
    5. vkEnumerateInstanceLayerProperties( &numInstanceExtensions,
    6. nullptr);
    7.  
    8. //如果有支持的层,查询它们的属性
    9. if (numInstanceLayers != 0)
    10. {
    11. instanceLayerProperties.resize(numInstanceLayers);
    12. vkEnumerateInstanceLayerProperties( nullptr,
    13. &numInstanceLayers,
    14. instanceLayerProperties.data());
    15. }

    如前所述,不但可以在实例层面注入层,而且可以应用在设备层面应用层。为了检查哪些层是设备可用的,调用vkEnumerateDeviceLayerProperties(),其原型如下。

    
     
    1. VkResult vkEnumerateDeviceLayerProperties (
    2. VkPhysicalDevice physicalDevice,
    3. uint32_t* pPropertyCount,
    4. VkLayerProperties* pProperties);

    因为系统里的每个物理设备可用的层可能不一样,所以每个物理设备可能报告出一套不同的层。需要查询可用层的物理设备通过physicalDevice传入。传入vkEnumerateDeviceLayerProperties()的参数pPropertyCount和pProperties的行为与传入vkEnumerateInstanceLayerProperties()的相似。设备层也由结构体VkLayerProperties的实例描述。

    为了在实例层面激活某个层,需要将其名字包含在结构体VkInstanceCreateInfo的字段ppEnabledLayerNames里,这个结构体用于创建实例。同样,为了在创建对应系统里的某个物理设备的逻辑设备时激活某个层,需要将这个层的名字包含在结构体VkDeviceCreateInfo的成员ppEnabledLayerNames里,这个结构体用于创建设备。

    官方SDK包含若干个层,大部分与调试、参数验证和日志有关。具体内容如下。

    • VK_LAYER_LUNARG_api_dump 将Vulkan的函数调用以及参数输出到控制台。
    • VK_LAYER_LUNARG_core_validation 执行对用于描述符集、管线状态和动态状态的参数和状态的验证;验证SPIR-V模块和图形管线之间的接口;跟踪和验证用于支持对象的GPU内存的使用。
    • VK_LAYER_LUNARG_device_limits 保证作为参数或者数据结构体成员传入Vulkan的数值处于设备支持的特性集范围内。
    • VK_LAYER_LUNARG_image 验证图像使用和支持的格式是否相一致。
    • VK_LAYER_LUNARG_object_tracker 执行Vulkan对象追踪,捕捉内存泄漏、释放后使用的错误以及其他的无效对象使用。
    • VK_LAYER_LUNARG_parameter_validation 确认所有传入Vulkan函数的参数值都有效。
    • VK_LAYER_LUNARG_swapchain 执行WSI(Window System Integration,这将在第5章中讲解)扩展提供的功能的验证。
    • VK_LAYER_GOOGLE_threading 保证Vulkan命令在涉及多线程时有效使用,保证两个线程不会同时访问同一个对象(如果这种操作不允许的话)。
    • VK_LAYER_GOOGLE_unique_objects 确保每个对象都有一个独一无二的句柄,以便于应用程序追踪状态,这样能避免下述情况的发生:某个实现可能删除代表了拥有相同参数的对象的句柄。

    除此之外,把大量不同的层分到单个更大的层中,这个层名叫VK_LAYER_LUNARG_standard_validation,这样就很容易开启了。本书的应用程序框架在调试模式下编译时激活了这个层,而在发布模式下关闭了所有的层。

    1.7.2 扩展

    对于任何跨平台的开放式API(例如Vulkan),扩展都是最根本的特性。这些扩展允许实现者不断试验、创新并且最终推动技术进步。有用的特性最初作为扩展出现,经过实践证明后,最终变成API的未来版本。然而,扩展并不是没有开销的。有些扩展可能要求具体实现跟踪额外的状态,在命令缓冲区构建时进行额外的检查,或者即使扩展没有直接使用,也会带来性能损失。因此,扩展在使用前必须被应用程序显式启用。这意味着,应用程序如果不使用某个扩展就不需要为此付出增加性能开销和提高复杂性的代价。这也意味着,不会出现意外使用某个扩展的特性,这可以改善可移植性。

    扩展可以分为两类:实例扩展和设备扩展。实例扩展用于在某个平台上整体增强Vulkan系统。这种扩展或者通过设备无关的层提供,或者只是每个设备都暴露出来并提升进实例的扩展。设备扩展用于扩展系统里一个或者多个设备的能力,但是这种能力没必要每个设备都具备。

    每个扩展都可以定义新的函数、类型、结构体、枚举,等等。一旦激活,就可以认为这个扩展是API的一部分,对应用程序可用。实例和设备扩展必须在创建Vlukan实例与设备时激活。这导致了“鸡和蛋”的悖论:在初始化Vulkan实例之前我们怎么知道哪些扩展可用?

    Vulkan实例创建之前,只有少数的函数可用,查询支持的实例扩展是其中一个。通过调用函数vkEnumerateInstanceExtensionProperties()来执行这个操作,其原型如下。

    
     
    1. VkResult vkEnumerateInstanceExtensionProperties (
    2. const char* pLayerName,
    3. uint32_t* pPropertyCount,
    4. VkExtensionProperties* pProperties);

    字段pLayerName是可能提供扩展的层的名字,目前将这个字段设置为nullptr。pPropertyCount指向一个变量,用于存储从Vulkan查询到的实例扩展的数量,pProperties是个指向结构体VkExtensionProperties类型的数组的指针,会向这个数组中填充支持的扩展的信息。如果pProperties是nullptr,那么pPropertyCount指向的变量的初始值就会被忽略,并重写为支持的实例扩展的数量。

    如果pProperties不是nullptr,那么数组里的条目数量就是pPropertyCount指向的变量的值,此时,数组里的条目会被填充为支持的扩展的信息。pPropertyCount指向的变量会重写为实际填充到pProperties 的条目的数量。

    为了正确查询所有支持的实例扩展,调用vkEnumerateInstanceExtensionProperties()两次。第一次调用时,将pProperties设置为nullptr,以获取支持的实例扩展的数量。接着正确调整接收扩展属性的数组的大小,并再次调用vkEnumerateInstanceExtensionProperties(),这一次用pProperties传入数组的地址。代码清单1.5展示了如何操作。

    代码清单1.5 查询实例扩展

    
     
    1. uint32_t numInstanceExtensions = 0;
    2. std::vector<VkExtensionProperties> instanceExtensionProperties;
    3.  
    4. //查询实例扩展
    5. vkEnumerateInstanceExtensionProperties( nullptr,
    6. &numInstanceExtensions,
    7. nullptr);
    8.  
    9. //如果有支持的扩展,查询它们的属性
    10. if (numInstanceExtensions != 0)
    11. {
    12. instanceExtensionProperties.resize(numInstanceExtensions);
    13. vkEnumerateInstanceExtensionProperties( nullptr,
    14. &numInstanceExtensions,
    15. instanceExtensionProperties.data());
    16. }

    在代码清单1.5执行后,instanceExtensionProperties就包含了实例支持的扩展列表。VkExtension Properties类型的数组的每个元素描述了一个扩展。VkExtensionProperties的定义如下。

    
     
    1. typedef struct VkExtensionProperties {
    2. char extensionName[VK_MAX_EXTENSION_NAME_SIZE];
    3. uint32_t specVersion;
    4. } VkExtensionProperties;

    结构体VkExtensionProperties仅仅包含扩展名和版本号。扩展可能随着新的修订版的推出增加新的功能。字段specVersion允许在扩展中增加新的小功能,而无须创建新的扩展。扩展的名字存储在extensionName里面。

    就像你之前看到的,当创建Vulkan实例时,结构体VkInstanceCreateInfo有一个名叫ppEnabled ExtensionNames的成员,这个指针指向一个用于命名需要激活的扩展的字符串数组。如果某个平台上的Vulkan系统支持某个扩展,这个扩展就会包含在vkEnumerateInstanceExtensionProperties()返回的数组里,然后它的名字就可以通过结构体VkInstanceCreateInfo里的字段ppEnabledExtension Names传递给vkCreateInstance()。

    查询支持的设备扩展是个相似的过程,需要调用函数vkEnumerateDeviceExtensionProperties(),其原型如下。

    
     
    1. VkResult vkEnumerateDeviceExtensionProperties (
    2. VkPhysicalDevice physicalDevice,
    3. const char* pLayerName,
    4. uint32_t* pPropertyCount,
    5. VkExtensionProperties* pProperties);

    vkEnumerateDeviceExtensionProperties()的原型和vkEnumerateInstanceExtensionProperties()几乎一样,只是多了一个参数physicalDevice。参数physicalDevice是需要查询扩展的设备的句柄。就像vkEnumerateInstanceExtensionProperties()一样,如果pProperties是nullptr,vkEnumerateDevice ExtensionProperties()将pPropertyCount重写成支持的扩展的数量;如果pProprties不是nullptr,就用支持的扩展的信息填充这个数组。结构体VkExtensionProperties同时用于实例扩展和设备扩展。

    当创建逻辑设备时,结构体VkDeviceCreateInfo里的字段ppEnabledExtensionNames可能包含一个指针,指向vkEnumerateDeviceExtensionProperties()返回的字符串中的一个。

    有些扩展以可以调用的额外入口点的形式提供了新的功能。这些以函数指针的形式提供,这些指针必须在扩展激活后从实例或者设备中查询。实例函数对整个实例有效。如果某个扩展扩充了实例层面的功能,你应该使用实例层面的函数指针访问新特性。

    为了获取实例层面的函数指针,调用vkGetInstanceProcAddr(),其原型如下。

    
     
    1. PFN_vkVoidFunction vkGetInstanceProcAddr (
    2. VkInstance instance,
    3. const char* pName);

    参数instance是需要获取函数指针的实例的句柄。如果应用程序使用了多个Vulkan实例,那么这个指令返回的函数指针只对引用的实例所拥有的对象有效。函数名通过pName传入,这是个以nul结尾的UTF-8类型的字符串。如果识别了函数名并且激活了这个扩展,vkGetInstance ProcAddr()的返回值是一个函数指针,可以在应用程序里调用。

    PFN_vkVoidFunction是个函数指针定义,其声明如下。

    
     
    1. VKAPI_ATTR void VKAPI_CALL vkVoidFunction(void);

    Vulkan里没有这种特定签名的函数,扩展也不太可能引入这样的函数。绝大部分情况下,需要在使用前将生成的函数指针类型强制转换为有正确签名的函数指针。

    实例层面的函数指针对这个实例所拥有的所有对象都有效——假如创建这些对象(或者设备本身,如果函数在这个设备上调度)的设备支持这个扩展,并且这个设备激活了这个扩展。由于每个设备可能在不同的Vulkan驱动里实现,因此实例函数指针必须通过一个间接层登录正确的模块进行调度。因为管理这个间接层可能引起额外开销,所以为了避免这个开销,你可以获取一个特定于设备的函数指针,这样可以直接进入正确的驱动。

    为了获取设备层面的函数指针,调用vkGetDeviceProcAddr(),其原型如下。

    
     
    1. PFN_vkVoidFunction vkGetDeviceProcAddr (
    2. VkDevice device,
    3. const char* pName);

    使用函数指针的设备通过参数device传入。需要查询的函数的名字需要使用pName传入,这是个以nul 结尾的UTF-8类型的字符串。返回的函数指针只在参数device指定的设备上有效。device必须指向支持这个扩展(提供了这个新函数)的设备,并且这个扩展已经激活。

    vkGetDeviceProcAddr()返回的函数指针特定于参数device。即使同样的物理设备使用同样的参数创建出了多个逻辑设备,你也只能在查询这个函数指针的逻辑设备上使用该指针。

    1.8 彻底地关闭应用程序

    在程序结束之前,你需要自己负责清理干净。在许多情况下,操作系统会在应用程序结束时清理已经创建的资源。然而,应用程序和代码同时结束的情景并不经常出现。比如你正在写一个大型应用程序的组件,应用程序可能结束了使用Vulkan实现的渲染和计算操作,但是并没有完全退出。

    在清除时,通常来说,较好的做法如下。

    • 完成或者终结应用程序正在主机和设备上、Vulkan相关的所有线程里所做的所有工作。
    • 按照创建对象的时间逆序销毁对象。

    逻辑设备很可能是初始化应用程序时创建的最后一个对象(除了运行时使用的对象之外)。在销毁设备之前,需要保证它没有正在执行来自应用程序的任何工作。为了达到这个目的,调用vkDeviceWaitIdle(),其原型如下。

    
     
    1. VkResult vkDeviceWaitIdle (
    2. VkDevice device);

    把设备的句柄传入device。当vkDeviceWaitIdle()返回时,所有提交给设备的工作都保证已经完成了——当然,除非同时你继续向设备提交工作。需要保证其他可能向设备提交工作的线程已经终止了。

    一旦确认了设备处于空闲状态,就可以安全地销毁它了。这需要调用vkDestroyDevice(),其原型如下。

    
     
    1. void vkDestroyDevice (
    2. VkDevice device,
    3. const VkAllocationCallbacks* pAllocator);

    把需要销毁的设备的句柄传递给参数device,并且访问该设备需要在外部同步。需要注意的是,其他指令对设备的访问都不需要外部同步。然而,应用程序需要保证当访问该设备的其他指令正在另一个线程里执行时,这个设备不要销毁。

    pAllocator应该指向一个分配的结构体,该结构体需要与创建设备的结构体兼容。一旦设备对象被销毁了,就不能继续向它提交指令了。进一步说,设备句柄就不可能再作为任何函数的参数了,包括其他将设备句柄作为第一个参数的对象销毁方法。这是应该按照创建对象的时间逆序来销毁对象的另一个原因。

    一旦与Vulkan实例相关联的所有设备都销毁了,销毁实例就安全了。这是通过调用函数vkDestroyInstance()实现的,其原型如下。

    
     
    1. void vkDestroyInstance (
    2. VkInstance instance,
    3. const VkAllocationCallbacks* pAllocator);

    将需要销毁的实例的句柄传给instance,与vkDestroyDevice()一样,与创建实例使用的分配结构体相兼容的结构体的指针应该传递给pAllocator。如果传递给vkCreateInstance()的参数pAllocator是nullptr,那么传递给vkDestroyInstance()的参数pAllocator也应该是这样。

    需要注意的是,物理设备不用销毁。物理设备并不像逻辑设备那样由一个专用的创建函数来创建。相反,物理设备通过调用vkEnumeratePhysicalDevices()来获取,并且属于实例。因此,当实例销毁后,和每个物理设备相关的实例资源也都销毁了。

    1.9 总结

    本章介绍了Vulkan。你已看到了Vulkan状态整体上如何包含在一个实例里。实例提供了访问物理设备的权限,每个物理设备提供了一些用于执行工作的队列。本章还演示了如何根据物理设备创建逻辑设备,如何扩展Vulkan,如何判断实例,设备能用哪些扩展,以及如何启用这些扩展。最后还演示了如何彻底地关闭Vulkan系统,操作顺序依次是等待设备完成应用程序提交,销毁设备句柄,销毁实例句柄。


    [1] 是的,确实是nul。字面量为零的ASCII字符被官方称为NUL。现在,不要再告诉我应该改成NULL。这是个指针,不是字符的名字。

    [2] 对于一个程序来说是最好的,但在另一个程序中就未必如此。另外,程序是由人编写的,人在写代码时就会有Bug。为了完全优化,或者消除应用程序的Bug,驱动有时候会使用可执行文件的名字,甚至使用应用程序的行为来猜测正在哪个应用程序上运行,并相应地改变行为。虽然并不完美,但这个新的机制至少消除了猜测。

    [3] 和OpenGL一样,Vulkan支持将扩展作为API的中心部分。然而,在OpenGL里,我们会创建一个运行上下文,查询支持的扩展,然后开始使用它们。这意味着,驱动需要假设应用程序可能在任何时候突然开始使用某个扩展,并随时准备好。另外,驱动不可能知道你正在查找哪些扩展,这一点更加重了这个过程的困难程度。在Vulkan里,要求应用程序选择性地加入扩展,并显式地启用它们。这允许驱动关闭没有使用的扩展,这也使得应用程序突然开始使用本没有打算启用的扩展中的部分功能变得更加困难。

    [4] 并没有关于PCI厂商或者设备标识符的官方的中央版本库。PCI SIG(可从pcisig网站获取)将厂商标识符指定给了它的成员,这些成员又将设备标识符指定给了它们的产品。人和机器同时可读的清单可从pcidatabase网站获取。

    本文摘自《Vulkan 应用开发指南》

    《Vulkan 应用开发指南》

    [美] 格拉汉姆·塞勒斯(Graham Sellers) 著,李晓波 等 译

    0101{60%}

    下一代OpenGL规范已经重新进行了设计,从而使得应用程序可以直接控制GPU的加速。本书系统地介绍下一代OpenGL规范Vulkan、它的目标以及构建其API的关键概念,揭示了Vulkan的独特性。

    本书讨论的主题非常宽泛,从绘图命令到内存,再到计算着色器的线程。本书重点展示了如何处理现在由开发人员负责的同步、调度和内存管理等任务。本书是Vulkan开发人员的指南和参考手册,有助于读者迅速掌握跨平台图形的下一代规范。你将从本书中学习到可用于从视频游戏到医学成像等领域的3D开发技术,以及解决复杂的科学计算问题的先进方法。

    本书主要内容

    • 大量经过反复测试的代码示例,用于演示Vulkan的功能并展示它与OpenGL的区别。
    • Vulkan中的新内存系统。
    • 队列、命令和移动数据的方法。
    • SPIR-V二进制着色语言和计算/图形管道。
    • 绘图命令、几何处理、片段处理、同步原语,以及将Vulkan数据读入应用程序。
    • 完整的案例研究应用程序:使用复杂的多通道架构和多个处理队列的延迟渲染。
    • Vulkan函数和SPIR-V操作码,以及完整的Vulkan词汇表。
    展开全文
  • Vulkan介绍

    2018-11-02 12:09:46
    Vulkan介绍 Vulkan是由Khronos组织开发的一种高级图形API。其他图形API(像OpenGL和Direct3D)需要驱动去将上层API翻译成适合硬件执行的指令。这些图形API是为了使开发者不需要关注复杂的图形硬件细节。 随着...
  • Vulkan学习(一)介绍

    千次阅读 2019-03-29 22:44:07
    1、Vulkan是什么 Vulkan是一个跨平台的2D和3D绘图应用程序接口(API),最早由科纳斯组织在2015年游戏开发者大会(GDC)上发表。 它是 AMD Mantle 的后续版本,继承了前者强大的低开销架构,使软件开发人员能够...
  • Vulkan入门(一).md

    千次阅读 2019-05-01 12:02:05
    文章目录参考资料一. 准备环境1.1 开发环境1.2 下载 SDK...https://vulkan.lunarg.com/doc/sdk/1.1.106.0/linux/getting_started.html 一. 准备环境 1.1 开发环境 sudo apt-get update sudo apt-get dist-upgrade...
  • OpenGL的替代者——Vulkan

    万次阅读 2018-04-12 10:14:40
    OpenGL的替代者——VulkanVulkan是一个跨平台的2D和3D绘图应用程序接口(API),最早由科纳斯(Khronos)组织在2015年游戏开发者大会(GDC)上发表。旨在替代OpenGL,提高图形性能。基于OpenGL的图形引擎性能瓶颈基于...
  • VulkanAPI架构

    2020-05-04 23:09:25
    Vulkan API基本概念 一、设备初始化 1.1 Instance --> GPU --> Device Instance表示具体的Vulkan应用。在一个应用程序中可以创建多个实例,这些实例之间相互独立,互不干扰。 当调用API创建Vulkan实例的时候,...
  • Android N(7.0)中的Vulkan支持

    万次阅读 2016-10-11 21:45:39
    Vulkan为Khronos Group推出的下一代跨平台图形开发接口,用于替代历史悠久的OpenGL。Android从7.0(Nougat)开始加入了对其的支持。Vulkan与OpenGL相比,接口更底层,从而使开发者能更直接地控制GPU。由于更好的并行...
  • Vulkan Tutorial

    千次阅读 2016-11-10 23:48:49
    Welcome to the Vulkan Samples Tutorial本教程分为多个部分,对应于创建一个简单的Vulkan程序所需要的基本步骤。教程的每一部分直接对应于LunarG示例完成进度中的一个示例程序,并且被设计成可以根据示例的开发进度...
  • Vulkan入门

    千次阅读 2017-02-16 16:17:00
    本人的显卡是GTX 770M,安装的驱动程序支持Vulkan.是否支持vulkan可以通过https://developer.nvidia.com/vulkan-driver查询结果 安装Vulkan SDK 下载并安装Vulkan SDK ...
  • Vulkan 简介

    2020-08-02 22:18:55
    1. Vulkan起源和历史 1.1 AMD Mantle 2013年,AMD主导开发了Mantle。Mantle是面向3D游戏的新一代图形渲染 API,可以让开发人员直接操作GPU硬件底层,从而提高硬件利用率和游戏性能,效果显著。 Mantle很好的带动了...
  • 30分钟入门Vulkan

    万次阅读 2017-04-17 13:56:35
    这篇文章是写给对已有的D3D11和GL比较熟悉并理解多线程、资源暂存、同步等概念但想进一步了解它们是如何以Vulkan实现的读者,因此本文会以Vulkan概念之旅结束。 文章并不追求易懂(为了解里面的晦涩内容,你应该去...
  • 前记:本人学生一枚,由于原来用Unity写Shader热爱计算机图形学,专业是软件工程,后来热爱OpenGL,听老师介绍说有个图形接口叫VulKan非常棒,就到VulKan官网上学习,这是我看官方文档后进行的简单翻译和总结,如果...
  • Vulkan示例

    千次阅读 2016-05-08 12:21:25
    更新到了Device,若想使用这些代码请自行下载Vulkan SDK并链接库,或者加入QQ群:308637496,我把资料已经传到QQ群里了 //win32平台 #define VK_USE_PLATFORM_WIN32_KHR //Vulkan相关头文件 #include #include ...
  • 发现好文章一篇,关于Vulkan的Layer的结构的。这是LunarG出品的,非常详细的介绍了Loader和Layer的关系和结构。 并且介绍了它们的加载过程以及调用关系;图文并茂! ICD的JSON文件路径: /usr/local/etc/...
1 2 3 4 5 ... 20
收藏数 4,687
精华内容 1,874
关键字:

vulkan