没有任何秘密的 API:Vulkan* 简介第 5 部分

签署人: Pawel Lapinski IDZSupport KS

已发布:10/24/2017   最后更新时间:08/12/2016

下载

查看 PDF [723KB]

返回:第 4 部分顶点属性
前往:第 6 部分描述符集


教程 5:分期资源 – 在缓冲区之间复制数据

本部分教程将重点关注如何改进性能。同时,我们将为下一篇教程做准备,下一篇教程将介绍图像与描述符(着色器资源)借助收集的信息,您将更轻松地掌握下一部分,最大限度发挥显卡硬件的性能。

什么是“分期资源”或“临时缓冲区”?它们指的是中间或临时资源,用于将数据从应用 (CPU) 传输到显卡内存 (GPU)。我们需要通过他们提升应用性能

在教程的第 4 部分,我们学习了如何使用缓冲区,将其绑定至主机可见内存,映射该内存,并将数据从 CPU 传输到 GPU。这个方法简单便捷,但是我们需要了解显卡内存的主机可见部分并不是最高效的部分。相比应用无法直接访问的内存(应用无法映射),它们的速度通常缓慢得多。这导致我们的应用以次优的方式执行。

解决该问题的办法是渲染过程中的所有资源始终使用设备本地内存。但是由于应用无法访问设备本地内存,我们不能将数据从 CPU 直接传输到内存。这就是我们需要中间或分期资源的原因。

在本部分教程,我们将借助顶点属性数据将缓冲区绑定至设备本地内存。我们将使用临时缓冲区协调从 CPU 到顶点缓冲区的数据传输。

再次声明,仅介绍本教程和上一教程(第 4 部分)的不同之处。

创建渲染资源

此次,我将渲染资源创建移至代码的开头部分。稍后,我们需要记录与提交一个命令缓冲区,以将数据从分期资源传输至顶点缓冲区。我还重构了渲染资源创建代码,以消除多个循环,将它们替换为单个循环。在这个循环中,我们创建构成虚拟帧的所有资源。

bool Tutorial05::CreateRenderingResources() {
  if( !CreateCommandPool( GetGraphicsQueue().FamilyIndex, &Vulkan.CommandPool ) ) {
    return false;
  }

  for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
    if( !AllocateCommandBuffers( Vulkan.CommandPool, 1, &Vulkan.RenderingResources[i].CommandBuffer ) ) {
      return false;
    }

    if( !CreateSemaphore( &Vulkan.RenderingResources[i].ImageAvailableSemaphore ) ) {
      return false;
    }

    if( !CreateSemaphore( &Vulkan.RenderingResources[i].FinishedRenderingSemaphore ) ) {
      return false;
    }

    if( !CreateFence( VK_FENCE_CREATE_SIGNALED_BIT, &Vulkan.RenderingResources[i].Fence ) ) {
      return false;
    }
  }
  return true;
}

bool Tutorial05::CreateCommandPool( uint32_t queue_family_index, VkCommandPool *pool ) {
  VkCommandPoolCreateInfo cmd_pool_create_info = {
    VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,       // VkStructureType                sType
    nullptr,                                          // const void                    *pNext
    VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT | // VkCommandPoolCreateFlags       flags
    VK_COMMAND_POOL_CREATE_TRANSIENT_BIT,
    queue_family_index                                // uint32_t                       queueFamilyIndex
  };

  if( vkCreateCommandPool( GetDevice(), &cmd_pool_create_info, nullptr, pool ) != VK_SUCCESS ) {
    std::cout << "Could not create command pool!"<< std::endl;
    return false;
  }
  return true;
}

bool Tutorial05::AllocateCommandBuffers( VkCommandPool pool, uint32_t count, VkCommandBuffer *command_buffers ) {
  VkCommandBufferAllocateInfo command_buffer_allocate_info = {
    VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,   // VkStructureType                sType
    nullptr,                                          // const void                    *pNext
    pool,                                             // VkCommandPool                  commandPool
    VK_COMMAND_BUFFER_LEVEL_PRIMARY,                  // VkCommandBufferLevel           level
    count                                             // uint32_t                       bufferCount
  };

  if( vkAllocateCommandBuffers( GetDevice(), &command_buffer_allocate_info, command_buffers ) != VK_SUCCESS ) {
    std::cout << "Could not allocate command buffer!"<< std::endl;
    return false;
  }
  return true;
}

bool Tutorial05::CreateSemaphore( VkSemaphore *semaphore ) {
  VkSemaphoreCreateInfo semaphore_create_info = {
    VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,          // VkStructureType                sType
    nullptr,                                          // const void*                    pNext
    0                                                 // VkSemaphoreCreateFlags         flags
  };

  if( vkCreateSemaphore( GetDevice(), &semaphore_create_info, nullptr, semaphore ) != VK_SUCCESS ) {
    std::cout << "Could not create semaphore!"<< std::endl;
    return false;
  }
  return true;
}

bool Tutorial05::CreateFence( VkFenceCreateFlags flags, VkFence *fence ) {
  VkFenceCreateInfo fence_create_info = {
    VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,              // VkStructureType                sType
    nullptr,                                          // const void                    *pNext
    flags                                             // VkFenceCreateFlags             flags
  };

  if( vkCreateFence( GetDevice(), &fence_create_info, nullptr, fence ) != VK_SUCCESS ) {
    std::cout << "Could not create a fence!"<< std::endl;
    return false;
  }
  return true;
}
1.	Tutorial05.cpp

1. Tutorial05.cpp

首先创建一个命令池,我们指示从这个池中分配的命令缓冲区存在时间较短。在本示例中,所有命令缓冲区将在记录之前仅提交一次。

接下来,我们将迭代任意数量的虚拟帧。本代码示例中虚拟帧的数量为 3。在循环内部,我们为每个虚拟帧分配一个命令缓冲区,创建两个旗语(一个用于图像获取,另一个用于显示帧渲染已完成)和一个栅栏。记录命令缓冲区之前,帧缓冲器创建已在绘制函数内部完成。

和第 4 部分使用的渲染资源集相同,您可以通过第 4 部分全面了解代码内部机制。我将跳过渲染通道和显卡管线创建。它们按照和之前完全相同的方式创建。由于此处无任何变动,我们将直接跳至缓冲区创建。

缓冲区创建

以下是创建缓冲区使用的通用代码:

VkBufferCreateInfo buffer_create_info = {
  VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,             // VkStructureType                sType
  nullptr,                                          // const void                    *pNext
  0,                                                // VkBufferCreateFlags            flags
  buffer.Size,                                      // VkDeviceSize                   size
  usage,                                            // VkBufferUsageFlags             usage
  VK_SHARING_MODE_EXCLUSIVE,                        // VkSharingMode                  sharingMode
  0,                                                // uint32_t                       queueFamilyIndexCount
  nullptr                                           // const uint32_t                *pQueueFamilyIndices
};

if( vkCreateBuffer( GetDevice(), &buffer_create_info, nullptr, &buffer.Handle ) != VK_SUCCESS ) {
  std::cout << "Could not create buffer!"<< std::endl;
  return false;
}

if( !AllocateBufferMemory( buffer.Handle, memoryProperty, &buffer.Memory ) ) {
  std::cout << "Could not allocate memory for a buffer!"<< std::endl;
  return false;
}

if( vkBindBufferMemory( GetDevice(), buffer.Handle, buffer.Memory, 0 ) != VK_SUCCESS ) {
  std::cout << "Could not bind memory to a buffer!"<< std::endl;
  return false;
}

return true;

2. Tutorial05.cpp, function CreateBuffer()

将代码打包至一个 CreateBuffer() 函数,该函数接收缓冲区的使用、大小和所需的内存属性。为了创建缓冲区,需要准备类型变量 VkBufferCreateInfo。该结构包含以下字段:

  • sType – 标准结构类型。此处应等于 VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 描述缓冲区其他属性的参数。现在,我们只能指定缓冲区可通过稀疏内存备份。
  • size – 缓冲区大小(字节)。
  • usage – 显示缓冲区既定用途的位字段。
  • sharingMode – 队列共享模式。
  • queueFamilyIndexCount – 并发共享模式下访问缓冲区的各类队列家族数量。
  • pQueueFamilyIndices – 使用并发共享模式时,访问缓冲区的所有队列家族的索引阵列。

目前,我们对绑定稀疏内存不感兴趣。我们不希望在不同设备队列之间共享缓冲区,因此,sharingMode、queueFamilyIndexCount 和 pQueueFamilyIndices 参数与本文无关。size 和 usage 是最重要的参数。我们不能将缓冲区用于缓冲器创建期间未指定的目的。最终,我们需要创建一个足够容纳数据的缓冲区。

为了创建缓冲区,需要调用 vkCreateBuffer() 函数,调用成功后,缓冲区句柄被保存在我们提供地址的变量中。但是创建缓冲区远远不够,创建的缓冲区没有存储功能。我们需要将内存对象(或部分内存对象)绑定至缓冲区,以支持存储。如果没有内存对象,需要分配一个内存对象。

每个缓冲区的 usage 可能包含不同的内存要求,当我们想要分配内存对象并将其绑定到缓冲区时,便涉及到这些要求。以下代码示例可将一个内存对象分配至特定缓冲区:

VkMemoryRequirements buffer_memory_requirements;
vkGetBufferMemoryRequirements( GetDevice(), buffer, &buffer_memory_requirements );

VkPhysicalDeviceMemoryProperties memory_properties;
vkGetPhysicalDeviceMemoryProperties( GetPhysicalDevice(), &memory_properties );

for( uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i ) {
  if( (buffer_memory_requirements.memoryTypeBits & (1 << i)) &&
      ((memory_properties.memoryTypes[i].propertyFlags & property) == property) ) {

    VkMemoryAllocateInfo memory_allocate_info = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,       // VkStructureType                sType
      nullptr,                                      // const void                    *pNext
      buffer_memory_requirements.size,              // VkDeviceSize                   allocationSize
      i                                             // uint32_t                       memoryTypeIndex
    };

    if( vkAllocateMemory( GetDevice(), &memory_allocate_info, nullptr, memory ) == VK_SUCCESS ) {
      return true;
    }
  }
}
return false;

3. Tutorial05.cpp, function AllocateBufferMemory()

和第 4 部分的代码类似,我们首先查看特定缓冲区的内存要求。然后检查特定物理设备提供的内存属性,包含内存堆数量及其功能等信息。

接下来,我们迭代每个可用的内存类型,并检查它是否满足特定缓冲区的要求。我们还检查了特定内存类型是否支持请求的额外属性,如特定内存类型是否对主机可见。找到匹配对象后,我们将填充 VkMemoryAllocateInfo 结构,并调用一个 vkAllocateMemory() 函数。

分配的内存对象便被绑定至缓冲区,从现在起,我们可以在应用中安全使用该缓冲区。

顶点缓冲区创建

顶点缓冲区是我们想要创建的第一个缓冲区,它能存储渲染期间使用的顶点属性数据。在本示例中,我们存储了四边形 4 个顶点的位置和颜色。与之前教程相比,最大的变化在于使用设备本地内存,而非主机可见内存。设备本地内存速度更快,但是我们无法直接将数据从应用复制到设备本地内存。我们需要使用临时缓冲区将数据复制到顶点缓冲区。

我们还需要为该缓冲区指定两个不同的 usage。第一个是 vertex buffer usage,意味着我们想将特定缓冲区用作顶点缓冲区,并从顶点缓冲区获取顶点属性数据。第二个是 transfer dst usage,意味着我们将复制数据至该缓冲区。它将被用作任何传输(复制)操作的目标。

以下代码用于创建包含上述要求的缓冲区:

const std::vector<float>& vertex_data = GetVertexData();

Vulkan.VertexBuffer.Size = static_cast<uint32_t>(vertex_data.size() * sizeof(vertex_data[0]));
if( !CreateBuffer( VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, Vulkan.VertexBuffer ) ) {
  std::cout << "Could not create vertex buffer!"<< std::endl;
  return false;
}

return true;

4. Tutorial05.cpp, function CreateVertexBuffer()

起初,我们使用顶点数据(在 GetVertexData() 函数中硬编码)检查保留所有顶点值所需的空间。随后调用之前介绍的 CreateBuffer() 函数,以创建一个顶点缓冲区并将设备本地内存绑定于此。

临时缓冲区创建

下面我们将创建一个中间临时缓冲区。由于该缓冲区不在渲染过程中使用,因此,可以将其绑定至速度较慢的主机可见内存。通过这种方式,我们可以映射缓冲区,并从应用中直接复制数据。随后,我们可以将数据从临时缓冲区复制到绑定设备本地内存的任何缓冲区(甚至图像)。如此一来,用于渲染目的的所有资源均被绑定至速度最快的可用内存。我们只需执行其他数据传输操作。

以下代码可用于创建临时缓冲区:

Vulkan.StagingBuffer.Size = 4000;
if( !CreateBuffer( VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, Vulkan.StagingBuffer ) ) {
  std::cout << "Could not staging buffer!"<< std::endl;
  return false;
}

return true;

5. Tutorial04.cpp, function CreateStagingBuffer()

我们将从该缓冲区复制数据至其他资源,因此,必须指定一个 transfer src usage(将用作传输操作的来源)。我们可以通过映射直接从应用中复制任何数据。为此,我们需要使用主机可见内存,这就是我们指定这个内存属性的原因。缓冲区大小可随机选择,但是必须足以保存顶点数据。在真实场景中,许多情况下我们应尝试尽可能多地重复使用临时缓冲区,使它的尺寸大到足以包含应用中大多数数据传输操作。当然,如果我们想同时执行多个传输操作,需要创建多个临时缓冲区。

在缓冲区之间复制数据

我们已经创建了两个缓冲区:一个用于顶点属性数据,另一个用作中间缓冲区。现在,我们需要将数据从 CPU 复制到 GPU。为此,需要映射临时缓冲区,获取一个指示器,并使用指示器将数据上传至显卡硬件内存。随后需要记录并提交命令缓冲区,后者将顶点数据从临时缓冲区复制到顶点缓冲区。现在,用于虚拟帧与渲染的所有命令缓冲区均被标记为短期,我们可以在操作中安全使用其中一个缓冲区。

首先,顶点属性数据如下所示:

static const std::vector<float> vertex_data = {
  -0.7f, -0.7f, 0.0f, 1.0f,
  1.0f, 0.0f, 0.0f, 0.0f,
  //
  -0.7f, 0.7f, 0.0f, 1.0f,
  0.0f, 1.0f, 0.0f, 0.0f,
  //
  0.7f, -0.7f, 0.0f, 1.0f,
  0.0f, 0.0f, 1.0f, 0.0f,
  //
  0.7f, 0.7f, 0.0f, 1.0f,
  0.3f, 0.3f, 0.3f, 0.0f
};

return vertex_data;

6. Tutorial05.cpp, function GetVertexData()

它是一个简单的硬编码浮点值阵列。每个顶点数据包含 4 个位置属性组件和 4 个颜色属性组件。如果我们渲染四个顶点,将得到 4 对属性。

以下代码将数据从应用复制到临时缓冲区,随后,从临时缓冲区复制到顶点缓冲区:

// Prepare data in a staging buffer
const std::vector<float>& vertex_data = GetVertexData();

void *staging_buffer_memory_pointer;
if( vkMapMemory( GetDevice(), Vulkan.StagingBuffer.Memory, 0, Vulkan.VertexBuffer.Size, 0, &staging_buffer_memory_pointer) != VK_SUCCESS ) {
  std::cout << "Could not map memory and upload data to a staging buffer!"<< std::endl;
  return false;
}

memcpy( staging_buffer_memory_pointer, &vertex_data[0], Vulkan.VertexBuffer.Size );

VkMappedMemoryRange flush_range = {
  VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,            // VkStructureType                        sType
  nullptr,                                          // const void                            *pNext
  Vulkan.StagingBuffer.Memory,                      // VkDeviceMemory                         memory
  0,                                                // VkDeviceSize                           offset
  Vulkan.VertexBuffer.Size                          // VkDeviceSize                           size
};
vkFlushMappedMemoryRanges( GetDevice(), 1, &flush_range );

vkUnmapMemory( GetDevice(), Vulkan.StagingBuffer.Memory );

// Prepare command buffer to copy data from staging buffer to a vertex buffer
VkCommandBufferBeginInfo command_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,      // VkStructureType                        sType
  nullptr,                                          // const void                            *pNext
  VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,      // VkCommandBufferUsageFlags              flags
  nullptr                                           // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

VkCommandBuffer command_buffer = Vulkan.RenderingResources[0].CommandBuffer;

vkBeginCommandBuffer( command_buffer, &command_buffer_begin_info);

VkBufferCopy buffer_copy_info = {
  0,                                                // VkDeviceSize                           srcOffset
  0,                                                // VkDeviceSize                           dstOffset
  Vulkan.VertexBuffer.Size                          // VkDeviceSize                           size
};
vkCmdCopyBuffer( command_buffer, Vulkan.StagingBuffer.Handle, Vulkan.VertexBuffer.Handle, 1, &buffer_copy_info );

VkBufferMemoryBarrier buffer_memory_barrier = {
  VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,          // VkStructureType                        sType;
  nullptr,                                          // const void                            *pNext
  VK_ACCESS_MEMORY_WRITE_BIT,                       // VkAccessFlags                          srcAccessMask
  VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,              // VkAccessFlags                          dstAccessMask
  VK_QUEUE_FAMILY_IGNORED,                          // uint32_t                               srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,                          // uint32_t                               dstQueueFamilyIndex
  Vulkan.VertexBuffer.Handle,                       // VkBuffer                               buffer
  0,                                                // VkDeviceSize                           offset
  VK_WHOLE_SIZE                                     // VkDeviceSize                           size
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, 0, 0, nullptr, 1, &buffer_memory_barrier, 0, nullptr );

vkEndCommandBuffer( command_buffer );

// Submit command buffer and copy data from staging buffer to a vertex buffer
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,                    // VkStructureType                        sType
  nullptr,                                          // const void                            *pNext
  0,                                                // uint32_t                               waitSemaphoreCount
  nullptr,                                          // const VkSemaphore                     *pWaitSemaphores
  nullptr,                                          // const VkPipelineStageFlags            *pWaitDstStageMask;
  1,                                                // uint32_t                               commandBufferCount
  &command_buffer,                                  // const VkCommandBuffer                 *pCommandBuffers
  0,                                                // uint32_t                               signalSemaphoreCount
  nullptr                                           // const VkSemaphore                     *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

vkDeviceWaitIdle( GetDevice() );

return true;

7. Tutorial05.cpp, function CopyVertexData()

起初,我们得到顶点数据,通过调用 vkMapMemory() 函数映射临时缓冲区内存。调用过程中,我们指定绑定临时缓冲区的内存句柄和缓冲区大小。我们获得了一个指示器,可以在普通的 memcpy() 函数中使用指示器将数据从应用复制到显卡硬件。

接下来刷新映射内存,以通知驱动程序哪些内存对象被修改。如需要,可以指定多个内存范围。我们需要刷新一个存储区,通过创建一个类型变量 VkMappedMemoryRange 和调用 vkFlushMappedMemoryRanges() 函数指定存储区。随后取消映射内存,但是没有必要这样做。我们可以保留一个指示器,以便稍后使用,这样不会影响应用性能。

接下来开始准备命令缓冲区。我们指定在重设前仅提交一次。填充 VkCommandBufferBeginInfo 结构并将它提供给 vkBeginCommandBuffer() 函数。

现在执行复制操作。首先创建一个类型变量 VkBufferCopy,包含以下字段:

  • srcOffset – 从中复制数据的源缓冲区中的偏移量(字节)。
  • dstOffset – 向其复制数据的目标缓冲区中的偏移量(字节)。
  • size – 想要复制的数据的大小(字节)。

我们将数据从临时缓冲区的开头复制到顶点缓冲区的开头,因此,指定两个偏移量为零。基于硬编码顶点数据计算顶点缓冲区的大小,因此,复制相同字节量。为了将数据从一个缓冲区复制到另一个缓冲区,需要调用 vkCmdCopyBuffer() 函数。

设置缓冲区内存壁垒

我们已经记录了复制操作,但是这并非全部内容。从现在起,缓冲区不再被用作传输操作的目标,而是用作顶点缓冲区。我们需要通知驱动程序缓冲区内存访问类型将发生变化,从现在起,它将用作顶点属性数据的来源。为了实现这个目标,按照之前在交换链图像中使用的方法设置一个内存壁垒。

首先准备一个类型变量 VkBufferMemoryBarrier,包含以下部分:

  • sType – 标准结构类型,此处设置为 VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER。
  • pNext – 为扩展功能预留的指示器。
  • srcAccessMask – 壁垒前在缓冲区上执行的内存操作类型。
  • dstAccessMask –壁垒后将在特定缓冲区上执行的内存操作。
  • srcQueueFamilyIndex – 之前访问缓冲区的队列家族索引。
  • dstQueueFamilyIndex – 从现在起访问缓冲区的队列家族。
  • buffer – 对其设置壁垒的缓冲区句柄。
  • offset – 缓冲区开始时的内存偏移量(从内存的基本偏移量绑定至缓冲区)。
  • size – 将对其设置壁垒的缓冲区存储区的大小。

如上所示,我们可以针对特定缓冲区内存范围设置壁垒。在本示例中,我们面向整个缓冲区设置壁垒,因此,指定偏移量为 0,大小为 VK_WHOLE_SIZE enum。我们不想在不同队列家族之间转让所有权,因此,在 srcQueueFamilyIndex 和 dstQueueFamilyIndex 中使用 VK_QUEUE_FAMILY_IGNORED enum。

srcAccessMask 和 dstAccessMask 是最重要的参数。我们已经将数据从临时缓冲区复制到顶点缓冲区。在壁垒出现之前,顶点缓冲区被用作传输操作和内存编写的目标。这就是我们将 srcAccessMask 字段设置为 VK_ACCESS_MEMORY_WRITE_BIT 的原因。但是从此以后,壁垒缓冲区只能被用作顶点属性数据的来源。因此,我们将 dstAccessMask 字段指定为 VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT。

为了设置壁垒,我们调用了一个 vkCmdPipelineBarrier() 函数。为了完成命令缓冲区记录,我们调用了 vkEndCommandBuffer()。接下来,我们通过调用 vkQueueSubmit() 函数来提交命令缓冲区,以执行所有上述操作。

通常情况下,提交命令缓冲区时,我们应该提供一个栅栏。所有传输操作和整个命令缓冲区完成后,将发送信号。为了简单起见,我们调用 vkDeviceWaitIdle(),并等待特定设备上执行的所有操作完成。完成后,我们成功地将数据传输至设备本地内存,并且使用顶点缓冲区时无须担心性能下降。

Tutorial05 Execution

渲染操作的结果和第 4 部分完全相同:

我们渲染了一个四角颜色分别为红、绿、深灰和蓝色的四边形。四边形将调整大小(和形状),以匹配窗口的大小和形状。

清除

在本部分教程,我重构了清除代码。我们创建了两个缓冲区,每个缓冲区包含单独的内存对象。为了避免代码冗余,我准备了一个缓冲区清除函数:

if( buffer.Handle != VK_NULL_HANDLE ) {
  vkDestroyBuffer( GetDevice(), buffer.Handle, nullptr );
  buffer.Handle = VK_NULL_HANDLE;
}

if( buffer.Memory != VK_NULL_HANDLE ) {
  vkFreeMemory( GetDevice(), buffer.Memory, nullptr );
  buffer.Memory = VK_NULL_HANDLE;
}

8. Tutorial05.cpp, function DestroyBuffer()

该函数检查特定缓冲区是否已成功创建,如果成功创建,它将调用一个 vkDestroyBuffer() 函数。它还通过调用 vkFreeMemory() 函数释放了与特定缓冲区相关的内存。在析构函数中调用 DestroyBuffer() 函数,释放了与本部分教程相关的其他全部资源:

if( GetDevice() != VK_NULL_HANDLE ) {
  vkDeviceWaitIdle( GetDevice() );

  DestroyBuffer( Vulkan.VertexBuffer );

  DestroyBuffer( Vulkan.StagingBuffer );

  if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
    vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
    Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
  }

  if( Vulkan.RenderPass != VK_NULL_HANDLE ) {
    vkDestroyRenderPass( GetDevice(), Vulkan.RenderPass, nullptr );
    Vulkan.RenderPass = VK_NULL_HANDLE;
  }

  for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
    if( Vulkan.RenderingResources[i].Framebuffer != VK_NULL_HANDLE ) {
      vkDestroyFramebuffer( GetDevice(), Vulkan.RenderingResources[i].Framebuffer, nullptr );
    }
    if( Vulkan.RenderingResources[i].CommandBuffer != VK_NULL_HANDLE ) {
      vkFreeCommandBuffers( GetDevice(), Vulkan.CommandPool, 1, &Vulkan.RenderingResources[i].CommandBuffer );
    }
    if( Vulkan.RenderingResources[i].ImageAvailableSemaphore != VK_NULL_HANDLE ) {
      vkDestroySemaphore( GetDevice(), Vulkan.RenderingResources[i].ImageAvailableSemaphore, nullptr );
    }
    if( Vulkan.RenderingResources[i].FinishedRenderingSemaphore != VK_NULL_HANDLE ) {
      vkDestroySemaphore( GetDevice(), Vulkan.RenderingResources[i].FinishedRenderingSemaphore, nullptr );
    }
    if( Vulkan.RenderingResources[i].Fence != VK_NULL_HANDLE ) {
      vkDestroyFence( GetDevice(), Vulkan.RenderingResources[i].Fence, nullptr );
    }
  }

  if( Vulkan.CommandPool != VK_NULL_HANDLE ) {
    vkDestroyCommandPool( GetDevice(), Vulkan.CommandPool, nullptr );
    Vulkan.CommandPool = VK_NULL_HANDLE;
  }
}

9. Tutorial05.cpp, destructor

首先等待该设备执行的所有操作完成。接下来,破坏顶点和临时缓冲区。然后,按照与创建时相反的顺序破坏所有其他资源:显卡管线、渲染通道和每个虚拟帧的资源,包含一个帧缓冲器、命令缓冲区、两个旗语、一个栅栏和一个帧缓冲器。最终,我们破坏了一个命令缓冲区从中分配的命令池。

结论

在本教程中,我们使用推荐的技术将数据从应用传输到显卡硬件。该数据使渲染过程中的资源实现了最佳性能,支持将数据从应用映射与复制到临时缓冲区。我们只需准备额外的命令缓冲区记录与提交,以在不同缓冲区之间传输数据。

建议使用临时缓冲区,它的用途不仅限于在不同缓冲区之间复制数据。我们可以使用相同的方法将数据从缓冲区复制到图像。本教程的下一部分将通过展示描述符、描述符集、描述符布局等 Vulkan API 的重要组成部分,介绍如何执行上述操作。

前往:没有任何秘密的 API:Vulkan* 简介第 6 部分

该示例源代码根据“英特尔示例源代码许可协议”发布

产品和性能信息

1

英特尔的编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。这些优化包括 SSE2、SSE3 和 SSSE3 指令集和其他优化。对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。该产品中依赖于微处理器的优化仅适用于英特尔微处理器。某些非特定于英特尔微架构的优化保留用于英特尔微处理器。关于此通知涵盖的特定指令集的更多信息,请参阅适用产品的用户指南和参考指南。

通知版本 #20110804