Resources and Memory
[TOC]
内容
Creating a buffer
Allocating and binding a memory object for a buffer
Setting a buffer memory barrier
Creating a buffer view
Creating an image
Allocating and binding a memory object to an image
Setting an image memory barrier
Creating an image view
Creating a 2D image and view
Creating a layered 2D image with a CUBEMAP view
Mapping, updating, and unmapping host-visible memory
Copying data between buffers
Copying data from a buffer to an image
Copying data from an image to a buffer
Using a staging buffer to update a buffer with a device-local memory bound
Using a staging buffer to update an image with a device-local memory bound
Destroying an image view
Destroying an image
Destroying a buffer view
Freeing a memory object
Destroying a buffer
Vulkan里非常重要的存储数据的资源时buffers和images.buffers存储linear数组数据.Images和OpenGL的textures类似,有1D,2D,3D.Buffers和Images可以用于很多目的:shaders里可以read或者sample数据,或者存储数据.Images可以用于color或者depth/stencil绑定(RT),也就是说可以渲染到其上.Buffer还可以存储顶点数据、indices,parameters(indiret drawing).
重要的是提及的所有usages需要在创建资源时指明(可以一次提供很多).
Vulkan里buffers和images没有自己的storage,需要创建和绑定memory objects.
本节介绍如何使用这些资源、如何申请缓存和绑定、如何上传数据到GPU、如何在资源见进行拷贝.
buffer
创建buffer
buffers能用于很多目的.它们能通过descriptor sets在pipelines中统一uniform buffers、storage buffers、texel buffer等的后备缓冲.它们能作为vertex indices或者attributes的数据源,或者暂存从CPU到GPU移动数据的staging resources.为了这些目的,我们需要创建buffer和指定它的usage.
buffers只能用于创建时指定的usages.
buffers支持的使用方式列表
flag | description |
---|---|
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | specifies that the buffer can be a source of data for copy operations |
VK_BUFFER_USAGE_TRANSFER_DST_BIT | specifies that we can copy data to the buffer |
VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | indicates that the buffer can be used in shaders as a uniform texel buffer |
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | specifies that the buffer can be used in shaders as a storage texel buffer |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | indicates that the buffer can be used in shaders as a source of values for uniform variables |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | indicates that we can store data in the buffer from within shaders |
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | specifies that the buffer can be used as a source of vertex indices during drawing |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | indicates that the buffer can be a source of data for vertex attributes specified during drawing |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | indicates that the buffer can contain data that will be used during indirect drawing |
1 | VkBufferCreateInfo buffer_create_info = { |
之前的VK_SHARING_MODE_EXCLUSIVE(sharingMode)是一个非常重要的参数.通过它我们能指定多families里的queues能否同时访问buffer.Exclusive shaing mode(独占)告诉驱动程序缓冲区只能由一个系列中的队列一次引用.如果想从另一个family提交的commands使用buffer,必须在所有者改变时明确告诉驱动(从一个family改为另一个).这样性能更好但也更麻烦.
我们也可以指明VK_SHARING_MODE_CONCURRENT,这样多families多queues能够同时访问一个buffer,并且不用进行所有者转换,但并发性能可能很低.
创建buffer
1 | VkResult result = vkCreateBuffer( logical_device, &buffer_create_info, |
allocating and binding a memory object for a buffer
Vulkan里buffers和images没有自己的memroy,需要allocate memory object并绑定.
关于内存管理
https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator
https://www.youtube.com/watch?v=rXSdDE7NWmA
过程
1.take VkPhysicalDevice physical_device
2.create VkPhysicalDeviceMemoryProperties physical_device_memory_properties
3.clall vkGetPhysicalDeviceMemoryProperties( physical_device,&physical_device_memory_properties )会存储内存相关参数(heaps的数量,大小,types)
4.take VkDevice logical_device
5.take VkBuffer buffer
6.create VkMemoryRequirements memory_requirements.
7.call vkGetBufferMemoryRequirements(logical_device, buffer,&memory_requirements);
8.create VkDeviceMemory memory_object = VK_NULL_HANDLE;
9.create VkMemoryPropertyFlagBits memory_properties
10.遍历physical device的physical_device_memory_properties的内存types,每次循环进行如下操作:
1.确保memory_requirements.memoryTypeBits 设置了
2.确保memory_properties变量的位设置与memory type的propertyflags成员相同,该成员位于物理_device_memory_properties变量中memorytypes数组的索引类型.
3.如果1、2为false,continue
4.创建VkMemoryAllocateInfo buffer_memory_allocate_info
.allocationSize = memory_requirements.size,
.memoryTypeIndex = type
5.vkAllocateMemory( logical_device,&buffer_memory_allocate_info, nullptr, &memory_object)
6.确认结果为VK_SUCCESS
11.确保allocate的memory object成功
12.绑定,call vkBindBufferMemory(logical_device, buffer, memory_object, 0 )
13.确保call成功
为了给buffer allocate a memory,需要知道可用的memory types(physical device)以及有多少.
1 | VkPhysicalDeviceMemoryProperties physical_device_memory_properties; |
接下来,我们需要知道给定缓冲区需要多少存储(缓冲区的内存可能需要大于缓冲区的大小),以及与之兼容的内存类型.这些信息存储在VkMemoryRequirements:
1 | VkMemoryRequirements memory_requirements; |
接下来,我们需要检查哪个内存类型对应于缓冲区的内存需求:
1 | memory_object = VK_NULL_HANDLE; |
这里遍历所有可用memory types以检查是否能够用于我们的buffer,我们也能检查一些额外的属性,比如如果想直接从CPU上传数据到GPU,memory mapping必须支持.这种情况下,我们需要使用的memory type为host-visible.
当我们找到合适的memory type后,我们能用它allocate memory object并break,然后可以绑定了:
1 | if( VK_NULL_HANDLE == memory_object ) { |
绑定时,指定了offset等其他参数.这个参数在内存管理里很有用.
通常来说,不会为每个buffer使用一个分开的memory object.而是allocate很大的memory objects,多个buffers各使用其一部分.在这种方式中,我们通过call vkGetPhysicalDeviceMemoryProperties来获取物理设备的可用内存类型.但通常来说,为了提高app的性能,不会每次需要allocate memory object的时候都调用它.我们只需要调用一次,在我们选择一个physical device后可以使用存储的参数.
set a buffer memory barrier
我们必须通知一个驱动程序每一个这样的使用,不仅在缓冲区创建期间,而且在预期的使用之前.当我们出于一个目的使用缓冲区,并且从现在开始我们想以不同的方式使用它时,我们必须告诉驱动程序缓冲区的使用发生了变化.这是通过缓冲存储器屏障(barrier)实现的.在cb record时,它们作为pipeline barriers的一部分.
自定义一个结构体
1 | struct BufferTransition { |
在Vulkan里,提交给queues的opoerations是按顺序执行的,但也是独立的.有时有些操作需要等待其他操作结束后才能执行,这时候memory barriers就有用了.
memory barriers用于定义命令缓冲区执行中的时刻,在这些时刻中,后面的命令应该等待前面的命令完成它们的工作.它们还使这些操作的结果对其他操作可见。
image
使用目的
type | description |
---|---|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | specifies that the image can be used as a source of data for copy operations |
VK_IMAGE_USAGE_TRANSFER_DST_BIT | specifies that we can copy data to the image |
VK_IMAGE_USAGE_SAMPLED_BIT | indicates that we can sample data from the image inside shaders |
VK_IMAGE_USAGE_STORAGE_BIT | specifies that the image can be used as a storage image inside shaders |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | specifies that we can render into an image (use it as a color render target/attachment in a framebuffer) |
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | indicates that the image can be used as a depth and/or stencil buffer (as a depth render target/attachment |
in a framebuffer) |
| VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | indicates that the memory bound to the image will be allocated lazily (on demand) |
| VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT | specifies that the image can be used as an input attachment inside shaders |
不同的usage情景要求使用不同的image layout,通过使用image memory barriers进行改变,我们需需要指明VK_IMAGE_LAYOUT_UNDEFINED,如果不在意初始化内容,或者VK_IMAGE_LAYOUT_PREINITIALIZED如果想通过mapping host-visible memory上传数据.在使用前总需要transition to another layout.
1 | VkImageCreateInfo image_create_info = { |
创建Image时需要指明tiling
- linear tiling:在memory中线性存储,这允许我们映射图像的内存并直接从应用程序读取或初始化它,因为我们知道内存是如何组织的.但对使用目的有严格显示,比如不能作为depth texture或cubemap.会降低性能
- optimal tiling:能用于所有目的,性能更好,作为代价不知道image得memory如何组织.
不同硬件存储image数据方式不同.所以不能在app中直接mapping、初始化、读image的内存.此时,可以使用staging resources.
1 | VkResult result = vkCreateImage( logical_device, &image_create_info, |
allocating and binding a memory object to an image
vkGetImageMemoryRequirements
vkBindImageMemory
先检查可用的memory types.
1 | VkPhysicalDeviceMemoryProperties physical_device_memory_properties; |
然后给image请求指定的memory requirememts.每个image可能不同,与format,size,mipmaps和layers的数量和其他属性有关.
1 | VkMemoryRequirements memory_requirements; |
下一步是找到与image的memory requirements适配的memory type
1 | memory_object = VK_NULL_HANDLE; |
1 | if( VK_NULL_HANDLE == memory_object ) { |
申请大内存共享,可以提高性能,可以减少memory浪费.
setting an image memory barrier
image 用于texture,RT(通过descriptor sets),swapchain的images.拷贝(目标或源)
定义一个结构体
1 | struct ImageTransition { |
在我们不想改变ownerships时我们能使用VK_QUEUE_FAMILY_IGNORED.
内存屏障用于定义命令缓冲区执行中的时刻,在这些时刻中,后面的命令应该等待前面的命令完成其任务.它们还使这些操作的结果对其他操作可见.
Barriers用于让后续commands 的memory操作可见
为了性能,最好给特定的usages用image memory layout,尽管需要注意频繁转换用处的layout.
VkImageMemoryBarrier
1 | std::vector<VkImageMemoryBarrier> image_memory_barriers; |
需要定义pipeline stages
左侧vertex等待fragment结束
右侧fragment等待vertex结束,减少barrier数量很重要,如果需要以正确设置绘图命令并为屏障选择适当的pipeline stages
1 | if( image_memory_barriers.size() > 0 ) { |
如果多次使用于同样的目的,不需要重复设置barrier.这个设置是在改变时设置,而非usage.
create an image view
Images在Vulkan Commands中被直接使用,Framebuffer和shaders(通过descriptor sets)通过image views访问images.Images view定义了image的内存的选定部分和指明了读取image数据的额外的信息.
VkImageViewCreateInfo
VkImageView
image view定义了用于访问image的额外的数据,通过它我们能指明commands能够访问的image的部分.比如,如果渲染到image,可以指明就一个mipmap level需要更新.
Image view也定义了image内存如何解释.一个好的例子是multiple layers.对于它,我们可以定义一个image view来解释image,作为一个layered image,或者我们可以使用image view从中创建一个cubemap映射。
1 | VkImageViewCreateInfo image_view_create_info = { |
vkCreateImageView
1 | VkResult result = vkCreateImageView( logical_device, |
create a 2D image and view
RGBA 32bits 2D texture最常用.
分三步
- 创建一个image
- 创建一个memory object(或者使用以存在的)绑定到image
- 创建一个image view
需要创建type为 VK_IMAGE_TYPE_2D,format为VK_FORMAT_R8G8B8A8_UNORM的image.其他image的属性依赖于image的size(换句话说,我们从一个已经存在的image文件创建一个textur,需要匹配它的dimensions)、filtering类型(如果想用mipmapping的话)、samples的数量(如果需要multisampled的话)、期望的usage.
将image创建过程封装在CreateImage里
1 | if( !CreateImage( logical_device, VK_IMAGE_TYPE_2D, format, { size.width, |
将allocate和bind一个memory object封装在AllocateAndBindMemoryObjectToImage
1 | if( !AllocateAndBindMemoryObjectToImage( physical_device, logical_device, |
也能使用一个已经创建的memory
然后创建image view.
1 | if( !CreateImageView( logical_device, image, VK_IMAGE_VIEW_TYPE_2D, format, |
create a layered 2D image with a CUEMAP view
app常用于模拟物体反射环境的CUBEMAPs,不需要创建一个CUBEMAP image,只需要创建一个layered image和通过image view告诉硬件将之视为6个CUBEMAP表面.
与创建普通image一样,不同之处是CUBEMAP需要6个layres,同时不能每个texel不能使用超过一个sample.
1 | if( !CreateImage( logical_device, VK_IMAGE_TYPE_2D, |
allocate和bind a memory object
1 | if( !AllocateAndBindMemoryObjectToImage( physical_device, logical_device, |
创建image view,指明CUBEMAP view type
1 | if( !CreateImageView( logical_device, image, VK_IMAGE_VIEW_TYPE_CUBE, |
faces order +X,-X,+Y,-Y,+Z,-Z
data
mapping,updating and unmapping host-visible memory
images和buffers绑定的memory位于显卡硬件(device-local memory),高性能,但不能直接使用,我们需要使用中间的(staging)资源作为GPU(device)-CPU(host)中转.
staging resources需要host-visible,为了上传数据或者读取数据,需要map it.
mapping memory是最简单的用于upload data的方式.需要指明需要map的memory的内容(offset,size).
1 | VkResult result; |
mapping给我们一个指针,与标准c++一样使用,不限制是读还是写.
1 | std::memcpy( local_pointer, data, data_size ); |
当更新了mapped memory range.需要告诉驱动,内存的内容被修改了否则更新的数据不会立刻被其他提交给queues的操作访问.这个过程称为flush.
1 | std::vector<VkMappedMemoryRange> memory_ranges = { |
在使用完映射的memory后,可以unmap.memory mapping不会影响app运行效率.但关闭程序前必须unmap.
1 | if( unmap ) { |
copy data between buffers
除了mapping,vulkan还支持memory间(包括不同types)进行内存拷贝.
这类操作需要在command buffer中record.
1 | if( regions.size() > 0 ) { |
为了最好的性能,渲染阶段用到的资源,需要绑定device-local memory.但我们不能map这类memory.使用vkCmdCopyBuffer,能拷贝数据到另一个host-visible内存.这种memory能直接被app给mapped和updated.
能被拷贝的memory创建时要有VK_BUFFER_USAGE_TRANSFER_SRC_BIT usage.
能拷贝数据的memory创建时需要有VK_BUFFER_USAGE_TRANSFER_DST_BIT usage.
当想拷贝buffer到另一个buffer,我们需要设置一个memory barrier,告诉硬件从现在开始该buffer的操作需要按照VK_ACCESS_TRANSFER_WRITE_BIT来.当拷贝完后,我们想将之用于特定目的,需要设置另一个memory barrier.
copy data from a buffer to an image
对于images,能帮到不同memory types的memory objects.但只有host-visible memory能被app直接mapped或updated.当想更新一个device-local memory的image需要从一个buffer进行拷贝.
拷贝buffer的数据到image通过comman buffer完成.
1 | if( regions.size() > 0 ) { |
需要知道image data是如何组织在buffer里的.包括memory offset,length of the data row,height of data in a buffer.可以给row length和height设置为0,表明是紧密的数据,并与image的尺寸一致.
image from:VK_BUFFER_USAGE_TRANSFER_SRC_BIT,在transfer之前,image layout需要为VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
buffer to:VK_BUFFER_USAGE_TRANSFER_DST_BIT
在从image拷贝数据前,需要设置一个memory barrier,改变image得layout VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL.内存方位类型变为VK_ACCESS_TRANSFER_READ_BIT.如果拷贝完后要用于其他目的,需要设置另一个barrier..
staging buffer
使用staging buffer更新device-local memory
staging resources用于更新not host-visible的memory的内容.这种memory不能mapped.需要一个中间buffer(可以mapped和更新),通过它传输数据.
需要一个能mapped的buffer,可以用池.
1 | VkBuffer staging_buffer; |
然后map it和更新内容
1 | if( !MapUpdateAndUnmapHostVisibleMemory( logical_device, memory_object, 0, |
然后开始record command buffer.先给目标buffer设置一个memory barrier,改变它的usage为copy操作的target.staging buffer不需要barrier.当我们map和update buffer的memory,它的内容对其他commands也是可见的.因为导尿管我们开始command buffer recording一个隐性的barrier为host写操作而设置了.
1 | if( !BeginCommandBufferRecordingOperation( command_buffer, |
然后我们能record拷贝操作
1 | CopyDataBetweenBuffers( command_buffer, staging_buffer, destination_buffer, |
之后,需要为target buffer设置另一个barrier.将他的usage改为使用时期望的
1 | SetBufferMemoryBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, |
然后创建一个fence,并在submit cb到queue时使用
1 | VkFence fence; |
如果不再想使用staging buffer,销毁它.但必须在使用完成之后才能进行.(fence).
1 | if( !WaitForFences( logical_device, { fence }, VK_FALSE, 500000000 ) ) { |
真实应用中,通常使用一个池,复用它,而不是动态创建.这样能避免wait fence的时间,也能提高效率
使用staging buffer更新device-local memory image
与上面的类似.
1 | VkBuffer staging_buffer; |
设置barrier
1 | if( !BeginCommandBufferRecordingOperation( command_buffer, |
再次修改image的usag
1 | SetImageMemoryBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, |
fence
1 | VkFence fence; |
如果是用池的话就不需要fence.
destroy
销毁iamge view
1 | if( VK_NULL_HANDLE != buffer_view ) { |
memory object
1 | if( VK_NULL_HANDLE != memory_object ) { |
buffer
1 | if( VK_NULL_HANDLE != buffer ) { |