Vulkan 围炉夜话1:https://developer.aliyun.com/article/1598108
vkAllocateMemory —— 分配设备内存
VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory( VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory);
- VkMemoryAllocateInfo
typedef struct VkMemoryAllocateInfo { VkStructureType sType; const void* pNext; // 这个参数表示需要分配的内存大小 VkDeviceSize allocationSize; // 这个参数设置了内存类型的索引号,可以用来设置内存所在的堆以及内存类型 uint32_t memoryTypeIndex; } VkMemoryAllocateInfo;
vkBindImageMemory —— 内存绑定图像资源
vkBindImageMemory 函数把分配的内存 VkDeviceMemory 绑定给 VkImage 使用
VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory( VkDevice device, // 设置了需要绑定到内存的 VkImage 对象 VkImage image, // 设置了分配的 VkDeviceMemory 内存对象 VkDeviceMemory memory, // 设置了图像绑定到内存的起点偏移地址,按字节数计算 VkDeviceSize memoryOffset);
vkCreateImageView—— 创建图像资源视图
主机端要使用 VkImageView 来操作 VkImage,使用 vkCreateImageView 对两者进行关联
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView( VkDevice device, const VkImageViewCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImageView* pView);
- VkImageViewCreateInfo
typedef struct VkImageViewCreateInfo { VkStructureType sType; const void* pNext; VkImageViewCreateFlags flags; VkImage image; VkImageViewType viewType; VkFormat format; VkComponentMapping components; VkImageSubresourceRange subresourceRange; } VkImageViewCreateInfo;
- VkImageViewCreateFlagBits
typedef enum VkImageViewCreateFlagBits { VK_IMAGE_VIEW_CREATE_FRAGMENT_DENSITY_MAP_DYNAMIC_BIT_EXT = 0x00000001, VK_IMAGE_VIEW_CREATE_FRAGMENT_DENSITY_MAP_DEFERRED_BIT_EXT = 0x00000002, VK_IMAGE_VIEW_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkImageViewCreateFlagBits; typedef VkFlags VkImageViewCreateFlags; typedef VkFlags VkShaderModuleCreateFlags;
- VkImageViewType
typedef enum VkImageViewType { VK_IMAGE_VIEW_TYPE_1D = 0, VK_IMAGE_VIEW_TYPE_2D = 1, VK_IMAGE_VIEW_TYPE_3D = 2, VK_IMAGE_VIEW_TYPE_CUBE = 3, VK_IMAGE_VIEW_TYPE_1D_ARRAY = 4, VK_IMAGE_VIEW_TYPE_2D_ARRAY = 5, VK_IMAGE_VIEW_TYPE_CUBE_ARRAY = 6, VK_IMAGE_VIEW_TYPE_MAX_ENUM = 0x7FFFFFFF } VkImageViewType;
- VkComponentMapping
typedef struct VkComponentMapping { VkComponentSwizzle r; VkComponentSwizzle g; VkComponentSwizzle b; VkComponentSwizzle a; } VkComponentMapping;
- VkImageSubresourceRange
typedef struct VkImageSubresourceRange { VkImageAspectFlags aspectMask; uint32_t baseMipLevel; uint32_t levelCount; uint32_t baseArrayLayer; uint32_t layerCount; } VkImageSubresourceRange; typedef enum VkImageAspectFlagBits { VK_IMAGE_ASPECT_COLOR_BIT = 0x00000001, VK_IMAGE_ASPECT_DEPTH_BIT = 0x00000002, VK_IMAGE_ASPECT_STENCIL_BIT = 0x00000004, VK_IMAGE_ASPECT_METADATA_BIT = 0x00000008, ... VK_IMAGE_ASPECT_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkImageAspectFlagBits; typedef VkFlags VkImageAspectFlags;
VkBuffer 相关
完整的缓存资源创建工作流如下图:
vkCreateBuffer—— 创建缓存资源
VKAPI_ATTR VkResult VKAPI_CALL vkCreateBuffer( VkDevice device, const VkBufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBuffer* pBuffer);
- VkBufferCreateInfo
typedef struct VkBufferCreateInfo { VkStructureType sType; const void* pNext; VkBufferCreateFlags flags; VkDeviceSize size; // 创建的缓存总大小,单位字节 VkBufferUsageFlags usage; // 缓存资源用途 VkSharingMode sharingMode; // 图像在多个队列族之间的共享模式 uint32_t queueFamilyIndexCount; // 数组 pQueueFamilyIndices 的元素个数 const uint32_t* pQueueFamilyIndices; // 准备访问缓存的队列族的数组 } VkBufferCreateInfo;
- VkBufferCreateFlags
typedef enum VkBufferCreateFlagBits { VK_BUFFER_CREATE_SPARSE_BINDING_BIT = 0x00000001, VK_BUFFER_CREATE_SPARSE_RESIDENCY_BIT = 0x00000002, VK_BUFFER_CREATE_SPARSE_ALIASED_BIT = 0x00000004, VK_BUFFER_CREATE_PROTECTED_BIT = 0x00000008, VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT = 0x00000010, VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT_EXT = VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT, VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT_KHR = VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT, VK_BUFFER_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkBufferCreateFlagBits; typedef VkFlags VkBufferCreateFlags;
- VkBufferUsageFlagBits
typedef enum VkBufferUsageFlagBits { VK_BUFFER_USAGE_TRANSFER_SRC_BIT = 0x00000001, VK_BUFFER_USAGE_TRANSFER_DST_BIT = 0x00000002, VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT = 0x00000004, VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT = 0x00000008, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT = 0x00000010, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT = 0x00000020, VK_BUFFER_USAGE_INDEX_BUFFER_BIT = 0x00000040, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT = 0x00000080, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT = 0x00000100, VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT = 0x00020000, ... VK_BUFFER_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkBufferUsageFlagBits; typedef VkFlags VkBufferUsageFlags; typedef VkFlags VkBufferViewCreateFlags;
示例
VkBufferCreateInfo bufInfo = {}; bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufInfo.pNext = NULL; bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; bufInfo.size = sizeof(MVP); bufInfo.queueFamilyIndexCount = 0; bufInfo.pQueueFamilyIndices = NULL; bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; bufInfo.flags = 0; // Use create buffer info and create the buffer objects result = vkCreateBuffer(deviceObj->device, &bufInfo, NULL, &UniformData.buffer);
vkBindBufferMemory —— 内存绑定缓存资源
vkBindBufferMemory 函数把分配的内存 VkDeviceMemory 绑定给 VkBuffer 使用
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory( VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize memoryOffset);
vkCreateBufferView —— 创建缓存资源视图
可选择使用 vkCreateBufferView 对 VkBuffer 和 VkBufferView 进行关联
VKAPI_ATTR VkResult VKAPI_CALL vkCreateBufferView( VkDevice device, const VkBufferViewCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBufferView* pView);
- VkBufferViewCreateInfo
typedef struct VkBufferViewCreateInfo { VkStructureType sType; const void* pNext; VkBufferViewCreateFlags flags; VkBuffer buffer; VkFormat format; // 如果我们将颜色、深度、模板转换为颜色分量,这个参数用来设置重映射的偏移量 VkDeviceSize offset; // 用来选择一个 mipmap 和纹理数组的层级范围,确保它们对视图可见 VkDeviceSize range; } VkBufferViewCreateInfo;
VkDeviceMemory 映射
vkMapMemory —— 映射到主机空间
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory( VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size, VkMemoryMapFlags flags, void** ppData);
vkUnmapMemory —— 解除映射
VKAPI_ATTR void VKAPI_CALL vkUnmapMemory( VkDevice device, VkDeviceMemory memory);
理解代码流线
我们现在详细地理解一下之前的实现过程。首先创建一个 VkCreateBufferInfo 结构体,用顶点缓存的元数据填充它的内容。我们将缓存的用途类型保存在这里,用来设置顶点的信息(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)。其他用途类型可以是针对索引缓存、uniform 缓存、纹理缓存,等等。我们需要设置顶点缓存的大小(按字节)。因为并没有设置针对多队列的需求,我们可以设置共享模式为 VK_SHARING_MODE_EXCLUSIVE 。将结构体传递给 API 函数 vkCreateBuffer() ,创建顶点缓存对象(VertexBuffer::buf)。
我们将创建的缓存对象(VertexBuffer::buf)作为参数传递给函数vkGetBufferMemoryRequirements() ,从而获取缓存分配的内存需求信息(memRqrmnt)。这个信息可以帮助系统按照合适的内存大小来分配缓存资源所需的空间。这个函数还需要用到结构体 VkCreateBufferInfo 。
下一步,我们准备进行空间分配,根据之前的需求信息创建一个 VkMemoryAllocateInfo 对象(allocinfo)。我们设置分配的字节大小并且获取兼容的内存类型。内存的分配需要用到 vkAllocateMemory 函数,输入参数为 allocinfo,获得的设备内存类型为 VkDeviceMemory,保存在 VertexBuffer.mem 变量中。
完成物理内存的分配之后,使用 vkMapMemory() 将它映射到宿主机本地内存,这样我们就可以直接将几何体数据复制到物理设备内存当中。我们将数据复制到物理设备内存之后,别忘了使用 vkUnmapMemory() 执行解除映射的操作。
最后我们通过 vkBindBufferMemory() 函数将设备内存(VkDeviceMemory)和缓存对象(VkBuffer)相互绑定在一起。
渲染通道
理解渲染通道
渲染通道(Render Pass)设置了准备在渲染时用到的帧缓存附件和子通道。附件,例如颜色和深度,设置了当前颜色和深度图像的数量。它设置了渲染过程中每幅图像所需使用的采样位数,以及图像内容的使用方法。它还设置了每个渲染通道实例的开始和结束部分如何处理图像数据。在指令缓存中使用的渲染通道被称作渲染通道实例。它负责管理子通道之间的依赖关系,定义附件与子通道之间的协议关系。
渲染通道中主要包括两大部分:附件和子通道。以下针对附件和子通道进行进一步的讲解。
附件
附件表示执行渲染指令时用到的一块表面区域(例如颜色、深度/模板,或者执行解析操作的解析附件)。附件的类型主要有以下五种:
颜色附件(color attachment):颜色附件表示渲染图元数据时用到的绘制目标图像。
深度附件(depth attachment):深度附件负责保存深度信息,并执行深度/模板测试操作。
解析附件(resolve attachment):解析附件在子通道的末尾会自动从一个多重采样的附件降维到一个单一采样的附件。解析附件与多重采用的颜色附件对应,并且在子通道的末尾从颜色附件转换到对应的解析附件,即 vkCmdResolveImage。有一个例外情况是,有时候驱动程序可能会采用更优化的工作方式,例如同时执行溢出和解析操作。
输入附件(input attachment):这里包含了一组附件并与着色器对象共享使用。输入附件有点类似一个严格定义的纹理,而着色器中执行的唯一操作就是纹素的获取(texture(tex,uv))——即按照着色器当前像素位置去读取相应的纹素数据。这里有一个理所当然的应用就是在经典的延迟渲染过程中,从 G-buffer 读取数据并进行后处理的滤波算法(无模糊等),以及光照处理等操作。
保留附件(preserve attachment):在一个给定的子通道中,保留附件中的内容会始终保持不变。保留附件并不会在其他 API 中使用,它们唯一的作用是暂时保持某些附件对应的内容在当前子通道中不变(之后可能会使用)。对于桌面系统 GPU 来说这个功能可能没有什么用,因为渲染目标是直接写入到内存的。但是,对于其他系统来说,某一部分保存在芯片内存上的附件数据可能需要在某个子通道中被其他附件复用,此时不需要将附件内容重新放回到内存中。
子通道
渲染通道中的子通道负责读取和写入到对应的附件中。当前渲染通道中的子通道的执行可以通过下面的渲染指令来完成:
子通道可以从之前写入的附件(必须是保留附件)读取,然后写入到当前关联的附件。
在渲染通道的实例中,子通道的附件也写入到颜色、深度和模板缓存。
如果要在后续的通道中使用当前的子通道附件,应用程序需要负责确保子通道信息在使用过程中始终有效。
保留附件在子通道的生命周期当中可以始终保持附件内容不变。此时子通道不会影响到被保护的附件的读写操作。换句话说,在子通道的生命周期当中,附件无法被读取或者写入。
创建渲染通道
- vkCreateRenderPass
VKAPI_ATTR VkResult VKAPI_CALL vkCreateRenderPass( VkDevice device, const VkRenderPassCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkRenderPass* pRenderPass);
- VkRenderPassCreateInfo
typedef struct VkRenderPassCreateInfo { VkStructureType sType; const void* pNext; VkRenderPassCreateFlags flags; uint32_t attachmentCount; const VkAttachmentDescription* pAttachments; uint32_t subpassCount; const VkSubpassDescription* pSubpasses; uint32_t dependencyCount; const VkSubpassDependency* pDependencies; } VkRenderPassCreateInfo;
- VkAttachmentDescription
typedef struct VkAttachmentDescription { VkAttachmentDescriptionFlags flags; VkFormat format; VkSampleCountFlagBits samples; VkAttachmentLoadOp loadOp; VkAttachmentStoreOp storeOp; VkAttachmentLoadOp stencilLoadOp; VkAttachmentStoreOp stencilStoreOp; VkImageLayout initialLayout; VkImageLayout finalLayout; } VkAttachmentDescription;
- VkSubpassDescription
typedef struct VkSubpassDescription { VkSubpassDescriptionFlags flags; VkPipelineBindPoint pipelineBindPoint; uint32_t inputAttachmentCount; const VkAttachmentReference* pInputAttachments; uint32_t colorAttachmentCount; const VkAttachmentReference* pColorAttachments; const VkAttachmentReference* pResolveAttachments; const VkAttachmentReference* pDepthStencilAttachment; uint32_t preserveAttachmentCount; const uint32_t* pPreserveAttachments; } VkSubpassDescription;
- VkSubpassDependency
typedef struct VkSubpassDependency { uint32_t srcSubpass; uint32_t dstSubpass; VkPipelineStageFlags srcStageMask; VkPipelineStageFlags dstStageMask; VkAccessFlags srcAccessMask; VkAccessFlags dstAccessMask; VkDependencyFlags dependencyFlags; } VkSubpassDependency;
- VkAttachmentReference
typedef struct VkAttachmentReference { uint32_t attachment; VkImageLayout layout; } VkAttachmentReference;
VkFramebuffer 帧缓存
vkCreateFramebuffer —— 创建帧缓存
VKAPI_ATTR VkResult VKAPI_CALL vkCreateFramebuffer( VkDevice device, const VkFramebufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkFramebuffer* pFramebuffer);
- VkFramebufferCreateInfo
typedef struct VkFramebufferCreateInfo { VkStructureType sType; const void* pNext; VkFramebufferCreateFlags flags; VkRenderPass renderPass; uint32_t attachmentCount; const VkImageView* pAttachments; uint32_t width; uint32_t height; uint32_t layers; } VkFramebufferCreateInfo;
- VkFramebufferCreateFlags
typedef enum VkFramebufferCreateFlagBits { VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT = 0x00000001, VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT_KHR = VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT, VK_FRAMEBUFFER_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkFramebufferCreateFlagBits; typedef VkFlags VkFramebufferCreateFlags;
示例
VkFramebufferCreateInfo fbInfo = {}; fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; fbInfo.pNext = NULL; fbInfo.renderPass = renderPass; fbInfo.attachmentCount = includeDepth ? 2 : 1; fbInfo.pAttachments = attachments; fbInfo.width = width; fbInfo.height = height; fbInfo.layers = 1; uint32_t i; framebuffers.clear(); framebuffers.resize(swapChainObj->scPublicVars.swapchainImageCount); for (i = 0; i < swapChainObj->scPublicVars.swapchainImageCount; i++) { attachments[0] = swapChainObj->scPublicVars.colorBuffer[i].view; result = vkCreateFramebuffer(deviceObj->device, &fbInfo, NULL, &framebuffers.at(i)); assert(result == VK_SUCCESS); }
流水线
流水线指的是由一系列固定阶段组成,数据流输入之后,每一个阶段对数据进行处理之后,将它传递给下一个阶段。最终的成果可以是2D的光栅化之后的绘制图形(图形流水线),也可以是通过某种计算逻辑(计算流水线)完成更新的资源信息(缓存或者图像)。
Vulkan 支持两种类型的流水线——图形流水线和计算流水线。
图形流水线(graphics pipeline):这个流水线会使用指令缓存中的一组 Vulkan 指令,将 2D/3D 场景绘制到 2D 光栅化图像上。
计算流水线(compute pipeline):这个流水线使用指令缓存中的一组 Vulkan 指令,来处理计算性的工作。
整个流水线的执行是从输入装配开始,此时输入的顶点数据会被装配成点、线、或者三角形形式的图元拓扑结构。通过可编程的顶点着色器阶段,输入的顶点数据被转换到剪切空间。几何体通过细分控制着色器和细分计算着色器两个装配阶段进行细分,而几何着色器是唯一一个可以从单一的输入图元产生多个输出图元的阶段。
然后,我们通过图元装配环节获取前面阶段中变换坐标后的所有顶点,并且将它们按照之前输入的指定绘制/图元类型(点、线、三角形)进行排列。如果输入的顶点坐标已经在当前视口区域之外,那么对应的图元会被裁切掉,此时被裁切的片元(视口之外)也会被直接丢弃。
光栅化即变换到屏幕坐标空间的图元(点、线、三角形)转换为离散元素(即片元)的过程。片元会交由下一个阶段进行操作,即片元着色器。片元着色器对每个单独的片元执行运算。这些片元最终会成为帧缓存的一部分,并且会再度经历一系列可能的数据更新操作,比如深度测试、模板测试,以及片元融混。
缓存和图像内存可以放在一个独立的流水线中以 1D/2D/3D 工作组的形式进行处理,即计算流水线。计算流水线对于各种并行计算的需求来说是非常强大的。在计算流水线中可以同时修改(读/写)缓存和图像的内存数据。
流水线中通常包括了三大基本概念:流水线状态对象、流水线缓冲对象、流水线布局。它们可以高效地控制底层流水线的操作:
流水线状态对象(Pipeline State Object,PSO):物理设备或者 GPU 端可以直接在硬件层面执行一些特定类型的操作。这些操作包括光栅化和条件更新,比如融混、深度测试、模板化等。 Vulkan 允许我们通过 PSO 自由控制这些硬件属性设置。其他一些硬件端直接控制的操作还包括根据图元拓扑类型(点、线、三角形)进行给定几何体形状装配、视口的控制,等等。
流水线缓冲对象(Pipeline Cache Object,PCO):流水线缓冲的机制可以帮助我们快速获取合复用之前的流水线。应用程序可以因此更好地避免反复创建相同或者相似的流水线对象。
流水线布局(Pipeline layout):缓存和图像数据都是间接关联到着色器上的,可以通过着色器的资源变量进行访问。资源变量被关联到缓存和图像的视图对象。这些资源变量可以通过描述符和描述符的集合布局来进行管理。在流水线当中,流水线布局可以管理一系列的描述符集合布局。
vkCreatePipelineLayout,vkCreateGraphicsPipelines,vkCreateComputePipelines,vkCreatePipelineCache,vkMergePipelineCaches
创建流水线缓冲对象(PCO) —— vkCreatePipelineCache
VKAPI_ATTR VkResult VKAPI_CALL vkCreatePipelineCache( VkDevice device, const VkPipelineCacheCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkPipelineCache* pPipelineCache);
- VkPipelineCacheCreateInfo
typedef struct VkPipelineCacheCreateInfo { VkStructureType sType; const void* pNext; VkPipelineCacheCreateFlags flags; size_t initialDataSize; const void* pInitialData; } VkPipelineCacheCreateInfo;
合并流水线缓冲 —— vkMergePipelineCaches
VKAPI_ATTR VkResult VKAPI_CALL vkMergePipelineCaches( VkDevice device, VkPipelineCache dstCache, uint32_t srcCacheCount, const VkPipelineCache* pSrcCaches);
从流水线缓冲获取数据 —— vkGetPipelineCacheData
VKAPI_ATTR VkResult VKAPI_CALL vkGetPipelineCacheData( VkDevice device, VkPipelineCache pipelineCache, size_t* pDataSize, void* pData);
Vulkan 围炉夜话3: https://developer.aliyun.com/article/1598111