前言
现代 C++ 部分的学习和总结也差不多了。最后以 Vulkan 总结来收尾吧!Vulkan 已经玩了快两年了,官方教程和官方示例也算是被我玩烂了。自己也写了几版基于 Vulkan 的渲染接口。然而最近发现 Vulkan 支持了 RAII,再重写一版吧!毕竟相比自己设计的 RAII,还是官方的更香。Vulkan 规范只看完了前六章,暂时放一下,以后再找时间看吧。毕竟学函数式编程和模板元编程花了太多的时间。好在买《Template》的时候看到了《Vulkan应用开发指南》这本书。正好可以作为 Vulkan 阶段性总结的资料。
第1章 Vulkan 概述
1.1 引言
- Vulkan 可以访问运行应用程序的主处理器上的共享或非共享内存
- 功能划分为几类
- 传输类别——用于复制数据
- 计算类别——用于运行着色器进行计算工作
- 图形类别——包括光栅化、图元装配、混合、深度和模板测试,以及图形程序员所熟悉的其他功
1.2 实例、设备和队列
- Vulkan 包含了一个层级化的功能结构
- 顶层为实例(Instance)
- 可以一个或多个
- 物理设备 (Physical Device) 为实例的成员变量
- 物理设备数量为当前计算机 GPU 数量
- 物理设备可以创建多个逻辑设备 (Device)
- 逻辑设备启用对应物理设备队列的不同子集
- 队列 (Queue) 执行应用程序请求的工作
- 顶层为实例(Instance)
- 实例
- 是一个软件概念,在逻辑上将应用程序的状态与其他应用程序或者运行在应用程序环境里的库分开
- 物理设备
- 表示为实例的成员变量,每个都有一定的功能,包括一组可用的队列
- 备通常表示一个单独的硬件或者互相连接的一组硬件
- 逻辑设备
- 是一个与物理设备相关的软件概念
- 表示与某个特定物理设备相关的预定资源
- 其中包括了物理设备上可用队列的一个子集
- 可以过创建多个逻辑设备来表示一个物理设备
- 应用程序花大部分时间与逻辑设备交互
1.2.1 Vulkan 实例
- 创建实例
VkResult vkCreateInstance ( const VkInstanceCreateInfo* pCreateInfo, //指向主机内存分配器的指针 //该分配器由应用程序提供,用于管理 Vulkan 系统使用的主机内存 //设置为 nullptr 会导致 Vulkan 系统使用它内置的分配器 const VkAllocationCallbacks* pAllocator, //64 位宽,与主机系统的位数无关 VkInstance* pInstance );
VkInstanceCreateInfo
typedef struct VkInstanceCreateInfo { //结构体的类型 //默认 VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO VkStructureType sType; //将一个相连的结构体链表传入函数 //默认 nullptr,但是推荐填充为有用的信息 const void* pNext; //保留字段,设置为 0 VkInstanceCreateFlags flags; const VkApplicationInfo* pApplicationInfo; //激活的实例层的个数,不需要层,将其设置为 0 //提供日志、性能分析、调试或者其他特性 uint32_t enabledLayerCount; //激活的实例层的名称,不需要层,将其设置为 nullptr const char* const* ppEnabledLayerNames; //激活的扩展的个数,不需要扩展,将其设置为 0 uint32_t enabledExtensionCount; //激活的扩展的名称,不需要扩展,将其设置为 nullptr const char* const* ppEnabledExtensionNames; } VkInstanceCreateInfo;
VkApplicationInfo
typedef struct VkApplicationInfo { //默认值 VK_STRUCTURE_TYPE_APPLICATION_INFO VkStructureType sType; //默认值 nullptr const void* pNext; //应用程序名称,字符串类型 const char* pApplicationName; //应用程序的版本号 //用于标识正在执行的程序,以便工具和驱动识别 uint32_t applicationVersion; //引擎或中间件的名称与版本号 const char* pEngineName; uint32_t engineVersion; //使用 Vulkan API 的版本号 uint32_t apiVersion; } VkApplicationInfo;
1.2.2 Vulkan 物理设备
- Vulkan 有两种设备:物理设备和逻辑设备
- 物理设备通常是系统的一部分——显卡、加速器、数字信号处理器或者其他的组件
- 逻辑设备是物理设备的软件抽象,以应用程序指定的方式配置。逻辑设备是应用程序花费大部分时间处理的对象。但是在创建逻辑设备之前,必须查找连接的物理设备
- 查找连接的物理设备
VkResult vkEnumeratePhysicalDevices ( VkInstance instance, //同时作为输入和输出 //作为输出, Vulkan 将系统里的物理设备数量写入该指针变量 //作为输入, 它会初始化为应用程序能够处理的设备的最大数量 uint32_t* pPhysicalDeviceCount, //指向 VkPhysicalDevice 句柄数组的指针 VkPhysicalDevice* pPhysicalDevices );
- 如果只想知道系统里有多少个设备
- 将 pPhysicalDevices 设置为 nullpt
- Vulkan 将忽视 pPhysicalDeviceCount 的初始值,将它重写为支持的设备的数量
- 可以调用
vkEnumeratePhysicalDevices()
两次,动态调整VkPhysicalDevice
数组的大小- 第一次将
pPhysicalDevices
设置为nullptr
- 第二次将
pPhysicalDevices
设置为一个数组- 此时,数组的大小已经调整为第一次调用返回的物理设备数量
- 第一次将
- 如果只想知道系统里有多少个设备
- 物理设备句柄用于查询设备的功能,并最终用于创建逻辑设备
void vkGetPhysicalDeviceProperties ( VkPhysicalDevice physicalDevice, //包含大量描述物理设备属性的字段 VkPhysicalDeviceProperties* pProperties );
VkPhysicalDeviceProperties
typedef struct VkPhysicalDeviceProperties { //设备支持 Vulkan 的最高版本 uint32_t apiVersion; //了用于控制设备的驱动的版本号, 硬件生产商特定的 uint32_t driverVersion; //标识生产商, 通常是 PCI 生产商 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;
- 判定物理设备支持哪些特性
void vkGetPhysicalDeviceFeatures ( VkPhysicalDevice physicalDevice, //Vulkan 支持的每一个可选特性都有一个布尔类型的字段 VkPhysicalDeviceFeatures* pFeatures );
1.2.3 物理设备内存
- Vulkan 设备要么是一个独立于主机处理器之外的一块物理硬件,要么工作方式 非常不同,以独有的方式访问内存
- Vulkan 里的设备内存是指,设备能够访问到并且用作纹理和其他数据的后备存储器的内存
- 内存可以分为几类,每一类都有一套属性
- 缓存标志位
- 主机和设备之间的一致性行为
- 每种类型的内存都由设备的某个堆(可能会有多个堆)进行支持
- 查询堆配置以及设备支持的内存类型
void vkGetPhysicalDeviceMemoryProperties ( VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties* pMemoryProperties );
VkPhysicalDeviceMemoryProperties
typedef struct VkPhysicalDeviceMemoryProperties { //内存类型数量 //可能为内存类型的最大数量 VK_MAX_MEMORY_TYPES 定义的值 (定义为 32) uint32_t memoryTypeCount; //VkMemoryType 描述内存类型 VkMemoryType memoryTypes[VK_MAX_MEMORY_TYPES]; uint32_t memoryHeapCount; //每一个元素描述了设备的一个内存堆 VkMemoryHeap memoryHeaps[VK_MAX_MEMORY_HEAPS]; } VkPhysicalDeviceMemoryProperties;
VkMemoryType
typedef struct VkMemoryType { //标志位描述内存的类型 //由 VkMemoryPropertyFlagBits 类型的标志位组合而成 VkMemoryPropertyFlags propertyFlags; //每种内存类型都指定了从哪个堆上使用空间 //内存类型的堆栈索引 uint32_t heapIndex; } VkMemoryType;
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
- 内存分配类型不一定立即使用关联的堆的空间
- 驱动可能延迟分配物理内存,直到内存对象用来支持某个资源
VkMemoryHeap
typedef struct VkMemoryHeap { //堆的大小(单位是字节) VkDeviceSize size; //描述堆的标识符 //在 Vulkan 1.0 中,唯一标识符是 VK_MEMORY_HEAP_DEVICE_LOCAL_BIT //如果定义,堆对于设备来说就是本地的 VkMemoryHeapFlags flags; } VkMemoryHeap;
1.2.4 设备队列
- 每个设备都有一个或者多个队列
- 每个队列都从属于设备的某个队列族
- 一个队列族是一组拥有相同功能同时又能并行运行的队列
- 查询设备的队列族
//调用方式与 vkEnumeratePhysicalDevices 相似,支持两次调用 void vkGetPhysicalDeviceQueueFamilyProperties ( VkPhysicalDevice physicalDevice, uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties* pQueueFamilyProperties );
VkQueueFamilyProperties
typedef struct VkQueueFamilyProperties { //描述队列的所有功能, VkQueueFlagBits 类型的标志位的组合组成 VkQueueFlags queueFlags; //族里的队列数量 uint32_t queueCount; //表示当从队列里取时间戳时,多少位有效 //如果为 0,队列不支持时间戳 //如果不是 0,保证最少支持 36 位 uint32_t timestampValidBits; //队列传输图像时支持多少单位(如果有的话) VkExtent3D minImageTransferGranularity; } VkQueueFamilyProperties;
- 如果设备的结构体
VkPhysicalDeviceLimits
里的字段timestampComputeAndGraphics
是VK_TRUE
,那么所有支持VK_QUEUE_GRAPHICS_BIT
或VK_QUEUE_COMPUTE_BIT
的队列都能保证支持 36 位的时间戳- 这种情况下,无须检查
每一个队列。
- 这种情况下,无须检查
- 如果设备的结构体
VkQueueFlagBits
VK_QUEUE_GRAPHICS_BIT
- 支持图形操作,例如绘制点、线和三角形
VK_QUEUE_COMPUTE_BIT
- 支持计算操作,例如发送计算着色器
VK_QUEUE_TRANSFER_BIT
- 支持传送操作,例如复制缓冲区和图像内容
VK_QUEUE_SPARSE_BINDING_BIT
- 内存绑定操作,用于更新稀疏资源
1.2.5 创建逻辑设备
- 在创建逻辑设备时,可以选择可选特性,开启需要的扩展
VkResult vkCreateDevice ( VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice );
VkDeviceCreateInfo
typedef struct VkDeviceCreateInfo { //VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO VkStructureType sType; //除非希望使用扩展,否则 pNext 应设置为 nullptr const void* pNext; //设置为 0 VkDeviceCreateFlags flags; uint32_t queueCreateInfoCount; //描述一个或者多个队列 const VkDeviceQueueCreateInfo* pQueueCreateInfos; //下面四个字段激活层和扩展 uint32_t enabledLayerCount; const char* const* ppEnabledLayerNames; uint32_t enabledExtensionCount; const char* const* ppEnabledExtensionNames; //指明哪些可选扩展是应用程序希望使用的 //设置为 nullpt,不使用 //激活不会使用的特性会影响程序性能 const VkPhysicalDeviceFeatures* pEnabledFeatures; } VkDeviceCreateInfo;
VkDeviceQueueCreateInfo
typedef struct VkDeviceQueueCreateInfo { //VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO VkStructureType sType; const void* pNext; //设置为 0 VkDeviceQueueCreateFlags flags; //指定希望创建的队列所属的族 uint32_t queueFamilyIndex; //创建的队列个数 //设备支持的队列数量必须不小于这个值 uint32_t queueCount; //可选的指针, 指向浮点数数组,表示提交给每个队列的工作的相对优先级 //取值范围是 0.0~1.0 //设置为 nullptr 所有的队列都指定相同的默认优先级 //所有设备最少支持两个离散的优先级 const float* pQueuePriorities; } VkDeviceQueueCreateInfo;
1.3 对象类型和函数约定
- 对象句柄可以分为两大类
- 可调度对象
- 内部包含了一个调度表,其实就是函数表
- 目前有
- 实例 (VkInstance)
- 物理设备 (VkPhysicalDevice)
- 逻辑设备 (VkDevice)
- 命令缓冲区 (VkCommandBuffer)
- 队列 (VkQueue)
- 不可调度对象
- 其他剩余的对象都可以被视为不可调度对象
- 可调度对象
- 任何 Vulkan 函数的第一个参数总是个可调度对象,唯一的例外是创建和初始化实例的相关函数
1.4 管理内存
- Vulkan 提供两种内存
- 主机内存
- Vulkan API 创建的对象需要一定数量的主机内存
- 资源对象(例如缓冲区和图像)需要一定数量的设备内存
- 这就是用于存储资源里数据的内存
- 设备内存
- 主机内存
- 应用程序有可能为 Vulkan 具体的实现管理主机内存,但是要求应用程序管理设备内存
- 需要创建设备内存管理子系统。可以查询创建的每个资源,得到用于支持它的内存的数量和类型
- 每次动态内存分配都会在系统上产生开销。因此,尽量少分配对象是非常重要的
- 推荐做法是,设备内存分配器要分配大块的内存。大量小的资源可以放置在少数几个设备内存块里面
1.5 Vulkan 里的多线程
- 外部同步
- Vulkan 通常会假设应用程序能够保证两个线程会在同一个时间修改同一个对象
- 在 Vulkan 里性能至上的部分 (例如构建命令缓冲区) 中,绝大部分 Vulkan 命令不提供同步功能
- Vulkan 实现从来不需要在内部使用互斥量或者其他的同步原语来保护数据结构体
- 多线程程序很少由于跨线程引起卡顿或者阻塞
- 专门用来允许多线程执行任务时互不阻塞的高级特性
- 主机内存分配可以通过如下方式进行
- 将一个主机内存分配结构体传入创建对象的函数。通过每个线程使用一个分配器,这个分配器里的数据结构体就不需要保护了
- 命令缓冲区是从内存池中分配的,并且访问内存池是由外部同步的。
- 如果应用程序对每个线程都使用单独的命令池,那么命令缓冲区就可以从池内分配空间,而不会互相造成阻塞
- 描述符是从描述符池里的集合分配的
- 描述符代表了运行在设备上的着色器使用的资源、如果每个线程都使用单独的池,描述符集就可以从池中分配,而不会彼此阻塞线程
- 副命令缓冲区允许大型渲染通道 (必须包含在某个命令缓冲区里) 里的内容并行产生,然后聚集起来,就像它们是从主命令缓冲区调用的一样
- 主机内存分配可以通过如下方式进行
1.6 数学概念
- 略
1.7 增强 Vulkan
- 有些功能是可选的,以层和扩展的形式使用的
1.7.1 层
- 层完全或者部分拦截 Vulkan,并增加新的功能
- 如日志、追踪、诊断、性能分析等
- 层可以添加到实例层面
- 会影响整个 Vulkan 实例
- 也有可能影响由实例创建的每个设备
- 层可以添加到设备层面中
- 它只会影响激活这个层的设备
- 查询系统里的实例可用的层
VkResult vkEnumerateInstanceLayerProperties ( //pProperties 是 nullptr, pPropertyCount 应该指向一个变量 //返回数量 //pProperties 不是 nullptr //为 pProperties 数组的长度 uint32_t* pPropertyCount, //不是 nullptr 会向这个数组填充关于系统里注册的层的信息 VkLayerProperties* pProperties );
VkLayerProperties
typedef struct VkLayerProperties { //名称 char layerName[VK_MAX_EXTENSION_NAME_SIZE]; //层实现的版本号 uint32_t specVersion; //具体实现的版本号 uint32_t implementationVersion; //描述层的可读字符串 char description[VK_MAX_DESCRIPTION_SIZE]; } VkLayerProperties;
- 检查哪些层是设备可用
VkResult vkEnumerateDeviceLayerProperties ( VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkLayerProperties* pProperties );
- 官方 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) 扩展提供的功能的验证
VK_LAYER_GOOGLE_threading
- 保证 Vulkan 命令在涉及多线程时有效使用,保证两个线程不会同时访问同一个对象(如果这种操作不允许的话)
VK_LAYER_GOOGLE_unique_objects
- 确保每个对象都有一个独一无二的句柄,以便于应用程序追踪状态,这样能避免下述情况的发生
- 某个实现可能删除代表了拥有相同参数的对象的句柄
- 确保每个对象都有一个独一无二的句柄,以便于应用程序追踪状态,这样能避免下述情况的发生
- 把大量不同的层分到单个更大的层中
VK_LAYER_LUNARG_standard_validation
- 在调试模式下编译时激活这个层
- 在发布模式下关闭所有的层
1.7.2 扩展
- 有些扩展可能要求具体实现跟踪额外的状态,在命令缓冲区构建时进行额外的检查,或者即使扩展没有直接使用,也会带来性能损失
- 扩展可以分为两类
- 实例扩展
- 在某个平台上整体增强 Vulkan 系统
- 设备扩展
- 扩展系统里一个或者多个设备的能力
- 实例扩展
- 实例和设备扩展必须在创建 Vlukan 实例与设备时激活
- 一旦激活,就可以认为这个扩展是 API 的一部分,对应用程序可用
- 查询支持的实例扩展
//用法与vkEnumerateInstanceLayerProperties类似 VkResult vkEnumerateInstanceExtensionProperties ( //可能提供扩展的层的名字, 该字段设置为 nullptr。 const char* pLayerName, //扩展的数量 uint32_t* pPropertyCount, //填充支持的扩展的信息 VkExtensionProperties* pProperties );
VkExtensionProperties
typedef struct VkExtensionProperties { //扩展名 char extensionName[VK_MAX_EXTENSION_NAME_SIZE]; //版本号 uint32_t specVersion; } VkExtensionProperties;
- 查询支持的设备扩展
//类似 vkEnumerateInstanceExtensionProperties VkResult vkEnumerateDeviceExtensionProperties ( VkPhysicalDevice physicalDevice, const char* pLayerName, uint32_t* pPropertyCount, VkExtensionProperties* pProperties );
- 获取实例层面的函数指针
PFN_vkVoidFunction vkGetInstanceProcAddr ( //需要获取函数指针的实例的句柄 VkInstance instance, //函数名, UTF-8 类型的字符串 const char* pName );
- 实例层面的函数指针对这个实例所拥有的所有对象都有效
- 可能能引起额外开销,建议获取一个特定于设备的函数指针
- 获取设备层面的函数指针
PFN_vkVoidFunction vkGetDeviceProcAddr ( VkDevice device, const char* pName );
1.8 彻底地关闭应用程序
- 在清除时,通常来说,较好的做法如下
- 完成或者终结应用程序正在主机和设备上、Vulkan 相关的所有线程里所做的所有工作
- 按照创建对象的时间逆序销毁对象。
- 逻辑设备很可能是初始化应用程序时创建的最后一个对象
- 在销毁设备之前,需要保证它没有正在执行来自应用程序的任何工作
- 为了达到这个目的,调用
vkDeviceWaitIdle(VkDevice device)
- 为了达到这个目的,调用
- 在销毁设备之前,需要保证它没有正在执行来自应用程序的任何工作
- 销毁逻辑设备
void vkDestroyDevice ( VkDevice device, const VkAllocationCallbacks* pAllocator );
- 销毁实例
void vkDestroyInstance ( VkInstance instance, const VkAllocationCallbacks* pAllocator );
- 物理设备不用销毁
第2章 内存和资源
2.1 主机内存管理
-
主机内存
- 是 CPU 可以访问的常规内存
-
只有存储在 CPU 端的数据结构是对齐的,Vulkan 才可以使用高性能指令,提升性能
-
vkCreateInstance()
的VkAllocationCallbacks
typedef struct VkAllocationCallbacks { //指针供应用程序使用 //可以指向任何位置 //Vulkan 不会解引用它 //可以放任何东西,需适配一个指针大小的 blob //传回 VkAllocationCallback 其余成员指向的回调函数 void* pUserData; //用于普通的、对象级别的内存管理 PFN_vkAllocationFunction pfnAllocation; //用于普通的、对象级别的内存管理 PFN_vkReallocationFunction pfnReallocation; //用于普通的、对象级别的内存管理 PFN_vkFreeFunction pfnFree; //指向代替 Vulkan 自带分配器的替换函数 //不返回值 //仅仅用于通知 //作用:程序可以跟踪 Vulkan 的内存使用量 PFN_vkInternalAllocationNotification pfnInternalAllocation; //指向代替 Vulkan 自带分配器的替换函数 //不应真的释放内存 //仅仅用于通知 PFN_vkInternalFreeNotification pfnInternalFree; } VkAllocationCallbacks;
-
pfnAllocation
函数指针所指的函数//负责新的内存分配 void* VKAPI_CALL Allocation( void* pUserData, //指定分配多少字节 size_t size, //指定以多少字节进行内存对齐 size_t alignment, //内存分配的范围和生命周期是什么样的 VkSystemAllocationScope allocationScope );
-
pfnReallocation
函数指针所指的函数void* VKAPI_CALL Reallocation( void* pUserData, void* pOriginal size_t size, size_t alignment, VkSystemAllocationScope allocationScope );
-
pfnFree
函数指针所指的函数void VKAPI_CALL Free( void* pUserData, void* pMemory );
-
pUserData
的合理实现- 用一个 C++ 类实现内存分配器,并且把这个类的 this 指针放进 pUserData 中
-
VkSystemAllocationScope
- VK_SYSTEM_ALLOCATION_SCOPE_COMMAND
- 内存分配只存活于调用该内存分配命令的时段
- 当它只在单条命令上起作用时,Vulkan 很有可能使用这个用于非常短期的临时内存分配
- VK_SYSTEM_ALLOCATION_SCOPE_OBJECT
- 示内存分配和一个特定的 Vulkan 对象直接关联
- 在销毁对象之前,分配的内存一直存在
- 这种类型的内存分配只发生在创建类型的命令执行期间
- 所有以 vkCreate 开头的函数
- VK_SYSTEM_ALLOCATION_SCOPE_CACHE
- 内存分配和内部缓存的某种形式或
VkPipelineCache
对象相关联
- 内存分配和内部缓存的某种形式或
- VK_SYSTEM_ALLOCATION_SCOPE_DEVICE
- 表示内存分配在整个设备中都有效
- Vulkan 实现需要和设备关联的内存时,进行这种内存分配
- VK_SYSTEM_ALLOCATION_SCOPE_INSTANCE
- 表示内存分配在一个实例内有效
- 这种类型的内存分配通常是由层或在 Vulkan 设置的早期阶段里进行的
- 如
vkCreateInstance()
,vkEnumeratePhysicalDevices()
- 如
- VK_SYSTEM_ALLOCATION_SCOPE_COMMAND
-
pfnInternalAllocation
void VKAPI_CALL InternalAllocationNotification( void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope );
-
pfnInternalFree
void VKAPI_CALL InternalFreeNotification( void* pUserData, size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope );
-
pfnInternalAllocation
和pfnInternalFree
- 只用于输出日志和跟踪应用程序的内存使用量
-
声明一个能够用作分配器的 C++ 类
- 略
2.2 资源
- Vulkan 有两种基本的资源:缓冲区和图像
- 缓冲区
- 是一个简单且连续的块状数据,可以用来存储任何东西
- 数据结构
- 原生数组
- 图像数据
- 是一个简单且连续的块状数据,可以用来存储任何东西
- 图像
- 是结构化的,拥有类型和格式信息,可以是多维的,自己也可组建数组,
- 支持对它进行高级的读写操作
- 是结构化的,拥有类型和格式信息,可以是多维的,自己也可组建数组,
- 缓冲区
- 两种类型的资源都是通过两个步骤构造的
- 创建资源自身
- 然后在内存中备份资源
2.2.1 缓冲区
- 缓冲区是 Vulkan 中最简单但使用非常广泛的资源类型
- 用来存储线性的结构化的或非结构化的数据
- 内存中可以有格式,或只是原生的字节数据
- 创建缓冲区对象
VkResult vkCreateBuffer ( VkDevice device, const VkBufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBuffer* pBuffer );
pCreateInfo
typedef struct VkBufferCreateInfo { //VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO VkStructureType sType; //默认使用 nullptr,使用扩展除外 const void* pNext; //缓冲区的属性信息 VkBufferCreateFlags flags; //缓冲区的大小,以字节为单位 VkDeviceSize size; //你如何使用缓冲区 VkBufferUsageFlags usage; //缓冲区在设备支持的多个缓冲区队列中如何使用 VkSharingMode sharingMode; uint32_t queueFamilyIndexCount; //sharingMode 当设置为 VK_SHARING_MODE_CONCURRENT //告诉 Vulkan 哪些队列将使用这个缓冲区 //sharingMode 当设置为 VK_SHARING_MODE_EXCLUSIVE //这两个字段被忽略 const uint32_t* pQueueFamilyIndices; } VkBufferCreateInfo;
VkBufferUsageFlagBits
- VK_BUFFER_USAGE_TRANSFER_SRC_BIT 和 VK_BUFFER_USAGE_TRANSFER_DST_BIT
- 分别表示在转移命令的过程中,可以用作数据源与目标
- 转移操作是把数据从数据源复制到目标的操作
- 分别表示在转移命令的过程中,可以用作数据源与目标
- VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT 和 VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT
- 分别表示缓冲区可用来存储 uniform 与 storage 类型的纹素缓冲区
- 纹素缓冲区是格式化的像素数组,可以用作源或者目标
- 被在 GPU 上运行的着色器读写
- 分别表示缓冲区可用来存储 uniform 与 storage 类型的纹素缓冲区
- VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT 和 VK_BUFFER_USAGE_STORAGE_BUFFER_BIT
- 分别表示可以用于存储 uniform 或 storage 类型的缓冲区
- 没有格式
- 可用来存储任意的数据
- 分别表示可以用于存储 uniform 或 storage 类型的缓冲区
- VK_BUFFER_USAGE_INDEX_BUFFER_BIT 和 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
- 分别用来存放索引与顶点数据
- 用于绘制命令
- VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT
- 存储间接分发和绘制命令的参数
- 这些命令从缓冲区中直接获取参数,而不是从应用程序
- 存储间接分发和绘制命令的参数
- VK_BUFFER_USAGE_TRANSFER_SRC_BIT 和 VK_BUFFER_USAGE_TRANSFER_DST_BIT
sharingMode
- VK_SHARING_MODE_EXCLUSIVE
- 明缓冲区只会被一个队列使用
- VK_SHARING_MODE_CONCURRENT
- 在多个队列中同时使用这个缓冲区
- 可能会导致在一些系统上效率不高
- VK_SHARING_MODE_EXCLUSIVE
2.2.2 格式和支持
-
确定各种格式的属性和支持级别
void vkGetPhysicalDeviceFormatProperties ( VkPhysicalDevice physicalDevice, //待检查的格式 VkFormat format, //如果设备能识别格式,将把支持级别写入 VkFormatProperties* pFormatProperties );
-
如果应用程序必须要求支持某些格式,可以在创建逻辑设备之前做检查并且在应用程序启动时拒用特定的物理设备
-
VkFormatProperties
typedef struct VkFormatProperties { //对图像线性平铺格式的支持级别 VkFormatFeatureFlags linearTilingFeatures; //对图像优化平铺格式的支持级别 VkFormatFeatureFlags optimalTilingFeatures; //这种格式在缓冲区里使用时支持的级别 VkFormatFeatureFlags bufferFeatures; } VkFormatProperties;
-
图像可以是两种基本平铺模式之一
- 线性的
- 图像数据在内存中线性地排列,先按行,再按照列排列
- 最优化的
- 图像数据以最优方案排列,可以最高效地利用显卡内存子系统
- 线性的
-
VkFormatFeatureFlags
- VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT
- 这种格式可以被着色器采样的只读图像使用
- VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT
- 过滤模式,包含线性过滤
- 可以用于采样的图像
- VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT
- 可以用于被着色器读写的图像
- VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT
- 可以用于支持着色器执行原子操作的读写图像
- VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT
- 可以用于着色器只读的纹素缓冲区
- VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT
- 可以用于读写纹素缓冲区
- 该缓冲区可以由着色器进行读写操作
- 可以用于读写纹素缓冲区
- VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_ATOMIC_BIT
- 可以用于读写纹素缓冲区
- 该缓冲区支持着色器执行的原子操作
- 可以用于读写纹素缓冲区
- VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT
- 可以在图形管线的顶点组装阶段用作顶点数据源
- VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT
- 可以在图形管线的颜色混合阶段用作颜色附件
- VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT
- 当启用该选项时, 这种格式的图像可用作颜色附件
- VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT
- 可用作深度、模板或深度-模板附件
- VK_FORMAT_FEATURE_BLIT_SRC_BIT
- 在图像复制操作里,此格式可以用作数据源
- VK_FORMAT_FEATURE_BLIT_DST_BIT
- 可以用作图像复制操作的目标
- VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT
-
当用于图像时,获取更多对某种格式的支持情况的信息
VkResult vkGetPhysicalDeviceImageFormatProperties ( VkPhysicalDevice physicalDevice, VkFormat format, //要询问的图像类型 //VK_IMAGE_TYPE_1D //VK_IMAGE_TYPE_2D //VK_IMAGE_TYPE_3D VkImageType type, //图像的平铺模式 //VK_IMAGE_TILING_LINEAR //VK_IMAGE_TILING_OPTIMAL VkImageTiling tiling, //图像的用途 VkImageUsageFlags usage, //设置为在创建图像时使用的相同值 VkImageCreateFlags flags, //如果 Vulkan 实现识别和支持这种格式,会把支持级别写入 VkImageFormatProperties* pImageFormatProperties );
-
VkImageFormatProperties
typedef struct VkImageFormatProperties { //某个格式的图像在创建时的最大尺寸 VkExtent3D maxExtent; //支持的最大 mipmap 层级数 //如果不支持,为 1 uint32_t maxMipLevels; //图像支持的数组层的最大数量 //如果不支持,为 1 uint32_t maxArrayLayers; //支持多重采样数 VkSampleCountFlags sampleCounts; //这种格式的资源的最大尺寸 VkDeviceSize maxResourceSize; } VkImageFormatProperties;
2.2.3 图像
-
创建图像
VkResult vkCreateImage ( VkDevice device, const VkImageCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImage* pImage );
-
VkImageCreateInfo
typedef struct VkImageCreateInfo { //VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO VkStructureType sType; const void* pNext; //描述图像部分属性信息的标志位 VkImageCreateFlags flags; //创建的图像的类型 //VK_IMAGE_TYPE_1D //VK_IMAGE_TYPE_2D //VK_IMAGE_TYPE_3D VkImageType imageType; VkFormat format; //像素为单位的大小 //能够创建的图像的最大尺寸依赖于每个 GPU 设备 VkExtent3D extent; //mipmap 层级的个数 //最小的层级 0 uint32_t mipLevels; uint32_t arrayLayers; //采样的次数 VkSampleCountFlagBits samples; //平铺模式 //VK_IMAGE_TILING_LINEAR //VK_IMAGE_TILING_OPTIMAL VkImageTiling tiling; //描述图像在哪里使用 VkImageUsageFlags usage; //VK_SHARING_MODE_EXCLUSIVE 在某个时刻只能被一个队列使用 //VK_SHARING_MODE_CONCURRENT 可以同时被多个队列访问 VkSharingMode sharingMode; //VK_SHARING_MODE_CONCURRENT 时设置哪些队列将使用 //其他值,忽略这两个参数 uint32_t queueFamilyIndexCount; const uint32_t* pQueueFamilyIndices; //任意时刻图像将会如何使用 //决定了图像以哪种布局创建 VkImageLayout initialLayout; } VkImageCreateInfo;
-
VkImageCreateFlags
- VK_IMAGE_CREATE_SPARSE_BINDING_BIT
- VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT
- VK_IMAGE_CREATE_SPARSE_ALIASED_BIT
- VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT
- 可以为图像创建具有不同格式的视图
- VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
- 可以创建立方纹理视图
-
能够创建的图像的最大尺寸依赖于每个 GPU 设备
*
对比了规范文档,该部分内容书中描述有误,这里更正了- 要获取这个最大尺寸, 可调用
vkGetPhysicalDeviceFeatures
- VkPhysicalDeviceLimits
- maxImageDimension1D
- 不小于 4096
- maxImageDimension2D
- 不小于 4096
- maxImageDimension3D
- 不小于 256
- maxImageArrayLayers
- 阵列图像里的最大层数
- 不小于 256
- maxImageDimensionCube
- 立方体的最大边长
- 不小于 4096
- maxImageDimension1D
- VkPhysicalDeviceLimits
-
VkImageUsageFlags
- VK_IMAGE_USAGE_TRANSFER_SRC_BIT 和 VK_IMAGE_USAGE_TRANSFER_DST_BIT
- 表示图像图像操作的源地址和目标地址
- VK_IMAGE_USAGE_SAMPLED_BIT
- 示图像可以被着色器采样
- VK_IMAGE_USAGE_STORAGE_BIT
- 图像可以作为通用存储,包括用于着色器的写入
- VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
- 像可以绑定一个颜色附件并且用绘制操作写入
- VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
- 可以绑定一个深度或者模板附件,且用作深度或者模板测试
- VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT
- 可以用作临时附件,用于存储绘制操作的中间结果
- 一种特殊的图像
- 可以用作临时附件,用于存储绘制操作的中间结果
- VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT
- 可用作渲染过程中的特殊输入
- 输入图像和常规采样或存储的图像的差异在于
- 片元着色器可以读取它们的像素点
- 输入图像和常规采样或存储的图像的差异在于
- 可用作渲染过程中的特殊输入
- VK_IMAGE_USAGE_TRANSFER_SRC_BIT 和 VK_IMAGE_USAGE_TRANSFER_DST_BIT
-
VkImageLayout
- VK_IMAGE_LAYOUT_UNDEFINED
- 状态未定义, 图像在任何用途中需要转换为另外一个布局
- VK_IMAGE_LAYOUT_GENERAL
- 最不常见
- 当对某使用场景没有其他布局可用时就使用这个布局
- 几乎可以在管线的任何地方使用
- VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
- 将使用图形管线渲染
- VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
- 将用作深度或者模板缓冲区,并作为图形管线的一部分
- VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
- 将用于深度测试,但是不会被图形管线写入
- 可以被着色器读取
- VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
- 图像将被绑定, 以供着色器读取
- 通常在图像被用作纹理时使用
- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
- 图像是复制操作的源
- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
- 图像是复制操作的目标地址
- VK_IMAGE_LAYOUT_PREINITIALIZED
- 图像包含外部对象放置的数据
- 如
- 通过映射潜在内存
- 从主机端写入
- 如
- 图像包含外部对象放置的数据
- VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
- 用作显示的数据源,直接显示给用户
- VK_IMAGE_LAYOUT_UNDEFINED
-
改变图像布局的机制也称为“管线屏障”,或简称“屏障”
-
线性图像
- 各种资源都有两种平铺模式
- VK_IMAGE_TILING_OPTIMAL
- 是一种不透明、实现方式各异的布局,用于提高设备内存子系统对图像的读写效率
- VK_IMAGE_TILING_LINEAR
- 是一种透明的数据布局方式,用于足够直观地排列图像
- 在图像内部,像素以从左到右、从上到下的方式布局
- 可以映射用于备份资源的内存,以允许主机直接读写内存
- VK_IMAGE_TILING_OPTIMAL
- 想让 CPU 访问底层图像数据, 除了图像的宽度、高度、深度和像素格式之外,还有其他
几个信息是必需的- 行间距
- 图像内每一行开始之间的距离
- 阵列间距
- 不同阵列层之间的距离
- 深度间距
- 深度切片之间的距离
- 阵列间距和深度间距分别只适用于阵列与 3D 图像
- 行距只适用于 2D 或 3D 图像
- 行间距
- 图像里不同的子资源的布局可能是不同的,查询可调用
void vkGetImageSubresourceLayout ( VkDevice device, //带查询的图像 VkImage image, //查询的信息 const VkImageSubresource* pSubresource, //写入子资源的布局参数 VkSubresourceLayout* pLayout );
VkImageSubresource
typedef struct VkImageSubresource { //一个或者多个层面 //color 图像为 VK_IMAGE_ASPECT_COLOR_BIT //深度图像为 VK_IMAGE_ASPECT_DEPTH_BIT //模板图像为 VK_IMAGE_ASPECT_STENCIL_BIT //深度-模板图像为 VK_IMAGE_ASPECT_DEPTH_BIT||VK_IMAGE_ASPECT_STENCIL_BIT VkImageAspectFlags aspectMask; uint32_t mipLevel; //阵列的层,默认填0 uint32_t arrayLayer; } VkImageSubresource;
VkSubresourceLayout
typedef struct VkSubresourceLayout { //资源中的偏移量, 子资源的开始位置 VkDeviceSize offset; //消耗的内存区的大小 VkDeviceSize size; //行间距 VkDeviceSize rowPitch; //阵列间距 VkDeviceSize arrayPitch; //深度间距 VkDeviceSize depthPitch; } VkSubresourceLayout;
- 内存布局和图像的个人理解
- 内存布局=容器
- 图像放入容器中
- 但未必铺满
- 因此 rowPitch 一定大于或等于 witdh
- 以此类推
- 各种资源都有两种平铺模式
-
非线性编码
- sRGB 颜色编码,是一种非线性编码
- 它使用了一条伽马曲线来逼近 CRT 编码
- 虽然 CRT 现在已经完全过时,但是 sRGB 编码仍广泛地应用于纹理和图片数据
- 线性空间转 srgb 在 43 页
- sRGB 颜色编码,是一种非线性编码
-
压缩图像的格式
- 当前 Vulkan 中定义的各种压缩图像格式称为 “块状压缩格式”
- 查询支持的压缩格式族
- 可以调用
vkGetPhysicalDeviceProperties()
VkPhysicalDeviceFeatures
textureCompressionBC
为VK_TRUE
,则块状压缩格式- 也称 BC 格式
- BC 格式族
- BC1 对图像以每块 4×4 纹素的方式编码,每一块用一个 64 位的 字段来表示
- VK_FORMAT_BC1_RGB_UNORM_BLOCK
- VK_FORMAT_BC1_RGB_SRGB_BLOCK
- VK_FORMAT_BC1_RGBA_UNORM_BLOCK
- VK_FORMAT_BC1_RGBA_SRGB_BLOCK
- BC2 对图像以每块 4×4 纹素的方式编码,每一个块用 128 位表示、包括 a 通道
- VK_FORMAT_BC2_UNORM_BLOCK
- VK_FORMAT_BC2_SRGB_BLOCK
- BC3同样是把纹素编码到 4×4 的块中,每一个块占用 128 位的存储空间。前 64 位存储了压缩的 alpha 值,允许连续的 alpha 值比 BC2 有更高的精度。后 64 位存储了压缩的颜色数据,和 BC1 类似
- VK_FORMAT_BC3_UNORM_BLOCK
- VK_FORMAT_BC3_SRGB_BLOCK
- BC4表示 单通道格式,同样把纹素编码到 4×4 的块中,每一个块占用 64 位的存储空间
- VK_FORMAT_BC4_UNORM_BLOCK
- VK_FORMAT_BC4_SRGB_BLOCK
- BC5 族是双通道格式,每个 4×4 块实质上都由两个相邻的 BC4 块组成
- VK_FORMAT_BC5_UNORM_BLOCK
- VK_FORMAT_BC5_SRGB_BLOCK
- BC6 格式分别是有符号与无符号的浮点压缩格式。每一个 4×4 的 RGB 纹素块存储在 128 位的数据里
- VK_FORMAT_BC6H_SFLOAT_BLOCK
- VK_FORMAT_BC6H_UFLOAT_BLOCK
- BC7 VK_FORMAT_BC7_UNORM_BLOCK 和 VK_FORMAT_BC7_SRGB_BLOCK 是 4 通道格式,每一个 4×4 的 RGBA 纹素数据块存储在一个 128 位的分量里
- VK_FORMAT_BC7_UNORM_BLOCK
- VK_FORMAT_BC7_SRGB_BLOCK
- BC1 对图像以每块 4×4 纹素的方式编码,每一块用一个 64 位的 字段来表示
textureCompressionETC2
为VK_TRUE
,则支持 ETC 格式 (包含 ETC2、EAC)- ETC2
- VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK 和 VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK
- 无符号格式,每一个 4×4 的 RGB 纹素块被打包到压缩的 64 位数据中
- VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK 和 VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK
- 无符号格式,每一个 4×4 的 RGB 纹素块以及每纹素占一位的 alpha 数据被打包到压缩的 64 位数据中
- VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK 和 VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK
- 每一个 4×4 的纹素块通过 128 位的数据表示。每纹素都有 4 个通道
- VK_FORMAT_EAC_R11_UNORM_BLOCK 和 VK_FORMAT_EAC_R11_SNORM_BLOCK
- 无符号和有符号单通道格式,每一个 4×4 的纹素块都通过 64 位的数据表示
- VK_FORMAT_EAC_R11G11_UNORM_BLOCK 和 VK_FORMAT_EAC_R11G11_SNORM_BLOCK
- 无符号和带符号双通道格式,每一个 4×4 的纹素块都通过 64 位数据表示
- VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK 和 VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK
textureCompressionASTC_LDR
为VK_TRUE
,则支持 ASTC 格式- ASTC
- 每块占用的位数总是 128
- 且所有 ASTC 格式都有 4 个通道
- 每个压缩块的纹素个数是变动的
- 支持 4×4、5×4、5×5、6×5、6×6、8×5、8×6、8×8、10×5、10×6、10×8、10×10、12×10 和 12×12 的压缩块尺寸
- ASTC 格式的符号名字是
VK_FORMAT_ASTC_{N}×{M}_{encoding}_BLOCK
格式的{N}
与{M}
代表压缩块的宽度和高度{encoding}
是 UNORM 或 SRGB- 示例
VK_FORMAT_ASTC_8x6_SRGB_BLOCK
- 可以调用
- 对于所有的格式,只有 RGB 通道是非线性编码的, A 通道总是以线性编码存储数据的
- 包括 SRGB
2.2.4 资源视图
- 缓冲区和图像是 Vulkan 支持的两种主要的资源类型
- 缓冲区的视图称为缓冲区视图(buffer view)
- 表示缓冲区对象的一个子区间
- 图像的视图称为图像视图(image view)
- 可以以不同格式展示,或表示为另外一个图像的子资源
- 在创建缓冲区或图像视图之前,需给父对象绑定内存。
- 缓冲区视图
- 因为缓冲区内原生数据被视为连续的纹素,所以这也称为“纹素缓冲区视图”
- 纹素缓冲区视图可以被着色器直接访问
- 如 vertex buffer
- 创建缓冲区视图
VkResult vkCreateBufferView ( VkDevice device, const VkBufferViewCreateInfo* pCreateInfo, //如果不是nullptr 指定的分配回调函数用于为新对象分配任何所需的主机内存 const VkAllocationCallbacks* pAllocator, //新创建的缓冲区视图的句柄 VkBufferView* pView );
VkBufferViewCreateInfo
typedef struct VkBufferViewCreateInfo { //VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO VkStructureType sType; //设置为 nullptr const void* pNext; //保留, 设置为0 VkBufferViewCreateFlags flags; //父缓冲区 VkBuffer buffer; //格式 VkFormat format; //从 offset 字节开始 VkDeviceSize offset; //范围 VkDeviceSize range; } VkBufferViewCreateInfo;
- Vulkan 标准保证
maxTexelBufferElements
至少为 65 536- 一个纹素缓冲区能够存储的纹素的最大数量
- 视图不应超过这个值
- 图像视图
- 不能把图像资源直接作为帧缓冲区的一个附件使用,也不能把图像绑定到一个描述符集
- 创建新的视图
VkResult vkCreateImageView ( VkDevice device, const VkImageViewCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImageView* pView );
VkImageViewCreateInfo
typedef struct VkImageViewCreateInfo { //VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //设置为 0 VkImageViewCreateFlags flags; //父图像 VkImage image; //创建的视图类型 //视图类型必须和父图像类型兼容 VkImageViewType viewType; //新视图的格式 VkFormat format; //重映射,如 BGRA 图像创建 RGBA 视图 VkComponentMapping components; //子图像可以是父图像的一个子集 //子集指定 VkImageSubresourceRange subresourceRange; } VkImageViewCreateInfo;
VkImageViewType
- VK_IMAGE_VIEW_TYPE_1D、VK_IMAGE_VIEW_TYPE_2D 和 VK_IMAGE_VIEW_TYPE_3D
- VK_IMAGE_VIEW_TYPE_CUBE 和 VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
- 立方图与立方图的阵列图像
- VK_IMAGE_VIEW_TYPE_1D_ARRAY 和 VK_IMAGE_VIEW_TYPE_2D_ARRAY
- 1D 与 2D 阵列图像
- 如果一种或者两种格式是块状压缩图像格式,需满足
- 如果两张图像都是压缩格式,那么每块内的位数必须匹配
- 如果只有一张图像是压缩格式而另一张不是,那么压缩图像每个块的位数必须和非压缩图像每个像素的位数一样
VkComponentMapping
typedef struct VkComponentMapping { VkComponentSwizzle r; VkComponentSwizzle g; VkComponentSwizzle b; VkComponentSwizzle a; } VkComponentMapping;
VkComponentSwizzle
- VK_COMPONENT_SWIZZLE_R、VK_COMPONENT_SWIZZLE_G、VK_COMPONENT_SWIZZLE_B 和 VK_COMPONENT_SWIZZLE_A
- VK_COMPONENT_SWIZZLE_ZERO 和 VK_COMPONENT_SWIZZLE_ONE
- VK_COMPONENT_SWIZZLE_IDENTITY
- 表示子视图的数据应当从父图像里对应的通道中读取
VkImageSubresourceRange
typedef struct VkImageSubresourceRange { //指定了哪些层面受屏障影响 VkImageAspectFlags aspectMask; //指定视图从 mip 链的哪个位置开始 //如果父图像并没有 mipmap,设置为 0 uint32_t baseMipLevel; //包含多少个 mip 层级 //如果父图像并没有 mipmap,设置为 1 uint32_t levelCount; //指定起始层 //父图像并不是阵列图像 设置为 0 uint32_t baseArrayLayer; //指定起始层与层数 //父图像并不是阵列图像 设置为 1 uint32_t layerCount; } VkImageSubresourceRange;
- 层面
- 个人理解就是一张贴图存储了不同类型的数据,每种类型数据称为一个层面
VkImageAspectFlagBits
- VK_IMAGE_ASPECT_COLOR_BIT
- 图像的颜色部分
- VK_IMAGE_ASPECT_DEPTH_BIT
- 深度-模板图像的深度层面
- VK_IMAGE_ASPECT_STENCIL_BIT
- 深度-模板图像的模板层面
- VK_IMAGE_ASPECT_METADATA_BIT
- 和图像关联的额外信,可能用于跟踪它的状态,并且用于各种压缩技术
- VK_IMAGE_ASPECT_COLOR_BIT
- 图像阵列
- layerCount 大于 1 的图像称作阵列图像
- 在 2D 纹理的 y 方向上和 3D 纹理的 z 方向上可以进行线性过滤,而在一个阵列图像的多个层上不能执行过滤
- 而且大多数 Vulkan 实现不允许创建字段 arrayLayers 大于 1 的 3D 图像
- 除了图像阵列之外,立方纹理是一种特殊的图像,它允许把阵列图像的 6 个层解释为立方体的各个侧面
- 缓冲区视图
2.2.5 销毁资源
- 销毁缓冲区资源
void vkDestroyBuffer ( VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* pAllocator );
- 如果使用了主机内存分配器创建缓冲区对象,那么
pAllocator
就应该指向兼容的内存分配器; - 否则
pAllocator
应设置为nullptr
- 如果使用了主机内存分配器创建缓冲区对象,那么
- 销毁缓冲区视图
void vkDestroyBufferView ( VkDevice device, VkBufferView bufferView, const VkAllocationCallbacks* pAllocator );
- 图像销毁之前必须销毁其视图
- 销毁图像
void vkDestroyImage ( VkDevice device, VkImage image, const VkAllocationCallbacks* pAllocator );
- 销毁图像视图
void vkDestroyImageView ( VkDevice device, VkImageView imageView, const VkAllocationCallbacks* pAllocator );
2.3 设备内存管理
- Vulkan 系统有 4 个类别的内存
- 可以被 GPU 访问的内存称为设备内存(device memory)
- 主机内存又称为系统内存,是可以通过 malloc 和 new 操作获取的普通内存
2.3.1 分配设备内存
- 设备内存分配
VkResult vkAllocateMemory ( VkDevice device, //描述新分配的内存对象 const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, //指向新分配的内存 VkDeviceMemory* pMemory );
VkMemoryAllocateInfo
typedef struct VkMemoryAllocateInfo { //VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO VkStructureType sType; //nullptr const void* pNext; //指定需要分配的内存的大小 VkDeviceSize allocationSize; //内存的类型 uint32_t memoryTypeIndex; } VkMemoryAllocateInfo;
- 释放
void vkFreeMemory ( VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* pAllocator );
- 在某些平台上,单个进程内也许有内存分配次数的上限
maxMemoryAllocationCoun
- Vulkan 标准保证的最小值 4096
- 内存类型设置为 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT
- 延迟分配
- 内存是否已经在物理设备上分配
void vkGetDeviceMemoryCommitment ( VkDevice device, VkDeviceMemory memory, //返回实际为这个内存对象分配的字节数 VkDeviceSize* pCommittedMemoryInBytes );
2.3.2 CPU 访问设备内存
- 纯设备内存只能被设备访问
- 内存映射
- 让 Vulkan 返回一个指向从主机可访问区域分配的内存的指针
- 为了把设备内存映射到主机的地址空间,需要映射的内存对象必须从堆属性含有 VK_MEMORY_ PROPERTY_HOST_VISIBLE_BIT 标志位的堆中分配
- 映射内存来获取一个主机可用的指针
VkResult vkMapMemory ( VkDevice device, //内存对象 //一定要在外部同步访问这个内存对象 VkDeviceMemory memory, //映射一个内存对象的一部分确定起始位置 //映射整个内存对象为 0 VkDeviceSize offset, //指定区域的大小 //映射整个内存对象为 VK_WHOLE_SIZE VkDeviceSize size, //设置为 0 VkMemoryMapFlags flags, //返回映射区域的指针 void** ppData );
- Vulkan 保证,当从
vkMapMemory()
返回的指针减去offset
时,所得数值是设备内存映射的最小对齐值的倍数minMemoryMapAlignment
- 至少是 64 字节的
- Vulkan 保证,当从
- 解除映射
void vkUnmapMemory ( VkDevice device, VkDeviceMemory memory );
- 内存类型属性
- VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
- 缓存之间会自动保持一致
- 没设置
- 就需要显式地刷新缓存或者使缓存无效
- VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
- 刷新可能包含等待写操作的主机缓存
VkResult vkFlushMappedMemoryRanges ( VkDevice device, //需要刷新的区域大小 uint32_t memoryRangeCount, //范围的信息 const VkMappedMemoryRange* pMemoryRanges );
VkMappedMemoryRange
typedef struct VkMappedMemoryRange { //VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE VkStructureType sType; //nullptr const void* pNext; //被映射的内存对象 VkDeviceMemory memory; //映射区间 //刷新一个内存对象的任何映射区 //把 offset 设置为 0,把 size 设置为 VK_WHOLE_SIZE 即可 VkDeviceSize offset; VkDeviceSize size; } VkMappedMemoryRange;
- 如果主机向映射内存区域写入了数据而且需要设备看到写入的效果,刷新是必需的
- 果设备写入内存映射区域且需要主机能够看到写入的信息,就需要在主机端主动地使任何缓存无
效VkResult vkInvalidateMappedMemoryRanges ( VkDevice device, uint32_t memoryRangeCount, const VkMappedMemoryRange* pMemoryRanges );
vkFlushMappedMemoryRanges()
和vkInvalidateMappedMemoryRanges()
只会影响到缓存与主机访问的一致性,而不会影响到设备
2.3.3 绑定内存到资源上
- Vulkan 使用诸如缓冲区、图像这样的资源存储数据之前,内存必须绑定给它们
- 用什么类型的内存,以及资源需要多少内存
- 缓冲区
void vkGetBufferMemoryRequirements ( VkDevice device, VkBuffer buffer, //内存要求的信息 VkMemoryRequirements* pMemoryRequirements );
- 纹理
void vkGetImageMemoryRequirements ( VkDevice device, VkImage image, //内存要求的信息 VkMemoryRequirements* pMemoryRequirements );
- 缓冲区
VkMemoryRequirements
typedef struct VkMemoryRequirements { //资源所需内存量 VkDeviceSize size; //内存对齐的信息 VkDeviceSize alignment; //资源所能够绑定的所有内存类型 uint32_t memoryTypeBits; } VkMemoryRequirements;
- 图像选择一种内存类型示例
- Page 77
- 绑定内存到资源
- 缓冲区
VkResult vkBindBufferMemory ( VkDevice device, VkBuffer buffer, VkDeviceMemory memory, //内存对象中资源存在的位置 VkDeviceSize memoryOffset );
- 纹理
VkResult vkBindImageMemory ( VkDevice device, VkImage image, VkDeviceMemory memory, //内存对象中资源存在的位置 VkDeviceSize memoryOffset );
- 缓冲区
- 通过
vkBindBufferMemory()
和vkBindImageMemory()
分别对缓冲区与图像进行访问,必须在外部保持同步
2.3.4 稀疏资源
- 稀疏资源是一种特殊的资源,可以在内存中只存储一部分,可以在创建以后更改内存存储,甚至在应用程序使用过以后也能改变
- 稀疏资源在使用前也必须要绑定到内存,即使这个绑定可以改变
- 创建稀疏图像
- VkImageCreateInfo
- flags 设置为 VK_IMAGE_CREATE_SPARSE_BINDING_BIT
- VkImageCreateInfo
- 创建稀疏缓冲区
- VkBufferCreateInfo
- flags 设置为 VK_BUFFER_CREATE_SPARSE_BINDING_BIT
- VkBufferCreateInfo
- 稀疏图像查询图像所需要的附加条件
void vkGetImageSparseMemoryRequirements ( VkDevice device, VkImage image, uint32_t* pSparseMemoryRequirementCount, //填充该图像的限制条件 VkSparseImageMemoryRequirements* pSparseMemoryRequirements );
VkSparseImageMemoryRequirements
typedef struct VkSparseImageMemoryRequirements { VkSparseImageFormatProperties formatProperties; //指定的层级开始 uint32_t imageMipTailFirstLod; //tail 的大小 VkDeviceSize imageMipTailSize; //指定的位置开始 VkDeviceSize imageMipTailOffset; //距离 VkDeviceSize imageMipTailStride; } VkSparseImageMemoryRequirements;
VkSparseImageFormatProperties
typedef struct VkSparseImageFormatProperties { //属性将要应用到哪个图像层面 VkImageAspectFlags aspectMask; //当把内存绑定到稀疏图像上时,它是绑定到多个块上的,而不是一次绑定到整个资源上的 //内存必须在特定大小的块中进行绑定 VkExtent3D imageGranularity; //图像的更多行为 VkSparseImageFormatFlags flags; } VkSparseImageFormatProperties;
VkSparseImageFormatFlags
- VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT
- 图像是个阵列, 那么 mip 尾部共享所有阵列层共享的绑定
- 如果没有该标志位
- 每一个阵列层都有自己的 mip 尾部,该尾部可以绑定到其他独立的内存上
- VK_SPARSE_IMAGE_FORMAT_ALIGNED_MIP_SIZE_BIT
- 标志着从 mip 尾部起始的第一个层级不是图像绑定粒度的倍数
- 如果没有该标志位
- 从尾部起始的第一个层级要比图像的绑定粒度小
- VK_SPARSE_IMAGE_FORMAT_NONSTANDARD_BLOCK_SIZE_BIT
- 图像的格式支持稀疏绑定,但是块的大小和标准的块并不相同
- 稀疏纹理的块尺寸表格
- Page 80
- VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT
- 获知稀疏图像特定格式的属性
void vkGetPhysicalDeviceSparseImageFormatProperties ( VkPhysicalDevice physicalDevice, VkFormat format, VkImageType type, VkSampleCountFlagBits samples, //用途 VkImageUsageFlags usage, //平铺模式 VkImageTiling tiling, uint32_t* pPropertyCount, VkSparseImageFormatProperties* pProperties );
- 稀疏图像属性是物理设备的功能
- 因为用来存储稀疏图像的内存绑定不能更改,所以即使图像使用完之后,对该图像中绑定属性的更新也需要与那个任务一起放入管线中
- 内存绑定到稀疏资源的操作在队列中执行
- 绑定内存到稀疏资源
VkResult vkQueueBindSparse ( VkQueue queue, uint32_t bindInfoCount, const VkBindSparseInfo* pBindInfo, VkFence fence );
VkBindSparseInfo
typedef struct VkBindSparseInfo { //VK_STRUCTURE_TYPE_BIND_SPARSE_INFO VkStructureType sType; //nullptr const void* pNext; //需要等待的信号量的个数 uint32_t waitSemaphoreCount; //需要等待的信号量的句柄数组的指针 const VkSemaphore* pWaitSemaphores; //缓冲区绑定更新的数量 uint32_t bufferBindCount; //缓冲区对象绑定操作 const VkSparseBufferMemoryBindInfo* pBufferBinds; uint32_t imageOpaqueBindCount; //定义不透明内存绑定 //图像资源使用同一个结构体 VkSparseMemoryBind 来影响直接绑定到图像的内存称不透明图像内存绑定 const VkSparseImageOpaqueMemoryBindInfo* pImageOpaqueBinds; uint32_t imageBindCount; //绑定内存到一个显式的图像区域来执行不透明的图像内存绑定。 const VkSparseImageMemoryBindInfo* pImageBinds; //需要发送的信号量个数 uint32_t signalSemaphoreCount; //需要发送的信号量的句柄数组的指针 const VkSemaphore* pSignalSemaphores; } VkBindSparseInfo;
VkSparseBufferMemoryBindInfo
typedef struct VkSparseBufferMemoryBindInfo { VkBuffer buffer; //内存区域的大小 uint32_t bindCount; const VkSparseMemoryBind* pBinds; } VkSparseBufferMemoryBindInfo;
VkSparseMemoryBind
typedef struct VkSparseMemoryBind { //资源内存块的偏移量 VkDeviceSize resourceOffset; //内存块的尺寸 VkDeviceSize size; //作为绑定的存储源的内存对象 VkDeviceMemory memory; //内存对象内存块的偏移量 VkDeviceSize memoryOffset; //包含用来控制绑定过程的额外信息 //缓冲区资源来说,无须使用标志位 // VkSparseMemoryBindFlags flags; } VkSparseMemoryBind;
VkSparseImageOpaqueMemoryBindInfo
typedef struct VkSparseImageOpaqueMemoryBindInfo { VkImage image; uint32_t bindCount; const VkSparseMemoryBind* pBinds; } VkSparseImageOpaqueMemoryBindInfo;
- VkSparseImageMemoryBindInfo
typedef struct VkSparseImageMemoryBindInfo { VkImage image; uint32_t bindCount; const VkSparseImageMemoryBind* pBinds; } VkSparseImageMemoryBindInfo;
VkSparseImageMemoryBind
typedef struct VkSparseImageMemoryBind { VkImageSubresource subresource; //要绑定图像纹素区域的偏移量 VkOffset3D offset; //要绑定图像纹素区域的大小 VkExtent3D extent; //供绑定内存的内存对象 VkDeviceMemory memory; //内存中的偏移量 VkDeviceSize memoryOffset; VkSparseMemoryBindFlags flags; } VkSparseImageMemoryBind;
VkImageSubresource
typedef struct VkImageSubresource { VkImageAspectFlags aspectMask; uint32_t mipLevel; //于非阵列图像, 应设置为 0 uint32_t arrayLayer; } VkImageSubresource;