Instance
Extensions
1 | uint32_t extensions_count = 0; |
创建
1 | VkApplicationInfo |
做此刻最想做的事
1 | uint32_t extensions_count = 0; |
1 | VkApplicationInfo |
- Rendering a geometry with a vertex diffuse lighting
- Rendering a geometry with a fragment specular lighting
- Rendering a normal mapped geometry
- Drawing a reflective and refractive geometry using cubemaps
- Adding shadows to the scene
…
vertex
1 | #version 450 |
fragment
1 | #version 450 |
…
1 | #version 450 |
vertex
1 | #version 450 |
fragment
1 | #version 450 |
order:+X,-X,+Y,-Y,+Z,-Z
vertex
1 | #version 450 |
fragment
1 | #version 450 |
[TOC]
- Drawing a skybox
- Drawing billboards using geometry shaders
- Drawing particles using compute and graphics pipelines
- Rendering a tessellated terrain
- Rendering a full-screen quad for post-processing
- Using input attachments for a color correction post-process effect
order:right,left,up,down,backward,forward
vertex
1 | #version 450 |
fragment
1 | #version 450 |
geometry shader
1 | #version 450 |
…
vertex
1 | #version 450 |
tessellation control
1 | #version 450 |
tessellation evaluation shader
1 | #version 450 |
geometry shader
1 | #version 450 |
fragment
1 | #version 450 |
[TOC]
Preparing a translation matrix
Preparing a rotation matrix
Preparing a scaling matrix
Preparing a perspective projection matrix
Preparing an orthographic projection matrix
Loading texture data from a file
Loading a 3D model from an OBJ file
关于vulkan 坐标系x (lef/rightt), y (down/up), and z (near/far).右手坐标系
关于矩阵
1 | std::array<float, 16> translation_matrix = { |
All elements initialize with a 0.0f value
0th, 5th, 10th, and 15th elements (main diagonal) with a 1.0f value
12th element with a value stored in the x variable
13th element with a value stored in the y variable
14th element with a value stored in the z variable
也就是列主序
0th element with a x x (1.0f - c) + c
1st element with a y x (1.0f - c) - z s
2nd element with a z x (1.0f - c) + y s
4th element with a x y (1.0f - c) + z s
5th element with a y y (1.0f - c) + c
6th element with a z y (1.0f - c) - x s
8th element with a x z (1.0f - c) - y s
9th element with a y z (1.0f - c) + x s
10th element with a z z (1.0f - c) + c
The rest of the elements initialize with a 0.0f value
Except for the 15th element, which should contain a 1.0f value
Each column of such matrix defines the directions of x, y, and z axes respectively after the rotation is performed.
一个例子
1 | if( normalize ) { |
- All elements initialize with a 0.0f value
- 0th element with a value stored in the x variable
- 5th element with a value stored in the y variable
- 10th element with a value stored in the z variable
- 15th element with a 1.0f value
..
1 | std::array<float, 16> scaling_matrix = { |
1 | float aspect_ratio;//width/height |
- 0th element with a f / aspect_ratio
- 5th element with a -f
- 10th element with a far_plane / (near_plane - far_plane)
- 11th element with a -1.0f value
- 14th element with a (near_plane * far_plane) / (near_plane -
far_plane)- The rest of the elements initialize with a 0.0f value
1 | Matrix4x4 orthographic_projection_matrix = { |
stb
1 | int width = 0; |
1 | std::vector<unsigned char> image_data; |
https://github.com/syoyo/tinyobjloader.
1 | #include ... |
1 | tinyobj::attrib_t attribs; |
https://github.com/KhronosGroup/Vulkan-ValidationLayers
Vulkan API设计时时刻考虑着性能.其中一个提升其性能的方式是减少驱动的状态和错误检查.这是Vulkan被称为”thin API”或”thin driver”,因为它硬件的最小抽象–这在多个硬件上跨平台是必须的(高性能的PC,移动手机,低性能的嵌入式设备等).
这导致变现Vulkan API的app更困难.为了解决这个问题,Vulkan也被设计为一个分层的API,最底层和核心的曾是Vulkan API自己–与驱动交互.它的上策(app与Vulkan API之间),开发者可以激活额外的layers,简化debugging流程.
找到Vulkan SDK安装目录,找到./Config文件夹
将其内的vk_layer_settings.txt拷贝执行程序所在文件夹内
创建环境变量 VK_INSTANCE_LAYERS:
打开控制台(cmd.exe),输入一下内容:
setx VK_INSTANCE_LAYERS
VK_LAYER_LUNARG_standard_validation
重新打开控制台
使用控制台执行要测试的程序,warnings或errors会显示到控制台里
同上
同上
输入命令变为
export
VK_INSTANCE_LAYERS=VK_LAYER_LUNARG_standard_validation
同上
Vulkan validation layers包含了一组创建app时辅助找到潜在问题的库.他们的调试策略包括但不限于:参数检测,确认texture和RT的formats,追踪Vulkan objects的生命周期和使用,检查Vulkan API函数调用的潜在的内存泄漏和奔溃.这些功能通过激活不同的validation layers来启动,但其中大多数被集中到了一个叫VK_LAYER_LUNARG_standard_validation的层.其他layers包括:VK_LAYER_LUNARG_swapchain,VK_LAYER_LUNARG_object_tracker,VK_LAYER_GOOGLE_threading,VK_LAYER_LUNARG_api_dump等.多个layers可以同时激活,如果是windows这样做:
1 | setx VK_INSTANCE_LAYERS |
如果是linux:
1 | export VK_INSTANCE_LAYERS=VK_LAYER_LUNARG_api_dump:VK_LAYER_LUNARG_core_validation |
但是上述的方法都是全局的,事实上,实际编程时,我们根据是否时DEBUG模式动态决定是否使用Vulkan validation layers.
通过标准宏NDEBUG决定是否启用某些validation layers
通过vkEnumerateInstanceLayerProperties获得支持的validation layers
将支持且需要启用的validation layer绑定到VkInstanceCreateInfo
调用vkCreateInstance时启用
定义调试回调函数,PFN_vkDebugReportCallbackEXT
填充结构VkDebugReportCallbackCreateInfoEXT
调用vkCreateDebugReportCallbackEXT(扩展函数,需要使用vkGetInstanceprocAddr获得)创建
注意需要在清理Instance前调用vkDestroyDebugReportCallbackEXT清理callback
参考https://blog.csdn.net/lbknxy/article/details/52430599
在创建了Vulkan Instance 对象后,下一步就是枚举physical devices,选择其中一个,并根据它创建又给logical device,这些操作通过instance-level级函数进行执行.
Vulkan里几乎所有工作都是在logical device上执行的:在其上创建资源,管理他们的内存,记录由它们创建的command buffers,向他们的queues提交处理commands.
1 | uint32_t devices_count = 0; |
1 | uint32_t extensions_count = 0; |
1 | vkEnumeratePhysicalDevices() |
在vulkan里,对硬件的所有操作都是通过提交到queues里进行的.提交到同一个queue里的命令是一个接着一个执行的,提交到不同queue里的命令是独立的(需要同步他们)
不同的queues可能表示硬件的不同部分,因此会支持不同的操作,并非所有操作都可以在所有queue上执行.
有相同策略的queue被聚合在同样的family里,一个device可能暴露任意数量的queue families,每一个family可能包括一个或多个queues.要想检查硬件能执行什么操作,需要便利所有queue families的属性.
步骤
1 | uint32_t queue_families_count = 0; |
从properties中能获取到的最重要的信息是能在已给family的queues里进行的操作类型,操作类型被分为:
- Graphics:创建graphics pipelines和drawing
- Compute:创建compute pipelines和dispatching compute shaders
- Transfer:用于非常快速的memory-copying操作
- Sparse:允许附加的内存管理功能
family的queues可能支持不知一种操作,可能不同的queue families支持相同类型的操作.
family属性也告知了queues的数量,是否支持时间戳,图像传输操作的粒度(在copy/blit操作时能控制的图像的最小部分).
创建logic device需要的queue families数量,属性,每个family可用的queues数量准备好后,只在创建logical device时请求他们,需要制定需要多少queue以及来自哪些families,当创建好logic device后,对应的queue也自动被创建,我们只需要获得请求的queue的handles.
选择一个physical device
1 | uint32_t queue_family_index = 0; |
logical device是app里创建的最重要的object,它代表了真实硬件,包括所有的扩展和激活的特性,以及向它请求的所有队列.
logical device允许我们做渲染软件里的所有工作,比如创建images和buffers,设置pipeline state或者加载shaders.最终要的是记录commands(比如draw calls或者分发computational works)和提交它们到queues.
1 | struct QueueInfo { |
1 | std::vector<VkExtensionProperties> available_extensions; |
device queue是硬件资源,只能获取已有数量的
前提
1 | 获取到VkDvice logical_dvice |
1 | 调用 |
重复上述操作可以获取到所有queue families里的所有queues
现在现需要美剧所有physical device找到其中支持geometry shaders,graphics和compute queues的来创建logical device
前期准备
1 | 声明变量 |
1 | std::vector<VkPhysicalDevice> physical_devices; |
对每一个physical device进行如下操作
1 | for( auto & physical_device : physical_devices ) { |
如果graphics和computee queue families有相同的index,我们能使用同一个queue,如果不同就要用两个
创建logical device
1 | if( !CreateLogicalDevice( physical_device, requested_queues, {}, |
按照创建的相反顺序销毁资源
1 | if( logical_device ) { |
[TOC]
- Converting GLSL shaders to SPIR-V assemblies
- Writing vertex shaders
- Writing tessellation control shaders
- Writing tessellation evaluation shaders
- Writing geometry shaders
- Writing fragment shaders
- Writing compute shaders
- Writing a vertex shader that multiplies a vertex position by a projection matrix
- Using push constants in shaders
- Writing a texturing vertex and fragment shaders
- Displaying polygon normals with a geometry shader
3D graphics data(vertices和fragments/pixels)运行在叫做stages的一些列步骤里.某些stages只执行固定操作(只能配置某些扩展操作).单有其他stages需要programmed.控制这些stages的行为的小programs称为shaders.
vulkan里最主要的5个可编程graphic pipeline stages为vertex,tessellation control,evaluation,geometry,fragment.(1.1.85发布了Raytracing, Mesh Shaders & Other New NVIDIA Extensions,https://www.phoronix.com/scan.php?page=news_item&px=Vulkan-1.1.85-Released).compute pipeline有compute shader programs.core Vulkan API 通过SPIR-V的programs控制这些stages.他是一门中间语言,允许我们process graphics data和进行vectors,matrices,images,buffers,samplers的数学计算.这种语言的low-level特性提高了编译时间(compilation times).也使得书写shader困难.所以vulkan sdk提供一个工具链glslangValidator.
glslangValidator可以将GLSL转换为SPIR-V assemblies.
本文介绍GLSL,所有programmable stages的shaders实现,如何实现tessellation或texturing,如何使用geometry shaders来debuging.以及将GLSL转换为SPIR-V assemblies(使用glslangValidator).
shaders与stage
- vert for the vertex shader stage
- tesc for the tessellation control shader stage
- tese for the tessellation evaluation shader stage
- geom for the geometry shader stage
- frag for the fragment shader stage
- comp for the compute shader
顶点.
如果想绘制nosolid geometry,需要在创建logical device是激活fillModeNonSolid属性.
vertex processing是graphics pipeline第一个stage,主要目的是对顶点进行坐标转换,从局部坐标转换到coordinate system(clip space).这个clip coordinate system用于图形硬件后续步骤.其一为clipping.
但这个工作也可以延后到tessellation or geeometry shader.
存储到gl_Position.
一个例子
1 | #version 450 |
细分.
需要在创建logical device时激活tessellationShader特性.
可选的.分为三步,其中两步是可编程的.The first programmable tessellation stage is used to set up parameters that control how the tessellation is performed.通过编写tessellation control shaders指明tessellation factors的值.
tessellation control and tessellation evaluation shaders是一起用的.
tessellation stage对patches进行操作.Patches由vertices组成,但与传统polygons不同,$\color {red}{每个patch可能任意个顶点,1~(至少)32个}$.
细分是通过在shader代码里指明的inner和outer tessellation factors实现的.inner fctor由内置gl_TessLevelInner[]数组指明patch内部是如何细分的.outer factor 对应的是gl_TessLevelOuter[],定义了如何细分patches的外边缘(outer edges).每个数组元素对应于patches的给定边.
tessellation control shader对patch的每个vertex执行一次.当前vertex的index为gl_InvocationID.只有当前的可写,但可以通过gl_in[].gl_Position读取input patch的所有vertices.
一个例子
1 | #version 450 |
Tessellation evaluations是tessellation process的第二个可编程stage.当geometry被细分后(subdivided),手机细分的结果形成新的vertices并重新修改它们.我们需要编写tessellation evaluation shaders来获取生成的顶点的位置,并将它们提供给连续的consecutive pipeline stages.
在这两个着色器之间是根据control stage 提供的参数进行细分的真实过程.tessellation的结果在evaluation stage中用到,生成新的geometry.
在tessellation evaluation中能控制新的primitives如何对其并形成.我们指定了它们的缠绕顺序(winding order)和生成顶点之间的间距.同时能选择是否在tessellation stage创建isolines, triangles,or quads来改变图元类型.
新顶点㐊直接创建的,细分器只为新vertices生成重心细分坐标(barycentric tessellation coordinates)(weights),内置变量为gl_TessCoord.我们可以使用这些坐标在形成面片的顶点的原始位置之间进行插值(interpolate),并将新的顶点放置在正确的位置.这也是evaluation shader可以访问所有顶点的原因.(gl_in[].gl_Position)
三角形通常不用做任何事
1 | #version 450 |
当绘制Object时,我们提供顶点并指明图元(primitives)类型(points,lines,triangles).在vertex和可选的tessellation stages后,形成了指定的图元类型.可选的可以激活geometry stage并写geometry shaders控制或改变从vertices形成primitives的过程.在geometry shaders甚至可以创建新的primities或销毁已经存在的.
geometry shaders允许创建额外的vertices混合primitives,删除已经存在的或改变vertices形成的primitive类型.
它能访问primitvie的所有vertices,且能调整它们.根据这些数据能原文传递或者创建新的vertices和primitives.但不能在一个gs中创建太多vetices.tessellation shaders更适合做这种操作.应尽量减少gs发出的顶点数.
gs总生成strip primitives.如果想创建独立图元–不形成strip,只需要在合适的时机end a primitive–vertices emitted
1 | #version 450 |
fragment对geometry shader后的操作(rasterization,光栅化)的数据进行计算,一般使用屏幕坐标系(screen space coordinate(x,y,depth)).能选择哪个attachment写入颜色.
geometry 形成的primitives在rasterization过程中转换为fragments(pixels).fragment sahder对每个fragment执行一遍.shader中或framebuffer tests(depth,stencil,scissor tests)中Fragments可能discard,不会成为pixels–这是它们称为fragments而不是pixels的原因.
fragment sahder最重要的目的是设置写入attachement的颜色.通常进行光照计算和texturing.和compute shaders配合,fragment shader还常用来进行后处理(bloom等)或者defered shading/lighting.同时,只有fragment shader能访问render pass定义的input attachments.
一个简单例子
1 | #version 450 |
compute shader常用来数学计算.它们以3D形式的组执行并可能访问a common set of data.同时,很多local groups能一起执行能更快得到结果.
只能在compute pipeline里使用.
compute shadeers没有任何早于或晚于pipeline stages的输入或输出.只有compute pipeline stage的.uniform varables必须作为其数据来源.相似,计算结果存储在Uniform variables.
有些内置变量.用来索引.
local workgroup中给定compute shader调用的索引
1 | uvec3 gl_LocalInvocationID |
同时分发的workgroups的数量
1 | uvec3 gl_NumWorkGroups |
当前workgroup的number
1 | uvec3 gl_WorkGroupID |
还有个给当前shader在所有workgroups里所有调用唯一编号的
1 | uvec3 gl_GlobalInvocationID; |
local workgroup的size由input layout qualifier定义.在shader里,uvec3 gl_WorkGroupSize也可以获得这个size.
1 | #version 450 |
将geometry从local 变换到clip space通常在vertex shader进行,尽管其他vertex processing stage(tessellation or geometry)也能完成这个工作.这个变换通过指明model,view,projection matrices和从app提供三者的值给shader或者合为一个MVP矩阵.最普通和简单的方式是使用uniform buffer.
1 | #version 450 |
uniform buffer $0^{th}$ set,$0^{th}$ binding.
当给shaders提供数据通常使用uniform buffers,storage buffers,或其他descriptor resources.不幸的是更新这样一个 resources可能不太合适,尤其是经常改变数据的时候.
为了这个目的,需要引入push constants.通过它们我们能用更简单和快速的方法提供数据.单需要fit更小的可用空间.
使用GLSL shaders里的push constants类似uniform buffers.
不同之处
- 1.使用layout(push_constant)
- 必须指明instance name
- 每个shader只能定义一个
- 通过访问instance name of the block 访问push constant
constants在更新频繁的小数据上非常游泳,比如变换矩阵或物理量(time,dt等).需要记住数据的size比descriptor resources小得多.$\color{red}{规范要求push constants至少存储128 bytes数据}$.每个硬件平台可能允许更多的存储,但可能不会大得多.
在提供颜色的片段明暗器中定义和使用push常量的示例如下:
1 | #version 450 |
vertex shader
1 | #version 450 |
fragment shader
1 | #version 450 |
介绍一种简单的检查发现的方法.
vertex shader
1 | #version 450 |
view space
1 | #version 450 |
为什么使用一个uniform buffer呢?一般来说cb里步骤越少效率越高
fragment shader
1 | #version 450 |
Vulkan API 是硬件厂商提供的,在驱动里.安装Vulkan SDK后,开发者不用关心驱动层,可以通过(windows)vulkan-1.dll或(linux)libvulkan.so.1访问Vulkan API.
1 | #if defined _WIN32 |
第一种方式:可以进行静态链接,并使用vulkan.h里定义的函数原型
第二种方式:不用vulkan.h了定义的原型,而是动态加载函数指针
第一种方式简单,但是每次调用函数时都会重定向一次,效率更低
第二种方式无重定向过程,且可以选择子集,但麻烦点,基本都用这种方式的,这里只介绍这种方式
1.定义VK_NO_PROTOTYPES或者在引入vulkan.h前定义#define VK_NO_PROTOTYPES
2.创建新文件名为ListOfVulkanFunctions.inl
输入一下内容
1 | #ifndef EXPORTED_VULKAN_FUNCTION |
创建头文件VulkanFunctions.h,输入一下内容
1 | #include "vulkan.h" |
创建源文件VulkanFunctions.cpp,输入一下内容
1 | #include "VulkanFunctions.h" |
通过在ListOfVulkanFunctions.inl中添加新的函数名可以加载更多vulkan api
函数名原型:name的原型为PFN_name
分为3类
global-level,instance-level,device-level
创建变量
PFN_vkEnumerateInstanceExtensionProperties
PFN_vkEnumerateInstanceLayerProperties
PFN_vkCreateInstance
并调用vkGetInstanceProcAddr获取到对于函数指针
在ListOfVulkanFunctions.inl中添加
1 | #ifndef GLOBAL_LEVEL_VULKAN_FUNCTION |
在VulkanFunctions.cpp的#include “ListOfVulkanFunctions.inl”前添加
1 | #define GLOBAL_LEVEL_VULKAN_FUNCTION( name ) \ |
变量声明一样的
1 | vkEnumeratePhysicalDevices |
不同之处是,调用vkGetInstanceProcAddr获取函数指针,第一个参数为instance
如何判断一个函数是instance-level还是device-level?
所有device-level函数第一个参数为VkDevice,VkQueue,VkCommandBuffer,因此所有第一个参数不是这些且不是global-level函数的就是device-level
需要额外注意的是,extensions能引入新的函数,需要加载它们,以便在程序中调用.
为了加载instance-level函数,对ListOfVulkanFunctions.inl进行更新
1 | #ifndef INSTANCE_LEVEL_VULKAN_FUNCTION |
更新VulkanFunctions.cpp
1 | #define INSTANCE_LEVEL_VULKAN_FUNCTION( name ) \ |
1 | #define INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( name, extension ) |
创建logical device(VkDevice logical_device)后,需要渲染3D场景、计算对象碰撞,运行视频帧的时候
与前述基本一样,不同之处在于
调用vkGetDeviceProcAddr(device,”
“)获得函数指针
机核所有3D渲染软件都是使用device-level 函数进行工作的,它们用来创建buffers,images,samplers,shaders.使用device-level函数创建pipeline objects,同步primitives,framebuffers和很多其他资源.并且,最重要的,记录之后提交给queues让硬件执行的操作.
device-level函数可以通过vkGetInstanceProcAddr获取指针,但它不是最理想的.Vulkan被设计为灵活的API,它可以在单个程序里的多个devices上执行操作,但当我们调用vkGetInstanceProcAddr函数,我们不能提供任何与logical device关联的参数,因此这个函数返回的指针不能与device关联.device可能在调用vkGetInstanceProcAddr时都不存在,这是该调用返回一个dispatch函数–根据其参数,调用适用于给定的logical device的函数的实现.但是,这个jump有性能开销:很小但任然会消耗运行时间.
如果像避免这种不必要的jump且请求已给device的函数指针,我们需要调用vkGetDeviceProcAddr,这样能避免中间调用,这种方式有些缺陷:需要给app里所有device都请求各自的函数的函数指针.如果有多个device,需要给每个logical device自己的函数指针列表.使用C++预处理可以很容易获得已知device的函数的函数指针.
如何判断函数是device-level?
第一个参数是VkDevice,VkQueue,VkCommandBuffer
为了加载device-level函数,需要更新ListOfVulkanFunctions.inl
1 | #ifndef DEVICE_LEVEL_VULKAN_FUNCTION |
更新VulkanFunctions.cpp
1 | #define DEVICE_LEVEL_VULKAN_FUNCTION( name ) \ |
1 | #if defined _WIN32 |
[TOC]
Creating a command pool
Allocating command buffers
Beginning a command buffer recording operation
Ending a command buffer recording operation
Resetting a command buffer
Resetting a command pool
Creating a semaphore
Creating a fence
Waiting for fences
Resetting fences
Submitting command buffers to a queue
Synchronizing two command buffers
Checking if processing of a submitted command buffer has finished
Waiting until all commands submitted to a queue are finished
Waiting for all submitted commands to be finished
Destroying a fence
Destroying a semaphore
Freeing command buffers
Destroying a command pool
Vulkan的low-level APIs给予了我们队硬件更多的控制权.包括对资源的创建、管理、操作,以及与硬件的交互.command buffer是最重要的object.它允许我们记录操作并提交给硬件执行.更重要的是能在多线程中记录他们,而且是由驱动隐形记录并不需要开发者控制它们.vulkan允许我们重复使用已经存在的command buffers.给予了便利的同时也让开发者有更多的责任.
为此需要格外小心命令的提交,尤其要处理提交command buffer到GPU时如何同步,为此引入了semaphores和fences.
本节讨论command buffers的allocate、record、submit,如何创建synchronizatin primitives并使用它们控制提交操作,如何同步GPU上的command buffers,如何同步app和硬件.
command pool是command buffer申请缓存的地方.缓存是隐性和动态创建的,但如果没有它command buffer就没地方记录commands了.所以先创建command pool
1 | VkCommandPoolCreateInfo command_pool_create_info = { |
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT 从pool里申请一个cb,存在非常短的时间.
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,能立即重置cb.如果没有这个flag,我们只能通过group进行操作(pool的cbs一起).如果没有这个flag,只能记录一次cb.如果想再次记录,需要重置整个pool.
command pools控制command buffers提交的队列.这二十用过queue family index实现,必须在创建Pool时提供.特定pool提供的cb只能提交给特定的family.
1 | VkResult result = vkCreateCommandPool( logical_device, |
command pools在多线程里不能同时被访问.这就是为什么将在其上记录命令缓冲区的每个应用程序线程都应该使用单独的command pools的原因.
command buffers从command pools进行aloocate,这运行我们控制整个groups的一些属性.
- 只能提交到当创建command pool时选择的family的queue里.
- command pools不能异步使用,所以需要为每个线程创建自己的command pools,减少同步消耗,提升性能
command buffers也有自己的属性,一些是记录操作时指定,但在allocate command buffer时有一些重要参数,是否想allocate主或次command buffers:
- primary command buffers能直接提交到queues.也能执行(call)次command buffers.
- 次command buffers只能通过主command buffers执行.不能提交.
这些参数通过VkCommandBufferAllocateInfo指定
1 | VkCommandBufferAllocateInfo command_buffer_allocate_info = { |
allocate
1 | command_buffers.resize( count ); |
当想用硬件执行操作时,需要记录它们并提交给queue.命令记录在command buffers里.当我们记录它们时,我们需要选择一个command buffer.
vulkan里能做的最重要的事情就是记录command buffers了.也是告诉硬件做什么怎么做的唯一方式.当开始record command buffers时,它们的状态时无定义的.通常,command buffer不继承任何状态.当我们记录操作时,我们需要记住设置与这些操作相关的状态.比如drawing command,使用vertex attributes和indices.在我们记录一次drawing操作前,需要用顶点数据和索引数据绑定到各自适当的缓冲区.
主command buffers能call(执行)记录在次command buffers里的命令.执行次command buffers不继承主command buffers的状态.主command buffer的状态在执行次command buffer后也是未定义的(当我们record 一个主command buffer,并且执行了它的一个次command buffer后,我们如果想继续record主command buffer,需要重新设置状态).只有一个意外的state继承规则–当主command buffer在一个render pass里,我们执行一个它的次command buffer,主command buffer的render pass和subpass states被保持着.
1 | VkCommandBufferBeginInfo command_buffer_begin_info = { |
为了性能考虑,需要避免command buffer使用下列标记
1 | K_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT |
开始记录
1 | VkResult result = vkBeginCommandBuffer( command_buffer, |
现在可以向command buffer选择操作了.但我们如何知道哪些操作可以记录到command buffer里.这类函数的名字以vkCmd开头,第一个参数全是VkCommandBuffer.但需要记住不是所有命令都能给主和次command buffers使用.
当我们不想记录更多commands到command buffer,我们需要停止recording它.
为了更快记录和更小影响性能,记录commands时不会有任何报错,所有错误都在vkEndCommandBuffer函数汇报.
所以停止record时需要判断record是否成功
1 | VkResult result = vkEndCommandBuffer( command_buffer ); |
如果报错,就不能submit它,而需要重置它.
如果一个command buffer报错了,必须reset才能重新record.可以通过开始另一个record操作来隐性重置它,也可以显示做到这点.
可以通过重置整个command pool来重置command buffers.也可以独立进行.只有在使用K_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT标志创建了用于分配命令缓冲区的池时,才能执行单独的重置.
显示重置让我们控制command buffer从创建它的pool里申请欢成.在显示重置时,我们能决定是否向归还缓存给pool,或者是否command buffer需要继续保留并在下次command record时复用它.
1 | VkResult result = vkResetCommandBuffer( command_buffer, release_resources ? |
当我们不想单独重置command buffer或者如果我们创建的pool没有标记VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,我们能一次重置pool的所有command buffer.
重置command pool会导致所有由它创建的command buffers回到初始状态.这和单独重置所有command buffers一样,但更快速.并且没有指定VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT标记.
当command buffers被record,它们从pool里得到缓存,这被自动执行.当重置command buffer时,可以选择是否command buffers保留它们的缓存以备后用,或者返回给pool.
1 | VkResult result = vkResetCommandPool( logical_device, command_pool, |
在提交commands和利用(utilize)设备的处理能力前,我们需要知道如何同步操作.semaphores时用于同步的原语(primitives).它们允许我们不仅在一个队列中而且在一个逻辑设备中的不同队列之间协调提交到队列的操作.
semaphores当我们提交commands给queus使用.所以在我们使用它们前需要创建.
semaphores由两个状态:signaled或者unsignaled.semaphores在command buffer提交时使用.我们将它们提供给一个要发出信号的信号量列表时,一旦给定批中提交的所有工作完成,它们就会将状态更改为signaled.相同方式,当我们提交commands给queues,我们能指定提交的commands进入等待状态直到特定list里的所有semaphores变为signaled.通过这种方式,我们可以协调提交到队列的工作,并推迟对依赖于其他命令结果的命令的处理.
当所有semaphores处于signaled且所有等待它们的命令恢复(变为un-signaled)然后复用.
semaphores也用于从swapchain请求images,此时,semaphores必须当我们向相关的images提交commands时使用.这些commands必须等待知道images不再被presentation engine使用.
vkCreateSemaphore
1 | VkSemaphoreCreateInfo semaphore_create_info = { |
semaphores只能用于同步提交给queues的工作,位于硬件核心.app不能获得semaphore的状态,如果app需要同步提交了的commands需要使用fences.
fences是用来同步app和提交给图形硬件的commands的.
1 | VkFenceCreateInfo fence_create_info = { |
vkWaitForFences
1 | if( fences.size() > 0 ) { |
semaphores自动reset,但当fence标记为signaled后,app负责reset.
1 | if( fences.size() > 0 ) { |
1 | struct WaitSemaphoreInfo { |
1 | std::vector<VkSemaphore> wait_semaphore_handles; |
出于性能考虑,需要尽可能在少的dc中提交更多的batches
当一个cb提交后还没执行完,不能再次提交,除非使用flag:VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,但为了性能考虑要尽量避免这个flag.
semaphores
1 | struct WaitSemaphoreInfo { |
1 | std::vector<VkSemaphore> first_signal_semaphores; |
这里同时使用handles和pipeline stages,第二个batch会等待特定pipeline stages的所有semaphores,这意味着提交的cbs部分会开始执行,当到达提供的stages时会暂停
1 | if( !SubmitCommandBuffersToQueue( second_queue, synchronizing_semaphores, |
这显示了如何同步从同一逻辑设备提交到不同队列的多个命令缓冲区的工作.从第二次提交开始的命令缓冲区处理将被推迟,直到第一批中的所有命令完成.
当使用semaphores时,app没有被cb的同步给牵制.
当我们想知道cb何时结束,我们需要使用fences
创建fence,准备cb,提交给queue,记住提交时使用的fence
1 | if( !SubmitCommandBuffersToQueue( queue, wait_semaphore_infos, |
然后等待
1 | return WaitForFences( logical_device, { fence }, VK_FALSE, timeout ); |
这样可以确保cb成功执行才开始接下来的任务.
但是,通常,渲染不应该造成app完全暂停.我们需要检查fence是否signaled,如果没有,就继续执行其他任务,比如ai、物理.当fence signaled,然后执行需要以来提交的commands状态的任务.
fences在我们想reuse cb时也有用,在我们re-cecord它们前,必须确保不再被device执行.我们需要有一定数量的command buffers 一个接一个 记录和提交.只有这样,我们使用其中一个时,就启动一个fence(每个submitted batch需要有一个关联的fence).分批的cb越多,花费在等待fences上的时间越少.(但效率不一定)
并不总希望使用fences,app等待选择的queue执行结束也是可行的.
vkQueueWaitIdle
但这种同步在非常罕见的情况下执行.GPU比CPU快且可能需要不断提交工作以充分利用申请的性能.
在应用程序端执行等待可能会在图形硬件的管道中造成停顿,从而导致设备的利用效率低下
1 | VkResult result = vkQueueWaitIdle( queue ); |
这种等待一般要关闭app和想要销毁创建的资源时用到
1 | VkResult result = vkDeviceWaitIdle( logical_device ); |
1 | if( VK_NULL_HANDLE != fence ) { |
1 | if( VK_NULL_HANDLE != semaphore ) { |
1 | if( command_buffers.size() > 0 ) { |
1 | if( VK_NULL_HANDLE != command_pool ) { |