【问题标题】:How are you supposed to update a texture per frame in Vulkan?你应该如何在 Vulkan 中更新每帧的纹理?
【发布时间】:2020-06-20 09:56:09
【问题描述】:

我正在尝试在 vulkan 中使用 2D 和 3D。所以现在测试更新每一帧的纹理,无论 2D 发生了什么。我已经得到了一些纹理更新器的工作,问题是它非常慢并且可能不是它应该完成的方式。有没有更好的方法来完成这项工作?该代码基于https://vulkan-tutorial.com/ 代码。

https://vulkan-tutorial.com/code/26_depth_buffering.cpp

    void UpdateTexture()
{
    vkDeviceWaitIdle(device);
    vkFreeMemory(device, textureImageMemory, nullptr);

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
    memcpy(data, pixel2.data(), static_cast<size_t>(imageSize));
    vkUnmapMemory(device, stagingBufferMemory);

    createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);

    transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
    copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
    transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);

    createTextureImageView();
    createDescriptorPool();
    createDescriptorSets();
    createCommandBuffers();
}

【问题讨论】:

    标签: 2d game-engine vulkan texture2d


    【解决方案1】:

    这段代码看起来像是一些 OpenGL 代码的直接翻译,并不是特别好的/现代的 OpenGL 代码。

    这段代码有一个很多错误,但大部分都归结为过度同步。

    首先,您应该始终将任何对vkDeviceWaitIdle 的调用视为错误的做法。唯一的例外是当您准备销毁 VkDevice 本身时。没有其他理由像这样进行完整的 CPU/GPU 同步。

    大概存在这种同步,以便您可以确保 GPU 在修改图像之前已完成使用该图像。这是错误的做法。您应该改为使用多重缓冲。也就是说,您应该有 两张 图像供您使用。一个当前正在渲染过程中使用,而另一个正在转移到。

    您无需进行完整的设备同步,而是与两帧前发送的批次进行同步。也就是说,如果您要传输数据以供第 10 帧使用,那么您必须首先对第 8 帧中发送的批处理执行栅栏同步操作。第 9 帧仍在处理中,但第 8 帧可能由现在。所以同步应该不会造成太大的伤害。

    其次,永远不要在这样的操作中间分配内存。内存在您的应用程序的早期分配,并且您一直分配它直到销毁您的应用程序。如果您需要一个暂存缓冲区,请保留它并在后续帧中重用它。确保预先分配足够的存储空间。

    无论您的createBuffer 电话在做什么,这似乎都是个坏主意。 Vulkan 不是 OpenGL; Vulkan 将内存与出于某种原因使用它的缓冲区/纹理分开。创建隐藏这种分离的 API 基本上会抛弃所有这些。

    同样,永远不要取消映射内存,除非您要销毁该内存对象。 Vulkan (or OpenGL) 中没有问题,可以无限期地映射一段内存。只需映射整个内存范围并保持映射。事实上,你可以直接将映射指针传递给你的图像加载器,这取决于图像加载代码如何写入内存(如果它试图从这个指针读取数据,它们可能会很麻烦)。

    最后,执行传输的命令需要与使用图像的命令同步。这是如何发生的取决于正在使用哪些队列进行传输。

    当然,如果您想要获得最佳性能,您可能需要检查您的实现是否可以读取着色器中的线性图像。如果可以,那么您可能根本不需要分期;你可以直接将数据以Vulkan的图像格式写入内存,直接使用。

    采用上述所有方法会给您的应用程序增加很多复杂性。但这就是它应该如何工作的方式。

    【讨论】:

    • 绝对同意这里的一切,但只是补充一点,OP 还应该首先检查他是否真的需要将数据从 CPU 传输到纹理。没有提供关于为什么他需要每帧从 CPU 传输到 GPU 的 2D 数据的信息,但很有可能使用渲染到纹理上的绘制调用在 GPU 上生成 2D 数据.如果它是用例的可行选项,那么它可能是最有效的选项。
    • 现在我正在考虑使用巨大的地图纹理,然后将地图的一部分复制到屏幕纹理以开始测试。但是我可能会使用一些背景层(纹理),一些精灵层,如果需要的话可能会使用前景层。然后使用着色器将它们组合到屏幕图像中。问题是我需要更新每帧的图层,因为动画、相机移动以及基本示例中我没有想到的任何其他东西。
    • 你能否设计成每帧更新元数据,而不是纹理本身? “巨大”到底有多大?
    • @solidpixel:“元数据”是什么意思?如果此数据是图像视图的一部分,那么您可以每帧创建新视图(尽管您可能不应该这样做)。
    • @solidpixel 帧大小为 4k。我发现了一些关于精灵纹理图集的信息。我可能也会将其用于地图本身。我想我会朝那个方向看看。snorristurluson.github.io/TextureAtlassnorristurluson.github.io/TextureAtlas
    【解决方案2】:

    一种简单的方法是使用 CPU 根据时间或数据定义更新,然后为着色器更新数据,例如 MVP 转换矩阵。但这在很多同步和太低的刷新率的情况下效率低下,并且还会在循环中使 cpu 过载。

    所以人们建议使用许多缓冲区,有时会提到旧驱动程序。如果有人能澄清一下,那就太好了。我有一个幼稚且可能是错误的猜测。如果他们确切地知道帧速率,那么他们可以计算每帧的时间并提前调度几个帧。但这让我感到困惑,因为帧速率是动态的,尤其是对于具有动态刷新率的 FreeSync 功能的新屏幕。

    我想到了第三种可能性。可以直接在着色器中使用时钟。 GL_EXT_shader_realtime_clock 提供clockRealtimeEXT。它没有定义单位,当超过最大值时会换行。但据说“GPU 上的所有调用都是全局一致的”。在初始化期间,您可以使用统一缓冲区测量其速率,然后假设该速率将是恒定的。并管理包装。

    然后,如果您可以将着色器编写为时间的函数,例如在翻译中,那将是有效的。您只需要初始数据。请记住,必须避免着色器中的 if 条件。

    【讨论】:

      猜你喜欢
      • 2017-03-27
      • 1970-01-01
      • 2016-08-30
      • 1970-01-01
      • 1970-01-01
      • 2022-08-17
      • 1970-01-01
      • 2020-12-16
      • 2019-11-20
      相关资源
      最近更新 更多