Vulkan Cookbook 第四章 15 使用残存缓冲区更新设备本地内存绑定的缓冲区

使用残存缓冲区更新设备本地内存绑定的缓冲区

译者注:示例代码点击此处

暂存资源用于更新非主机不可见的内存内容。这样的内存无法映射,因此我们需要一个中间缓冲区,其内容可以很容易地映射和更新,并且可以从中传输数据。

怎么做...

1.获取存储在名为logical_device的VkDevice类型的变量中的逻辑设备句柄。
2.准备应上载到目标缓冲区的数据。设置指向数据源开头的指针,并将其存储在名为data的void*类型的变量中。数据的大小(以字节为单位)应使用名为data_size的VkDeviceSize类型的变量表示。
3.创建一个名为staging_buffer的VkBuffer类型的变量。其中将存储暂存
缓冲区的句柄。
4.创建一个足以容纳data_size字节数的缓冲区。在缓冲区创建期间指定VK_BUFFER_USAGE_TRANSFER_SRC_BIT用法。在创建过程中使用logical_device变量,并将创建的句柄存储在staging_buffer变量中。
5.获取创建logical_device句柄的物理设备的句柄。使用物理设备句柄初始化名为physical_device的VkPhysicalDevice类型变量。
6.创建一个名为memory_object的VkDeviceMemory类型变量,该变量将用于为暂存
缓冲区创建内存对象。
7.使用physical_device、logical_device和staging_buffer变量分配内存对象。从具有VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT属性的内存类型中的分配内存对象。将创建的句柄存储在memory_object变量中,并将其绑定到临时缓冲区(请参阅为缓冲区分配和绑定内存对象内容)。
8.使用logical_device变量映射memory_object的内存,为offset定义0值,使用data_size变量映射内存大小。将数据从数据指针复制到memory_object中。取消映射内存(请参阅映射、更新和取消映射主机可见内存内容)。
9.获取已分配的主命令缓冲区的句柄,并存储在名为command_buffer的VkCommandBuffer类型变量中。
10.开始记录command_buffer。并提供VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT标志(请参阅第三章,命令缓冲区和同步的开始命令缓冲区记录操作内容)。
11.获取将传输数据的缓冲区句柄。确保它是使用VK_BUFFER_USAGE_TRANSFER_DST_BIT创建的。将其句柄存储在名为destination_buffer的VkBuffer类型的变量中。
12.在command_buffer变量中记录destination_buffer的内存屏障。提供到目前为止的管线阶段。并为消费阶段使用VK_PIPELINE_STAGE_TRANSFER_BIT阶段。提供到目前为止已引用缓冲区的内存访问类型,并为新访问类型使用VK_ACCESS_TRANSFER_WRITE_BIT值。忽略队列族索引-为两个索引提供VK_QUEUE_FAMILY_IGNORED(请参阅设置缓冲区内存屏障)。
13.创建一个名为destination_offset的VkDeviceSize类型变量,并用一个偏移值对其进行初始化。
14.使用command_buffer变量将数据从staging_buffer复制到destination_buffer。为源偏移提供0值,为目标偏移提供destination_offset变量,为要传输的数据大小提供data_size变量(请参阅在缓冲区之间复制数据内容)。
15.在command_buffer变量中记录destination_buffer的另一个内存屏障。VK_PIPELINE_STAGE_TRANSFER_BIT值用于当前内存访问类型,以及适用于内存传输后缓冲区使用方法的值。对队列族索引使用VK_QUEUE_FAMILY_IGNORED值(请参阅在缓冲区之间复制数据内容)。
16.结束记录command_buffer(请参阅第三章,命令缓冲区和同步中的结束命令缓冲区记录操作内容)。
17.获取将执行传输操作的队列句柄。并将其存储在名为queue的VkQueue类型变量中。
18.创建一个信号量列表,在传输操作完成时应该发出信号。将其句柄存储在名为signal_semaphores的std::vector<VkSemaphore>变脸中。
19.创建一个名为fence的VkFence类型变量。
20.使用logical_device变量创建无信号的围栏。将创建的句柄储存在fence变脸中(请参阅第3章,命令缓冲区和同步中的创建围栏)。
21.将command_buffer提交到队列。提供signal_semaphores向量中的信号量列表作为要发信号通知的信号量列表,以及要接收信号通知的围栏fence变量(请参阅第3章,命令缓冲区和同步中的将命令缓冲区提交到队列内容)。
22.等待指定logical_device设备上的围栏fence变量发生信号,提供所需的超时值(请参阅第3章,命令缓冲区和同步中的等待围栏内容)。
23.销毁staging_buffer变量表示的缓冲区(请参阅销毁缓冲区内容)。
24.释放memory_object变量表示的内存对象(请参阅释放内存对象内容)。

这个怎么运作...

为了将暂存资源用于传输操作,我们需要一个具有可映射内存的缓冲区。可以使用现有的缓冲区或者创建一个新缓冲区,如下所示:

VkBuffer staging_buffer;
if( !CreateBuffer( logical_device, data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, *staging_buffer ) ) {
  return false;
}

VkDeviceMemory memory_object;
if( !AllocateAndBindMemoryObjectToBuffer( physical_device, logical_device, *staging_buffer, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, *memory_object ) ) {
  return false;
}

接下来,我们需要映射缓冲区的内存并更新其内容:

if( !MapUpdateAndUnmapHostVisibleMemory( logical_device, memory_object, 0, data_size, data, true, nullptr ) ) {
  return false;
}

准备好暂存缓冲区后,我们可以开始传输操作,将数据复制到所需的目标缓冲区。我们首先开始命令缓冲区记录操作,并为目标缓冲区设置内存屏障,将其用途更改为特定目的以进行数据复制操作。我们不需要残存缓冲区的内存屏障,当我们映射和更新缓冲区的内存时,其内容对于其他命令变为可见。因此当我们启动命令缓冲区记录时,为主机写入器设置了隐式屏障:

if( !BeginCommandBufferRecordingOperation( command_buffer, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, nullptr ) ) {
  return false;
}

SetBufferMemoryBarrier( command_buffer, destination_buffer_generating_stages, VK_PIPELINE_STAGE_TRANSFER_BIT, { { destination_buffer, destination_buffer_current_access, VK_ACCESS_TRANSFER_WRITE_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED } } );

接下来,我们可以记录从暂存资源到目标缓冲区放的数据复制操作:

CopyDataBetweenBuffers( command_buffer, *staging_buffer, destination_buffer, { { 0, destination_offset, data_size } } );

在此之后,我们需要为目标缓冲区提供第二个内存屏障。这一次将其用法从复制操作的目标更改为数据传输后缓冲区将用于的目标。我们还可以结束命令缓冲区记录:

SetBufferMemoryBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, destination_buffer_consuming_stages, { { destination_buffer, VK_ACCESS_TRANSFER_WRITE_BIT, destination_buffer_new_access, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED } } );

if( !EndCommandBufferRecordingOperation( command_buffer ) ) {
  return false;
}

接下来,我们创建一个围栏(fence)并将命令缓冲区提交给队列,在那里它将被处理并实际执行数据参数:

VkFence fence;
if( !CreateFence( logical_device, false, *fence ) ) {
  return false;
}

if( !SubmitCommandBuffersToQueue( queue, {}, { command_buffer }, signal_semaphores, *fence ) ) {
  return false;
}

如果我们不想再使用残存(staging)缓冲区,可以销毁它。但是,我们不能这样做,直到提交到队列的命令不再使用暂存缓冲区:这就是我们需要一个围栏的原因。我们等待它,直到驱动程序在处理完提交到命令缓冲区时发出信号。然后,我们可以安全销毁一个临时缓冲区并释放绑定到它的内存对象:

if( !WaitForFences( logical_device, { fence }, VK_FALSE, 500000000 ) ) { 
  return false;
}

DestroyBuffer( logical_device, staging_buffer ); 
FreeMemoryObject( logical_device, memory_object ); 
return true;

在实际场景中,我们应该使用现有的缓冲区,并尽可能多地将其作为暂存缓冲区重用,以避免不必要的缓冲区创建和销毁操作。这样,我们也避免了在围栏上等待

译者注:我们也避免了在围栏上等待,当我们再次使用同一个暂存缓冲区的时候还是需要等待围栏的或者检查围栏是否已经完成,只是因为不需要立刻删除缓冲区而进行立刻等待。在第三章检查提交的命令缓冲区的处理是否已完成中的最后一段有所描述。

猜你喜欢

转载自blog.csdn.net/qq_19473837/article/details/85048267
今日推荐