无法与渲染并行获取和呈现?
Impossible to acquire and present in parallel with rendering?
注意:我正在自学 Vulkan,对现代 OpenGL 知之甚少。
阅读 Vulkan 规范,我可以看到非常好的信号量,它允许命令缓冲区和交换链同步。这是我理解的一种简单(但我认为效率低下)的做事方式:
- 使用
vkAcquireNextImageKHR
获取图像,发信号 sem_post_acq
- 构建命令缓冲区(或使用预构建):
- 从
VK_IMAGE_LAYOUT_UNDEFINED
过渡图像的图像障碍
- 渲染
- 将图像转换为
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
的图像障碍
- 提交到队列,在片段阶段等待
sem_post_acq
并发出信号 sem_pre_present
。
vkQueuePresentKHR
正在等待 sem_pre_present
。
这里的问题是命令缓冲区中的图像屏障必须知道它们正在转换哪个图像,这意味着 vkAcquireNextImageKHR
必须 return 才能知道如何构建命令缓冲区(或要提交的预建命令缓冲区)。但是 vkAcquireNextImageKHR
可能会睡很多(因为演示引擎很忙,没有免费图像)。另一方面,命令缓冲区的提交本身是昂贵的,更重要的是,片段之前的所有阶段都可以 运行 而不知道最终结果将渲染到哪个图像。
理论上,在我看来,像下面这样的方案将允许更高的并行度:
- 构建命令缓冲区(或使用预构建):
- 从
VK_IMAGE_LAYOUT_UNDEFINED
过渡图像的图像障碍
- 渲染
- 将图像转换为
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
的图像障碍
- 提交到队列,在片段阶段等待
sem_post_acq
并发出信号 sem_pre_present
。
- 使用
vkAcquireNextImageKHR
获取图像,发信号 sem_post_acq
vkQueuePresentKHR
正在等待 sem_pre_present
。
从理论上讲,这将允许管道一直执行到片段着色器,而我们等待 vkAcquireNextImageKHR
。这不起作用的唯一原因是既不可能告诉命令缓冲区稍后将确定此图像(通过适当的同步),也不可能向表示引擎询问特定图像。
我的第一个问题是:我的分析是否正确?如果是这样,这样的优化在 Vulkan 中是不可能的吗?为什么不呢?
我的第二个问题是:如果您可以告诉 vkAcquireNextImageKHR
您想要获取哪个特定图像,然后自己遍历它们,这不是更有意义吗?这样,您就可以提前知道您要请求哪个图像,并相应地构建和提交您的命令缓冲区。
您的整个问题都基于这样的假设:如果没有特定的交换链图像,您将无法进行任何命令缓冲区构建工作。这根本不是真的。
首先,你总是可以建立二级命令缓冲区;提供 VkFramebuffer
只是出于礼貌,并非强制要求。如果您想使用 Vulkan 提高 CPU 性能,这一点非常重要。毕竟,能够并行构建命令缓冲区是 Vulkan 的卖点之一。对于注重性能的应用程序来说,只创建一个是一种浪费。
在这种情况下,只有主命令缓冲区需要实际图像。
其次,谁说您将对可呈现的图像进行大部分渲染?如果你正在做延迟渲染,你的大部分内容将被写入延迟缓冲区。甚至 post 色调映射、SSAO 等处理效果也可能会在中间缓冲区中完成。
最坏的情况,你总是可以渲染到你自己的图像。然后你构建一个命令缓冲区,它的唯一内容是从你的图像到可展示的图像的图像副本。
all stages before fragment can run without having any knowledge of which image the final result will be rendered to.
您假设硬件严格区分顶点处理和光栅化。这仅适用于基于图块的硬件。
直接渲染器只为每个渲染命令从上到下执行整个管道。它们不会将 post 转换后的顶点数据存储在大缓冲区中。它只是流向下一步。因此,如果 "fragment stage" 必须等待信号量,那么您可以假设所有其他阶段在等待时也将空闲。
wouldn't it have made more sense if you could tell vkAcquireNextImageKHR which particular image you want to acquire, and iterate through them yourself?
没有。该实现将无法决定接下来给您哪个图像。这正是您必须要求图像的原因:这样实现可以自行确定您拥有哪个图像是安全的。
此外,规范中有特定的语言,您提供的信号量 and/or 事件不仅必须未发出信号,而且不能有任何未完成的操作在等待它们。为什么?
因为vkAcquireNextImageKHR
可以失败。如果您在等待永远不会触发的信号量的队列中有一些操作,那将导致巨大的问题。必须先获取成功,再提交基于信号量的作品。
一般来说,如果您经常无法及时获得可展示的图像,则需要延长您的交换链。毕竟,这就是拥有多个缓冲区的意义所在。
就像 Nicol 所说的那样,您可以独立于将要渲染到的图像来记录辅助图像。
然而,您可以更进一步,提前记录所有 swpachain 图像的命令缓冲区,并select 从获取的图像中提交正确的图像。
这种类型的重用确实需要一些额外的考虑,因为所有使用的内存范围都被写入命令缓冲区。但在许多情况下,所需的渲染命令实际上并不会一帧一帧地改变,只是使用了一点点数据。
所以这样一帧的顺序是:
vkAcquireNextImageKHR(vk.dev, vk.swap, 0, vk.acquire, VK_NULL_HANDLE, &vk.image_ind);
vkWaitForFences(vk.dev, 1, &vk.fences[vk.image_ind], true, ~0);
engine_update_render_data(vk.mapped_staging[vk.image_ind]);
VkSubmitInfo submit = build_submit(vk.acquire, vk.rend_cmd[vk.image_ind], vk.present);
vkQueueSubmit(vk.rend_queue, 1, &submit, vk.fences[vk.image_ind]);
VkPresentInfoKHR present = build_present(vk.present, vk.swap, vk.image_ind);
vkQueuePresentKHR(vk.queue, &present);
虽然这不允许有条件的渲染,但 gpu 通常速度足够快,可以让一些几何图形在帧外渲染而没有任何明显的延迟。因此,在玩家到达必须显示新几何体的加载区之前,您可以让这些命令缓冲区保持活动状态。
注意:我正在自学 Vulkan,对现代 OpenGL 知之甚少。
阅读 Vulkan 规范,我可以看到非常好的信号量,它允许命令缓冲区和交换链同步。这是我理解的一种简单(但我认为效率低下)的做事方式:
- 使用
vkAcquireNextImageKHR
获取图像,发信号sem_post_acq
- 构建命令缓冲区(或使用预构建):
- 从
VK_IMAGE_LAYOUT_UNDEFINED
过渡图像的图像障碍
- 渲染
- 将图像转换为
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
的图像障碍
- 从
- 提交到队列,在片段阶段等待
sem_post_acq
并发出信号sem_pre_present
。 vkQueuePresentKHR
正在等待sem_pre_present
。
这里的问题是命令缓冲区中的图像屏障必须知道它们正在转换哪个图像,这意味着 vkAcquireNextImageKHR
必须 return 才能知道如何构建命令缓冲区(或要提交的预建命令缓冲区)。但是 vkAcquireNextImageKHR
可能会睡很多(因为演示引擎很忙,没有免费图像)。另一方面,命令缓冲区的提交本身是昂贵的,更重要的是,片段之前的所有阶段都可以 运行 而不知道最终结果将渲染到哪个图像。
理论上,在我看来,像下面这样的方案将允许更高的并行度:
- 构建命令缓冲区(或使用预构建):
- 从
VK_IMAGE_LAYOUT_UNDEFINED
过渡图像的图像障碍
- 渲染
- 将图像转换为
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
的图像障碍
- 从
- 提交到队列,在片段阶段等待
sem_post_acq
并发出信号sem_pre_present
。 - 使用
vkAcquireNextImageKHR
获取图像,发信号sem_post_acq
vkQueuePresentKHR
正在等待sem_pre_present
。
从理论上讲,这将允许管道一直执行到片段着色器,而我们等待 vkAcquireNextImageKHR
。这不起作用的唯一原因是既不可能告诉命令缓冲区稍后将确定此图像(通过适当的同步),也不可能向表示引擎询问特定图像。
我的第一个问题是:我的分析是否正确?如果是这样,这样的优化在 Vulkan 中是不可能的吗?为什么不呢?
我的第二个问题是:如果您可以告诉 vkAcquireNextImageKHR
您想要获取哪个特定图像,然后自己遍历它们,这不是更有意义吗?这样,您就可以提前知道您要请求哪个图像,并相应地构建和提交您的命令缓冲区。
您的整个问题都基于这样的假设:如果没有特定的交换链图像,您将无法进行任何命令缓冲区构建工作。这根本不是真的。
首先,你总是可以建立二级命令缓冲区;提供 VkFramebuffer
只是出于礼貌,并非强制要求。如果您想使用 Vulkan 提高 CPU 性能,这一点非常重要。毕竟,能够并行构建命令缓冲区是 Vulkan 的卖点之一。对于注重性能的应用程序来说,只创建一个是一种浪费。
在这种情况下,只有主命令缓冲区需要实际图像。
其次,谁说您将对可呈现的图像进行大部分渲染?如果你正在做延迟渲染,你的大部分内容将被写入延迟缓冲区。甚至 post 色调映射、SSAO 等处理效果也可能会在中间缓冲区中完成。
最坏的情况,你总是可以渲染到你自己的图像。然后你构建一个命令缓冲区,它的唯一内容是从你的图像到可展示的图像的图像副本。
all stages before fragment can run without having any knowledge of which image the final result will be rendered to.
您假设硬件严格区分顶点处理和光栅化。这仅适用于基于图块的硬件。
直接渲染器只为每个渲染命令从上到下执行整个管道。它们不会将 post 转换后的顶点数据存储在大缓冲区中。它只是流向下一步。因此,如果 "fragment stage" 必须等待信号量,那么您可以假设所有其他阶段在等待时也将空闲。
wouldn't it have made more sense if you could tell vkAcquireNextImageKHR which particular image you want to acquire, and iterate through them yourself?
没有。该实现将无法决定接下来给您哪个图像。这正是您必须要求图像的原因:这样实现可以自行确定您拥有哪个图像是安全的。
此外,规范中有特定的语言,您提供的信号量 and/or 事件不仅必须未发出信号,而且不能有任何未完成的操作在等待它们。为什么?
因为vkAcquireNextImageKHR
可以失败。如果您在等待永远不会触发的信号量的队列中有一些操作,那将导致巨大的问题。必须先获取成功,再提交基于信号量的作品。
一般来说,如果您经常无法及时获得可展示的图像,则需要延长您的交换链。毕竟,这就是拥有多个缓冲区的意义所在。
就像 Nicol 所说的那样,您可以独立于将要渲染到的图像来记录辅助图像。
然而,您可以更进一步,提前记录所有 swpachain 图像的命令缓冲区,并select 从获取的图像中提交正确的图像。
这种类型的重用确实需要一些额外的考虑,因为所有使用的内存范围都被写入命令缓冲区。但在许多情况下,所需的渲染命令实际上并不会一帧一帧地改变,只是使用了一点点数据。
所以这样一帧的顺序是:
vkAcquireNextImageKHR(vk.dev, vk.swap, 0, vk.acquire, VK_NULL_HANDLE, &vk.image_ind);
vkWaitForFences(vk.dev, 1, &vk.fences[vk.image_ind], true, ~0);
engine_update_render_data(vk.mapped_staging[vk.image_ind]);
VkSubmitInfo submit = build_submit(vk.acquire, vk.rend_cmd[vk.image_ind], vk.present);
vkQueueSubmit(vk.rend_queue, 1, &submit, vk.fences[vk.image_ind]);
VkPresentInfoKHR present = build_present(vk.present, vk.swap, vk.image_ind);
vkQueuePresentKHR(vk.queue, &present);
虽然这不允许有条件的渲染,但 gpu 通常速度足够快,可以让一些几何图形在帧外渲染而没有任何明显的延迟。因此,在玩家到达必须显示新几何体的加载区之前,您可以让这些命令缓冲区保持活动状态。