在我们开始实现纹理之前,我们需要先做一些准备工作。现在,我们正在通过将顶点缓冲区存储在 CPU_TO_GPU 内存类型中来实现它。这是可行的,但这不是处理网格数据的推荐方式。对于顶点缓冲区,您希望它们在最快的内存类型(即 GPU_ONLY 内存类型)中可访问。问题是您无法直接从 CPU 写入它。
要将数据发送到仅 GPU 内存中,您需要首先将数据复制到 CPU 可写缓冲区中,在 VkCommandBuffer
中编码复制命令,然后将该命令缓冲区提交到队列。这将使内存从您的 CPU 可写缓冲区传输到另一个缓冲区(可以是 GPU 分配的缓冲区)排队。
由于这在处理纹理时是必要的,我们将在 upload_mesh
函数中实现此复制逻辑。如果您一直在尝试加载大型网格,这可能会立即提高渲染速度。
上传上下文
由于从 CPU 缓冲区复制网格到 GPU 缓冲区不会是我们唯一要做的事情,我们将为此类短生命周期的命令创建一个小的抽象。
让我们首先向 VulkanEngine 添加一个新的结构体,以及一个用于立即命令执行的函数。
struct UploadContext {
VkFence _uploadFence;
VkCommandPool _commandPool;
VkCommandBuffer _commandBuffer;
};
class VulkanEngine {
public:
UploadContext _uploadContext;
void immediate_submit(std::function<void(VkCommandBuffer cmd)>&& function);
}
我们将上传相关结构存储在结构体中,以使 VulkanEngine 类中的对象数量更好地组织。immediate_submit()
函数以与我们在删除队列中非常相似的方式使用 std::function
。
最终,我们将添加更多即时提交函数,但这将是默认函数。
我们需要在上传上下文中初始化该命令池和栅栏。在 init_sync_structures()
中,我们将与我们已有的渲染栅栏一起初始化栅栏。
VkFenceCreateInfo uploadFenceCreateInfo = vkinit::fence_create_info();
VK_CHECK(vkCreateFence(_device, &uploadFenceCreateInfo, nullptr, &_uploadContext._uploadFence));
_mainDeletionQueue.push_function([=]() {
vkDestroyFence(_device, _uploadContext._uploadFence, nullptr);
});
在此栅栏上,我们不会设置 VK_FENCE_CREATE_SIGNALED_BIT
标志,因为我们不会像在渲染循环中那样尝试在发送命令之前等待它。
在我们继续之前,让我们为开始命令缓冲区和填写 VkSubmitInfo 定义有用的初始化器
// header
VkCommandBufferBeginInfo command_buffer_begin_info(VkCommandBufferUsageFlags flags = 0);
VkSubmitInfo submit_info(VkCommandBuffer* cmd);
// implementation
VkCommandBufferBeginInfo vkinit::command_buffer_begin_info(VkCommandBufferUsageFlags flags /*= 0*/)
{
VkCommandBufferBeginInfo info = {};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
info.pNext = nullptr;
info.pInheritanceInfo = nullptr;
info.flags = flags;
return info;
}
VkSubmitInfo vkinit::submit_info(VkCommandBuffer* cmd)
{
VkSubmitInfo info = {};
info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
info.pNext = nullptr;
info.waitSemaphoreCount = 0;
info.pWaitSemaphores = nullptr;
info.pWaitDstStageMask = nullptr;
info.commandBufferCount = 1;
info.pCommandBuffers = cmd;
info.signalSemaphoreCount = 0;
info.pSignalSemaphores = nullptr;
return info;
}
现在我们创建命令池并从中分配命令缓冲区,我们在 init_commands
中执行此操作
VkCommandPoolCreateInfo uploadCommandPoolInfo = vkinit::command_pool_create_info(_graphicsQueueFamily);
//create pool for upload context
VK_CHECK(vkCreateCommandPool(_device, &uploadCommandPoolInfo, nullptr, &_uploadContext._commandPool));
_mainDeletionQueue.push_function([=]() {
vkDestroyCommandPool(_device, _uploadContext._commandPool, nullptr);
});
//allocate the default command buffer that we will use for the instant commands
VkCommandBufferAllocateInfo cmdAllocInfo = vkinit::command_buffer_allocate_info(_uploadContext._commandPool, 1);
VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &_uploadContext._commandBuffer));
现在我们正在使用图形队列族创建池。这是因为我们将命令提交到与图形队列相同的队列。
初始化命令池和栅栏后,让我们编写 immediate_submit
函数的代码
void VulkanEngine::immediate_submit(std::function<void(VkCommandBuffer cmd)>&& function)
{
VkCommandBuffer cmd = _uploadContext._commandBuffer;
//begin the command buffer recording. We will use this command buffer exactly once before resetting, so we tell vulkan that
VkCommandBufferBeginInfo cmdBeginInfo = vkinit::command_buffer_begin_info(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo));
//execute the function
function(cmd);
VK_CHECK(vkEndCommandBuffer(cmd));
VkSubmitInfo submit = vkinit::submit_info(&cmd);
//submit command buffer to the queue and execute it.
// _uploadFence will now block until the graphic commands finish execution
VK_CHECK(vkQueueSubmit(_graphicsQueue, 1, &submit, _uploadContext._uploadFence));
vkWaitForFences(_device, 1, &_uploadContext._uploadFence, true, 9999999999);
vkResetFences(_device, 1, &_uploadContext._uploadFence);
// reset the command buffers inside the command pool
vkResetCommandPool(_device, _uploadContext._commandPool, 0);
}
这与我们在渲染循环中执行的逻辑非常相似。最重要的是我们正在帧与帧之间重用相同的命令缓冲区。如果我们想提交多个命令缓冲区,只需提前分配我们想要的尽可能多的命令缓冲区并重用它们即可。
我们首先分配命令缓冲区,然后我们在 begin/end 命令缓冲区之间调用该函数,然后我们提交它。然后我们等待提交完成,并重置命令池。
有了这个,我们现在有一种方法可以立即向 GPU 执行一些命令,而无需处理渲染循环和其他同步。这非常适合计算,并且,如果它提交到不同的队列,您可以从后台线程中使用它,与渲染循环分离。
传输内存。
现在我们有了即时命令系统,我们将重写 upload_mesh()
函数,以便它将网格上传到 GPU 本地缓冲区,以获得最佳速度。
首先,我们需要分配一个 CPU 端缓冲区来保存网格数据,然后再将其上传到 GPU 缓冲区。
void VulkanEngine::upload_mesh(Mesh& mesh)
{
const size_t bufferSize= mesh._vertices.size() * sizeof(Vertex);
//allocate staging buffer
VkBufferCreateInfo stagingBufferInfo = {};
stagingBufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
stagingBufferInfo.pNext = nullptr;
stagingBufferInfo.size = bufferSize;
stagingBufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
//let the VMA library know that this data should be on CPU RAM
VmaAllocationCreateInfo vmaallocInfo = {};
vmaallocInfo.usage = VMA_MEMORY_USAGE_CPU_ONLY;
AllocatedBuffer stagingBuffer;
//allocate the buffer
VK_CHECK(vmaCreateBuffer(_allocator, &stagingBufferInfo, &vmaallocInfo,
&stagingBuffer._buffer,
&stagingBuffer._allocation,
nullptr));
}
我们正在创建 stagingBuffer,其大小足以容纳网格数据,并为其赋予 VK_BUFFER_USAGE_TRANSFER_SRC_BIT
用法标志。此标志告诉 Vulkan,此缓冲区将仅用作传输命令的源。我们将不会将暂存缓冲区用于渲染。
我们现在可以将网格数据复制到此缓冲区
//copy vertex data
void* data;
vmaMapMemory(_allocator, stagingBuffer._allocation, &data);
memcpy(data, mesh._vertices.data(), mesh._vertices.size() * sizeof(Vertex));
vmaUnmapMemory(_allocator, stagingBuffer._allocation);
与往常一样,类似的 map/unmap 缓冲区逻辑。这与上一个版本的 upload_mesh()
相比没有变化。
顶点缓冲区现在位于 Vulkan CPU 端缓冲区中,我们需要创建实际的 GPU 端缓冲区。
//allocate vertex buffer
VkBufferCreateInfo vertexBufferInfo = {};
vertexBufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
vertexBufferInfo.pNext = nullptr;
//this is the total size, in bytes, of the buffer we are allocating
vertexBufferInfo.size = bufferSize;
//this buffer is going to be used as a Vertex Buffer
vertexBufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
//let the VMA library know that this data should be GPU native
vmaallocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
//allocate the buffer
VK_CHECK(vmaCreateBuffer(_allocator, &vertexBufferInfo, &vmaallocInfo,
&mesh._vertexBuffer._buffer,
&mesh._vertexBuffer._allocation,
nullptr));
我们为缓冲区赋予 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
和 VK_BUFFER_USAGE_TRANSFER_DST_BIT
用法标志,以便驱动程序知道我们将使用它来渲染网格和复制数据到其中。我们还通过使用 VMA_MEMORY_USAGE_GPU_ONLY
内存类型来确保 VMA 在 GPU VRAM 上分配它。
创建缓冲区后,我们现在可以执行复制命令。
immediate_submit([=](VkCommandBuffer cmd) {
VkBufferCopy copy;
copy.dstOffset = 0;
copy.srcOffset = 0;
copy.size = bufferSize;
vkCmdCopyBuffer(cmd, stagingBuffer._buffer, mesh._vertexBuffer._buffer, 1, ©);
});
我们使用 immediate_submit 函数来排队 vkCmdCopyBuffer()
命令。此命令将使用 VkBufferCopy
获取每个区域的详细信息,将一个缓冲区的区域复制到另一个缓冲区。在此处,我们将整个暂存缓冲区复制到顶点缓冲区中。
内存上传完成后,我们可以清理。
//add the destruction of mesh buffer to the deletion queue
_mainDeletionQueue.push_function([=]() {
vmaDestroyBuffer(_allocator, mesh._vertexBuffer._buffer, mesh._vertexBuffer._allocation);
});
vmaDestroyBuffer(_allocator, stagingBuffer._buffer, stagingBuffer._allocation);
对于 GPU 顶点缓冲区,我们将其添加到删除队列,但对于暂存缓冲区,我们将在完成操作后立即删除它。
尝试立即运行引擎,一切都应该完全正常工作。
使用这个新的网格加载代码,即使上传数千万个三角形的网格,您也将获得出色的性能。
所有用于复制数据的逻辑都已为网格完成,因此现在可以继续处理纹理了。
下一步:Vulkan 图像