【问题标题】:How to update texture for every frame in vulkan?如何更新vulkan中每一帧的纹理?
【发布时间】:2024-01-23 07:54:01
【问题描述】:

正如我的问题标题所说,我想为每一帧更新纹理。

我有个主意: 创建一个VkImage 作为具有以下配置的纹理缓冲区:
initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED
usage= VK_IMAGE_USAGE_SAMPLED_BIT

它的内存类型是VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT

在绘制循环中:

第一帧:

  1. 将纹理数据映射到VkImage(使用vkMapMemory)。
  2. VkImage 布局从VK_IMAGE_LAYOUT_PREINITIALIZED 更改为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
  3. 将此VkImage 用作纹理缓冲区。

第二帧:

第一帧之后的布局将是VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,我可以直接将下一个纹理数据映射到这个VkImage而不改变它的布局吗?如果我不能这样做,我可以将 VkImage 更改为哪个布局?

在 vkspec 11.4 中它说:

a 中使用的新布局 过渡不能是 VK_IMAGE_LAYOUT_UNDEFINED 或 VK_IMAGE_LAYOUT_PREINITIALIZED

所以,我无法将布局改回_PREINITIALIZED
任何帮助将不胜感激。

【问题讨论】:

    标签: graphics rendering vulkan


    【解决方案1】:

    这里有很多问题。

    第一:

    创建一个 VkImage 作为纹理缓冲区

    没有这样的事情。相当于 OpenGL 缓冲区纹理的是 Vulkan 缓冲区视图。这不使用任何类型的VkImageVkBufferViews 没有图片布局。

    其次,假设您正在使用某种VkImage,您已经认识到布局问题。除非纹理位于 GENERAL 布局中(除其他外),否则您无法修改纹理后面的内存。所以你必须强制转换到那个,等到转换命令真正完成执行,然后再做你的修改。

    第三,Vulkan 的执行是异步的,与 OpenGL 不同,它不会对您隐藏这一点。当您想要更改相关图像时,着色器可能仍会访问它。所以通常情况下,你需要对这些东西进行双重缓冲。

    在第 1 帧上,您为图像 1 设置数据,然后使用它进行渲染。在第 2 帧上,您为图像 2 设置数据,然后用它进行渲染。在第 3 帧,您覆盖图像 1 的数据(使用事件确保 GPU 已实际完成第 1 帧)。

    或者,您可以通过使用暂存缓冲区来使用双缓冲,而无需 CPU 等待。也就是说,不是直接写入图像,而是写入主机可见内存。然后使用vkCmdCopyBufferToImage 命令将该数据复制到映像中。这样,CPU 在发送数据之前不必等待事件或栅栏来确保图像在GENERAL 布局中。

    顺便说一句,Vulkan 不是 OpenGL。内存映射总是持久的;如果您要每帧都映射一块内存,就没有理由取消映射它。

    【讨论】:

    • 谢谢,我从您的回答中了解了有关 vulkan 的更多详细信息。图像 1 和图像 2 应在 VkDescriptorSetLayoutBinding 中指定?或者你的意思是存在一个在VkDescriptorSetLayoutBinding中指定的图像3,并带有gpu local memoty?
    • 要修改纹理后面的内存是否还需要使用VK_IMAGE_TILING_LINEAR 创建图像(除了在布局GENERAL 中)?尽管VK_IMAGE_LAYOUT_PREINITIALIZED 暗示了这一点,但还是值得一提。
    【解决方案2】:

    对于您的情况,您不需要LAYOUT_PREINITIALIZED。这只会使您的代码复杂化(迫使您为第一帧提供单独的代码)。

    LAYOUT_PREINITIALIZED 确实是一个非常特殊的布局,仅用于图像生命的开始。它对静态纹理更有用。

    LAYOUT_UNDEFINED开始,当你需要从CPU端写入Image时使用LAYOUT_GENERAL

    我提出这个方案:

    渲染循环之前

    1. 使用UNDEFINED 创建您的VkImage

    第 1 到第 N 帧(又名渲染循环)

    1. 将图像转换为GENERAL
    2. 同步(可能与VkFence
    3. 映射图像、写入图像、取消映射(好吧,映射和取消映射可能在渲染循环之外)
    4. 同步(可能隐式完成)
    5. 将图像转换为您接下来需要的任何布局
    6. 进行渲染等等
    7. 从 1 点重新开始。

    这是一个幼稚的实现,但对于普通爱好者使用应该足够了。

    可以实现双缓冲访问——例如VkBuffer 用于 CPU 访问,VkImage 用于 GPU 访问。并且VkCmdCopy*必须进行数据交接。

    它并不比上述方法复杂得多,并且可能会带来一些性能优势(如果您在项目阶段需要这些优势)。您通常希望您的资源在设备本地内存中,这通常也是主机不可见的。

    它会是这样的:

    渲染循环之前

    1. 使用由HOST_VISIBLE 内存支持的UNDEFINED 创建您的VkBuffer b 并映射它
    2. 使用由DEVICE_LOCAL 内存支持的UNDEFINED 创建您的VkImage i
    3. 准备ib 之间的同步原语:例如如果传输在同一个队列中,则可以使用两个信号量或事件或障碍

    第 1 到第 N 帧(又名渲染循环)

    bi 上的操作可以非常独立(甚至可以在不同的队列中)所以:

    对于b

    1. b 转换为GENERAL
    2. 与 CPU 同步(可能正在等待 VkFencevkQueueIdle
    3. 无效(如果不连贯),写入,刷新(如果不连贯)
    4. 与 GPU 同步(如果 3. 在队列提交之前隐式完成)
    5. b 转换为TRANSFER
    6. 同步以确保 i 未在使用中(可能正在等待 VkSemaphore
    7. i 转换为TRANSFER
    8. bi 执行vkCmdCopy*
    9. 同步让大家知道我已经完成了i(可能是VkSemaphore
    10. 从 1 点重新开始。

    (2 处的栅栏和 6 处的信号量必须预先发出信号或跳过第一帧才能工作)

    对于i

    1. 同步以确保i 可以免费使用(可能正在等待VkSemaphore
    2. i 转换为任何需要的对象
    3. 进行渲染
    4. 同步以告知我已完成i(可能会发出VkSemaphore 的信号)
    5. 从 1 点重新开始。

    【讨论】:

    • 谢谢,我现在了解您的单缓冲区方案,我可以使用VkImageMemoryBarrier 来同步何时更改布局。但是作为双缓冲区(这是我想要使用的方式),我不知道如何在下一帧到来时同步数据复制。此外,双缓冲区意味着两次内存复制(cup memcpy 和 gpu vkCmdCpy*),我的帧来自视频解码器,它经常更新,你确定双缓冲区比单缓冲区性能更好吗?
    • @libei:嗯,单缓冲意味着完全停止 CPU,直到 GPU 渲染帧。因此,除非您有足够的 CPU 时间,否则双缓冲通常是一种合理的性能与内存权衡。这与交换链通常被缓冲的原因完全相同:以便表示系统可以从一个读取,而您渲染到另一个。
    • @NicolBolas:是的,我同意你的看法。实际上,我的 cpu 需要大约 23 毫秒来生成一个新的纹理数据。
    • @libei 你可以在内存映射指针中构建你的数据以避免memcpy 如果可能的话。直接由 GPU 使用主机可见内存本身可能很糟糕(它通常在 CPU 上,因此无论如何都必须隐式完成复制)。 ;;让我们为 DB 案例制定类似的方案......
    • @NicolBolas 不是真的,你仍然可以在 CPU 上做其他工作,不是吗?只是不使用单缓冲资源。
    最近更新 更多