如何更新 vulkan 中每一帧的纹理?
How to update texture for every frame in vulkan?
正如我的问题标题所说,我想为每一帧更新纹理。
我有一个想法:
使用以下配置创建 VkImage
作为纹理缓冲区:
initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED
usage= VK_IMAGE_USAGE_SAMPLED_BIT
它的内存类型是VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
在绘图循环中:
第一帧:
- 将纹理数据映射到
VkImage
(使用 vkMapMemory
)。
- 将
VkImage
布局从 VK_IMAGE_LAYOUT_PREINITIALIZED
更改为 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
。
- 将此
VkImage
用作纹理缓冲区。
第二帧:
布局将在第一帧之后 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
,我可以将下一个纹理数据直接映射到这个 VkImage
而不改变它的布局吗?如果我做不到,我可以将此 VkImage
更改为哪种布局?
在 vkspec 11.4 中它说:
The new layout used in a
transition must not be VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED
所以,我无法将布局更改回 _PREINITIALIZED
。
任何帮助将不胜感激。
你这里有很多问题。
第一个:
create a VkImage as a texture buffer
没有这样的事情。 OpenGL 缓冲区纹理的等效项是 Vulkan 缓冲区视图。这不使用任何类型的 VkImage
。 VkBufferView
s 没有图像布局。
其次,假设您正在使用某种 VkImage
,您已经认识到布局问题。除非纹理位于 GENERAL
布局中(除其他外),否则您无法修改纹理背后的内存。所以你必须强制转换到那个,等到转换命令真正完成执行,然后再做你的修改。
第三,Vulkan 的执行是异步的,与 OpenGL 不同,它不会向您隐藏这一点。当您想要更改它时,着色器可能仍会访问有问题的图像。所以通常,你需要对这些东西进行双重缓冲。
在第 1 帧上,您设置图像 1 的数据,然后使用它进行渲染。在第 2 帧上,您设置图像 2 的数据,然后使用它进行渲染。在第 3 帧上,您覆盖图像 1 的数据(使用事件确保 GPU 实际上已完成第 1 帧)。
或者,您可以使用暂存缓冲区来使用双缓冲而无需等待 CPU。也就是说,您不是直接写入图像,而是写入主机可见内存。然后使用 vkCmdCopyBufferToImage
命令将该数据复制到图像中。这样,CPU 就不必等待事件或围栏来确保图像在发送数据之前位于 GENERAL
布局中。
顺便说一句,Vulkan 不是 OpenGL。内存映射总是持久的;如果您要在每一帧都映射一块内存,则没有理由取消映射它。
对于您的情况,您不需要 LAYOUT_PREINITIALIZED
。那只会使您的代码复杂化(迫使您为第一帧提供单独的代码)。
LAYOUT_PREINITIALIZED
确实是一个非常特殊的布局,仅用于图像生命周期的开始。它对静态纹理更有用。
从LAYOUT_UNDEFINED
开始,当你需要从CPU端写图像时使用LAYOUT_GENERAL
。
我提出这个方案:
渲染循环之前
- 用
UNDEFINED
创建您的 VkImage
第 1 到第 N 帧(又名渲染循环)
- 将图像转换为
GENERAL
- 同步(可能与
VkFence
)
- 映射图像,写入,取消映射(好吧,映射和取消映射可能在渲染循环之外)
- 同步(可能隐式完成)
- 将图像过渡到您接下来需要的任何布局
- 进行渲染等等
- 从 1 开始。
这是一个简单的实现,但应该足以满足普通爱好者的使用。
可以实现双缓冲访问——例如VkBuffer
用于 CPU 访问,VkImage
用于 GPU 访问。并且VkCmdCopy*
必须完成数据交接。
它并不比上述方法复杂多少,并且可以带来一些性能优势(如果您在项目阶段需要这些优势)。您通常希望您的资源位于设备本地内存中,而这通常也不是主机可见的。
它会是这样的:
渲染循环之前
- 创建你的
VkBuffer
b
UNDEFINED
由 HOST_VISIBLE
内存支持并映射它
- 创建你的
VkImage
i
UNDEFINED
由 DEVICE_LOCAL
记忆
- 准备
i
和 b
之间的同步原语:例如如果传输在同一个队列中,则可以使用两个信号量或事件或障碍
第 1 到第 N 帧(又名渲染循环)
b
和 i
上的操作可以完全分离(甚至可以在不同的队列上)所以:
对于b
:
- 过渡
b
到 GENERAL
- 同步到 CPU(可能等待
VkFence
或 vkQueueIdle
)
- 无效(如果不相干),写入,刷新(如果不相干)
- 同步到 GPU(如果 3. 在队列提交之前隐式完成)
- 过渡
b
到 TRANSFER
- 同步以确保
i
未在使用中(可能正在等待 VkSemaphore
)
- 过渡
i
到 TRANSFER
vkCmdCopy*
从 b
到 i
- 同步以告知我已完成
i
(可能发出 VkSemaphore
信号)
- 从 1 开始。
(第 2 处的栅栏和第 6 处的信号灯必须预先发出信号或跳过第一帧才能工作)
对于i
:
- 同步以确保
i
可以免费使用(可能正在等待 VkSemaphore
)
- 过渡
i
到任何需要的
- 进行渲染
- 同步以告知我已完成
i
(可能发出 VkSemaphore
信号)
- 从 1 开始。
正如我的问题标题所说,我想为每一帧更新纹理。
我有一个想法:
使用以下配置创建 VkImage
作为纹理缓冲区:
initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED
usage= VK_IMAGE_USAGE_SAMPLED_BIT
它的内存类型是VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
在绘图循环中:
第一帧:
- 将纹理数据映射到
VkImage
(使用vkMapMemory
)。 - 将
VkImage
布局从VK_IMAGE_LAYOUT_PREINITIALIZED
更改为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
。 - 将此
VkImage
用作纹理缓冲区。
第二帧:
布局将在第一帧之后 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
,我可以将下一个纹理数据直接映射到这个 VkImage
而不改变它的布局吗?如果我做不到,我可以将此 VkImage
更改为哪种布局?
在 vkspec 11.4 中它说:
The new layout used in a transition must not be VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED
所以,我无法将布局更改回 _PREINITIALIZED
。
任何帮助将不胜感激。
你这里有很多问题。
第一个:
create a VkImage as a texture buffer
没有这样的事情。 OpenGL 缓冲区纹理的等效项是 Vulkan 缓冲区视图。这不使用任何类型的 VkImage
。 VkBufferView
s 没有图像布局。
其次,假设您正在使用某种 VkImage
,您已经认识到布局问题。除非纹理位于 GENERAL
布局中(除其他外),否则您无法修改纹理背后的内存。所以你必须强制转换到那个,等到转换命令真正完成执行,然后再做你的修改。
第三,Vulkan 的执行是异步的,与 OpenGL 不同,它不会向您隐藏这一点。当您想要更改它时,着色器可能仍会访问有问题的图像。所以通常,你需要对这些东西进行双重缓冲。
在第 1 帧上,您设置图像 1 的数据,然后使用它进行渲染。在第 2 帧上,您设置图像 2 的数据,然后使用它进行渲染。在第 3 帧上,您覆盖图像 1 的数据(使用事件确保 GPU 实际上已完成第 1 帧)。
或者,您可以使用暂存缓冲区来使用双缓冲而无需等待 CPU。也就是说,您不是直接写入图像,而是写入主机可见内存。然后使用 vkCmdCopyBufferToImage
命令将该数据复制到图像中。这样,CPU 就不必等待事件或围栏来确保图像在发送数据之前位于 GENERAL
布局中。
顺便说一句,Vulkan 不是 OpenGL。内存映射总是持久的;如果您要在每一帧都映射一块内存,则没有理由取消映射它。
对于您的情况,您不需要 LAYOUT_PREINITIALIZED
。那只会使您的代码复杂化(迫使您为第一帧提供单独的代码)。
LAYOUT_PREINITIALIZED
确实是一个非常特殊的布局,仅用于图像生命周期的开始。它对静态纹理更有用。
从LAYOUT_UNDEFINED
开始,当你需要从CPU端写图像时使用LAYOUT_GENERAL
。
我提出这个方案:
渲染循环之前
- 用
UNDEFINED
创建您的
VkImage
第 1 到第 N 帧(又名渲染循环)
- 将图像转换为
GENERAL
- 同步(可能与
VkFence
) - 映射图像,写入,取消映射(好吧,映射和取消映射可能在渲染循环之外)
- 同步(可能隐式完成)
- 将图像过渡到您接下来需要的任何布局
- 进行渲染等等
- 从 1 开始。
这是一个简单的实现,但应该足以满足普通爱好者的使用。
可以实现双缓冲访问——例如VkBuffer
用于 CPU 访问,VkImage
用于 GPU 访问。并且VkCmdCopy*
必须完成数据交接。
它并不比上述方法复杂多少,并且可以带来一些性能优势(如果您在项目阶段需要这些优势)。您通常希望您的资源位于设备本地内存中,而这通常也不是主机可见的。
它会是这样的:
渲染循环之前
- 创建你的
VkBuffer
b
UNDEFINED
由HOST_VISIBLE
内存支持并映射它 - 创建你的
VkImage
i
UNDEFINED
由DEVICE_LOCAL
记忆 - 准备
i
和b
之间的同步原语:例如如果传输在同一个队列中,则可以使用两个信号量或事件或障碍
第 1 到第 N 帧(又名渲染循环)
b
和 i
上的操作可以完全分离(甚至可以在不同的队列上)所以:
对于b
:
- 过渡
b
到GENERAL
- 同步到 CPU(可能等待
VkFence
或vkQueueIdle
) - 无效(如果不相干),写入,刷新(如果不相干)
- 同步到 GPU(如果 3. 在队列提交之前隐式完成)
- 过渡
b
到TRANSFER
- 同步以确保
i
未在使用中(可能正在等待VkSemaphore
) - 过渡
i
到TRANSFER
vkCmdCopy*
从b
到i
- 同步以告知我已完成
i
(可能发出VkSemaphore
信号) - 从 1 开始。
(第 2 处的栅栏和第 6 处的信号灯必须预先发出信号或跳过第一帧才能工作)
对于i
:
- 同步以确保
i
可以免费使用(可能正在等待VkSemaphore
) - 过渡
i
到任何需要的 - 进行渲染
- 同步以告知我已完成
i
(可能发出VkSemaphore
信号) - 从 1 开始。