Vulkan 时间线信号量

Number of views 59

Vulkan 最开始使用的同步 API 依赖于两种独立的粗粒度同步调用:VkSemaphore 和 VkFence。这两种都是可复用的二进制状态对象,但它们的目的和行为略有不同。VkSemaphore 允许应用程序在设备队列之间的GPU同步操作,而 VkFence 则定位于从GPU到CPU的同步。通过这两者,应用程序能够观察和控制命令缓冲区及其他队列命令的执行,但它们也被当时底层操作系统和设备机制的各种限制,使得使用起来有些困难。

作为 VK_KHR_timeline_semaphore 的一部分发布,并且是 Vulkan 1.2 的核心特性之一,新的时间线信号量同步 API 定义了一种包含原有 VkSemaphore 和 VkFence 同步API功能超集的新型API,同时消除了许多以前 API 中最令人头疼的限制。简而言之,时间线信号量具有以下特点:

  • 是一种其状态由一个单调递增的64位整数值构成的同步API
  • 使用单一同步API实现GPU与CPU之间的全方位同步
  • 支持先等待后信号的提交顺序
  • 在某些情况下允许应用程序忽略信号操作
  • 消除了在重用前需要在信号操作之后重置的需求
  • 每次信号操作可以有多个等待操作

现在,大多数 Vulkan 队列操作都可以接受二进制同步API(VkSemaphore,VkFence)或时间线信号量,尽管窗口系统的集成 API 目前仍然是一个显著的例外。当在同一工作流中混合使用二进制和时间线信号量时,也有一些重要的注意事项,我们将在下文看到。

Timeline Semaphore(时间线信号量) API

在深入探讨这些特性定义的好处之前,让我们简要了解一下实现这些特性的 API 变更。时间线信号量是现有 VkSemaphore 对象的扩展,因此创建它们的过程与创建普通的或“二进制”VkSemaphore 对象类似:

VkSemaphoreTypeCreateInfo timelineCreateInfo;
timelineCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
timelineCreateInfo.pNext = NULL;
timelineCreateInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
timelineCreateInfo.initialValue = 0;

VkSemaphoreCreateInfo createInfo;
createInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
createInfo.pNext = &timelineCreateInfo;
createInfo.flags = 0;

VkSemaphore timelineSemaphore;
vkCreateSemaphore(dev, &createInfo, NULL, &timelineSemaphore);

请注意,时间线信号量的状态可以定义一个任意的初始值。

与二进制信号量类似,时间线信号量可以在大多数队列操作完成后被信号触发,并且可以在开始大多数队列操作之前等待。为此定义了一个扩展结构,它允许应用程序指定与时间线信号量的等待和信号操作相关联的额外状态:

const uint64_t waitValue = 2; // Wait until semaphore value is >= 2
const uint64_t signalValue = 3; // Set semaphore value to 3

VkTimelineSemaphoreSubmitInfo timelineInfo;
timelineInfo.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO;
timelineInfo.pNext = NULL;
timelineInfo.waitSemaphoreValueCount = 1;
timelineInfo.pWaitSemaphoreValues = &waitValue;
timelineInfo.signalSemaphoreValueCount = 1;
timelineInfo.pSignalSemaphoreValues = &signalValue;

VkSubmitInfo submitInfo;
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.pNext = &timelineInfo;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &timelineSemaphore;
submitInfo.signalSemaphoreCount  = 1;
submitInfo.pSignalSemaphores = &timelineSemaphore;
submitInfo.commandBufferCount = 0;
submitInfo.pCommandBuffers = 0;

vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);

此外,时间线信号量可以直接从主机CPU(Host)进行信号触发:

VkSemaphoreSignalInfo signalInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO,
    .pNext = NULL,
    .semaphore = timelineSemaphore,
    .value = 2, // 设置信号量的新值
};

vkSignalSemaphore(device, &signalInfo);

和 VkFence 类似,时间线信号量也可以直接从主机CPU线程进行等待:

const uint64_t waitValue = 1;

VkSemaphoreWaitInfo waitInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
    .pNext = NULL,
    .flags = 0,
    .semaphoreCount = 1,
    .pSemaphores = &timelineSemaphore,
    .pValues = &waitValue, // 等待直到信号量的值达到或超过此值
};

vkWaitSemaphores(device, &waitInfo, UINT64_MAX); // 超时参数设置为无限期等待

它们的当前值也可以直接从CPU线程查询:

uint64_t value;
vkGetSemaphoreCounterValue(device, timelineSemaphore, &value);

这使得应用程序能够在忙等(busy-wait)和阻塞等待(blocking-wait)算法中都使用时间线信号量。通过这种方式,开发者可以根据具体需求选择最合适的同步策略,提高应用的性能和响应性。

使用时间线信号量

有了这个新的 API,让我们来看一个完整的示例工作流程,该示例使用了时间线信号量所展示的许多新特性:

#include <thread>
#include <vulkan/vulkan_core.h>

// 单个 Vulkan 设备对象。
extern VkDevice dev;

// 来自 VkDevice <dev> 的三个独立的 Vulkan 队列。
extern VkQueue queue1;
extern VkQueue queue2;
extern VkQueue queue3;

// 一个时间线信号量对象。
VkSemaphore timeline;

static void thread1()
{
  const uint64_t waitValue1 = 0; // 无操作等待。值总是 >= 0。
  const uint64_t signalValue1 = 5; // 解除线程2的CPU工作的阻塞。

  VkTimelineSemaphoreSubmitInfo timelineInfo1 = {
    .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
    .pNext = NULL,
    .waitSemaphoreValueCount = 1,
    .pWaitSemaphoreValues = &waitValue1,
    .signalSemaphoreValueCount = 1,
    .pSignalSemaphoreValues = &signalValue1
  };

  VkSubmitInfo info1 = {
    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
    .pNext = &timelineInfo1,
    .waitSemaphoreCount = 1,
    .pWaitSemaphores = &timeline,
    .signalSemaphoreCount = 1,
    .pSignalSemaphores = &timeline,
    .commandBufferCount = 0,
    .pCommandBuffers = 0
  };

  // ... 在这里加入初始的设备工作。
  vkQueueSubmit(queue1, 1, &info1, VK_NULL_HANDLE);
}

static void thread2()
{
  // 等待线程1的设备工作完成。
  const uint64_t waitValue2 = 4;

  VkSemaphoreWaitInfo waitInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
    .pNext = NULL,
    .flags = 0,
    .semaphoreCount = 1,
    .pSemaphores = &timeline,
    .pValues = &waitValue2
  };

  vkWaitSemaphores(dev, &waitInfo, UINT64_MAX);

  // ... 在这里执行依赖于线程1的设备工作的某些CPU操作。

  // 解除线程3的设备工作的阻塞。
  VkSemaphoreSignalInfo signalInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO,
    .pNext = NULL,
    .semaphore = timeline,
    .value = 7
  };

  vkSignalSemaphore(dev, &signalInfo);
}

static void thread3()
{
  const uint64_t waitValue3 = 7; // 等待线程2的CPU工作完成。
  const uint64_t signalValue3 = 8; // 信号所有工作已完成。

  VkTimelineSemaphoreSubmitInfo timelineInfo3 = {
    .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
    .pNext = NULL,
    .waitSemaphoreValueCount = 1,
    .pWaitSemaphoreValues = &waitValue3,
    .signalSemaphoreValueCount = 1,
    .pSignalSemaphoreValues = &signalValue3
  };

  VkSubmitInfo info3 = {
    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
    .pNext = &timelineInfo3,
    .waitSemaphoreCount = 1,
    .pWaitSemaphores = &timeline,
    .signalSemaphoreCount = 1,
    .pSignalSemaphores = &timeline,
    .commandBufferCount = 0,
    .pCommandBuffers = 0
  };

  // ... 在这里加入依赖于线程2的CPU工作的设备工作。
  vkQueueSubmit(queue3, 1, &info3, VK_NULL_HANDLE);
}

int main()
{
  // 创建时间线信号量对象
  VkSemaphoreTypeCreateInfo timelineCreateInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
    .pNext = NULL,
    .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE,
    .initialValue = 0
  };

  VkSemaphoreCreateInfo createInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
    .pNext = &timelineCreateInfo,
    .flags = 0
  };

  vkCreateSemaphore(dev, &createInfo, NULL, &timeline);

  // 使用时间线信号量启动三个自由运行的CPU线程
  std::thread t1(thread1);
  std::thread t2(thread2);
  std::thread t3(thread3);

  // 使用时间线信号量等待设备和CPU工作完成
  const uint64_t waitValue = 8;

  VkSemaphoreWaitInfo waitInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
    .pNext = NULL,
    .flags = 0,
    .semaphoreCount = 1,
    .pSemaphores = &timeline,
    .pValues = &waitValue
  };

  vkWaitSemaphores(dev, &waitInfo, UINT64_MAX);

  // 销毁时间线信号量对象
  vkDestroySemaphore(dev, timeline, NULL);

  // 清理CPU线程。
  t3.join();
  t2.join();
  t1.join();

  return 0;
}

这个代码示例展示了如何使用 Vulkan 的时间线信号量(Timeline Semaphores)来同步多个 CPU 线程与 GPU 之间的操作。让我们逐步解析这段代码,以帮助理解其工作原理。

  • 初始化
#include <thread>
#include <vulkan/vulkan_core.h>

// 单个 Vulkan 设备对象。
extern VkDevice dev;

// 来自 VkDevice <dev> 的三个独立的 Vulkan 队列。
extern VkQueue queue1;
extern VkQueue queue2;
extern VkQueue queue3;

// 一个时间线信号量对象。
VkSemaphore timeline;
  • 这里定义了单个 Vulkan 设备 dev 和从该设备创建的三个队列 queue1, queue2, queue3。每个队列可以用于提交不同的命令或任务到 GPU。

  • timeline 是一个时间线信号量,它将被用来同步这些任务。

  • 创建时间线信号量

int main()
{
  // 创建时间线信号量对象
  VkSemaphoreTypeCreateInfo timelineCreateInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
    .pNext = NULL,
    .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE,
    .initialValue = 0
  };

  VkSemaphoreCreateInfo createInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
    .pNext = &timelineCreateInfo,
    .flags = 0
  };

  vkCreateSemaphore(dev, &createInfo, NULL, &timeline);

使用 vkCreateSemaphore 函数创建了一个时间线信号量,并将其初始值设为 0。这意味着在任何操作开始之前,所有等待该信号量的操作都会阻塞,直到它的值达到或超过等待的阈值。

  • 启动线程
  // 使用时间线信号量启动三个自由运行的CPU线程
  std::thread t1(thread1);
  std::thread t2(thread2);
  std::thread t3(thread3);
  • 创建了三个线程 t1, t2, t3,它们分别执行 thread1(), thread2(), thread3() 函数中的逻辑。这些线程是并发执行的,但它们通过时间线信号量来协调它们的工作顺序。

  • 线程1的运作

static void thread1()
{
  const uint64_t waitValue1 = 0; // 无操作等待。值总是 >= 0。
  const uint64_t signalValue1 = 5; // 解除线程2的CPU工作的阻塞。

  // ... [创建VkTimelineSemaphoreSubmitInfo和VkSubmitInfo]

  // ... 在这里加入初始的设备工作。
  vkQueueSubmit(queue1, 1, &info1, VK_NULL_HANDLE);
}
  • thread1 会等待时间线信号量的值达到 0(实际上是一个无操作的等待,因为初始值就是 0),然后将信号量的值设置为 5。这表示线程1的GPU工作已完成,可以解除线程2中 CPU 逻辑的阻塞。

  • 线程2: 依赖于线程1的CPU逻辑

static void thread2()
{
  // 等待线程1的设备工作完成。
  const uint64_t waitValue2 = 4;

  // ... [创建VkSemaphoreWaitInfo]

  vkWaitSemaphores(dev, &waitInfo, UINT64_MAX);

  // ... 在这里执行依赖于线程1的设备工作的某些CPU操作。

  // 解除线程3的设备工作的阻塞。
  VkSemaphoreSignalInfo signalInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO,
    .pNext = NULL,
    .semaphore = timeline,
    .value = 7
  };

  vkSignalSemaphore(dev, &signalInfo);
}
  • thread2 会等待时间线信号量的值达到 4,这确保了线程1的设备工作已经完成。之后,thread2 执行一些依赖于线程1的 CPU 操作,并将时间线信号量的值设置为 7,以解除线程3中GPU逻辑的阻塞。

  • 线程3: 依赖于线程2的GPU逻辑

static void thread3()
{
  const uint64_t waitValue3 = 7; // 等待线程2的CPU工作完成。
  const uint64_t signalValue3 = 8; // 信号所有工作已完成。

  // ... [创建VkTimelineSemaphoreSubmitInfo和VkSubmitInfo]

  // ... 在这里加入依赖于线程2的CPU工作的设备工作。
  vkQueueSubmit(queue3, 1, &info3, VK_NULL_HANDLE);
}
  • thread3 会等待时间线信号量的值达到 7,这确保了线程2的 CPU 工作已经完成。之后,thread3 执行一些依赖于线程2的GPU逻辑,并将时间线信号量的值设置为 8,表示所有工作都已完成。

  • 主线程等待所有工作完成

  // 使用时间线信号量等待设备和CPU工作完成
  const uint64_t waitValue = 8;

  VkSemaphoreWaitInfo waitInfo = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
    .pNext = NULL,
    .flags = 0,
    .semaphoreCount = 1,
    .pSemaphores = &timeline,
    .pValues = &waitValue
  };

  vkWaitSemaphores(dev, &waitInfo, UINT64_MAX);

  // 销毁时间线信号量对象
  vkDestroySemaphore(dev, timeline, NULL);

  // 清理CPU线程。
  t3.join();
  t2.join();
  t1.join();

  return 0;
}
  • 主线程会等待时间线信号量的值达到 8,这确保了所有线程的工作都已经完成。之后,主线程销毁时间线信号量并清理所有 CPU 线程。

请注意,在这个例子中,主机上生成的三个线程是以任意顺序运行的。这种CPU侧无需同步的情况是由于时间线信号量支持先等待后信号的操作模式:无论提交顺序如何,时间线信号量的行为,以及对应GPU处理的顺序,都是明确定义的。

此外,这个示例在线程2()的函数调用中包含了CPU逻辑,这些逻辑在CPU与GPU侧被线程1()和线程3()提交的GPU逻辑所对应与锁定,它展示了时间线信号量在GPU到CPU和CPU到GPU之间的同步能力。正如这里展示的,当从CPU发出信号时,必须确保信号量的状态是明确定义的。虽然CPU和GPU的信号操作都是原子性的(是指操作的不可中断性,确保操作要么完全执行,要么根本不执行),但相对于其他CPU或GPU的信号操作,并没有隐含的顺序保证。结合单调性要求,这意味着应用程序必须确保任何先前或并发提交的GPU信号操作与CPU信号之间有明确的顺序关系,如本例中由于之前的CPU等待和随后的GPU等待而实现的那样。进一步地,如果多个线程或进程要在给定的时间线信号量上执行CPU信号操作,应用程序必须确保它们的执行以同样明确的顺序进行,以确保单调值更新。

该示例还展示了时间线信号量状态的灵活性。尽管应用程序必须确保值单调递增即值不能减少),除此之外,这个值是任意的,可以用于超出简单同步之外的目的。例如,应用程序可能会发现使用信号量状态来跟踪单调递增的时间戳值是有用的。

最后,请注意使用时间线信号量本身来决定何时安全地销毁时间线信号量对象。在 Vulkan 1.2 之前,需要一个单独的 VkFence 对象来完成这样的任务。

虽然时间线信号量通常消除了对Vulkan GPU设备工作提交进行CPU侧同步的需求,但在使用它们时有一个许多开发者会遇到的缺点:Vulkan 的窗口系统集成 API 尚不支持时间线信号量,而且时间线信号量的等待前信号行为不会被二进制信号量对象继承。为了说明这一限制的全部影响,让我们再次查看上述示例代码,但这次加入了一个呈现(上屏)步骤。所需的额外代码如下所示(以虚线标出):

#include <thread>
----------
#include <atomic>
----------
#include <vulkan/vulkan_core.h>

// A single Vulkan device object.
extern VkDevice dev;

// Three independent Vulkan queues from VkDevice <dev>.
extern VkQueue queue1;
extern VkQueue queue2;
extern VkQueue queue3;

----------
extern VkQueue queue4;
// Swapchain data
extern VkSwapchainKHR swapchain;
extern uint32_t swapchainImageIndex;
----------

// One timeline semaphore object.
VkSemaphore timeline;

----------
// One binary semaphore object for vkQueuePresent()
VkSemaphore binary;
// Track signal submission count.
std::atomic_uint64_t submitCount(0);
----------

static void thread1()
{
  const uint64_t waitValue1 = 0; // No-op wait. Value is always >= 0.
  const uint64_t signalValue1 = 5; // Unblock thread2's CPU work.

  VkTimelineSemaphoreSubmitInfo timelineInfo1;
  timelineInfo1.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO;
  timelineInfo1.pNext = NULL;
  timelineInfo1.waitSemaphoreValueCount = 1;
  timelineInfo1.pWaitSemaphoreValues = &waitValue1;
  timelineInfo1.signalSemaphoreValueCount = 1;
  timelineInfo1.pSignalSemaphoreValues = &signalValue1;

  VkSubmitInfo info1;
  info1.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  info1.pNext = &timelineInfo1;
  info1.waitSemaphoreCount = 1;
  info1.pWaitSemaphores = &timeline;
  info1.signalSemaphoreCount  = 1;
  info1.pSignalSemaphores = &timeline;
  // ... Enqueue initial device work here.
  info1.commandBufferCount = 0;
  info1.pCommandBuffers = 0;

  vkQueueSubmit(queue1, 1, &info1, VK_NULL_HANDLE);
  
  ----------
  submitCount++;
  ----------
}

static void thread2()
{
  // Wait for thread1's device work to complete.
  const uint64_t waitValue2 = 4;

  VkSemaphoreWaitInfo waitInfo;
  waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
  waitInfo.pNext = NULL;
  waitInfo.flags = 0;
  waitInfo.semaphoreCount = 1;
  waitInfo.pSemaphores = &timeline;
  waitInfo.pValues = &waitValue2;

  vkWaitSemaphores(dev, &waitInfo, UINT64_MAX);

  // ... Perform some CPU work dependent on thread1's device work here.

  // Unblock thread3's device work.
  VkSemaphoreSignalInfo signalInfo;
  signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO;
  signalInfo.pNext = NULL;
  signalInfo.semaphore = timeline;
  signalInfo.value = 7;

  vkSignalSemaphore(dev, &signalInfo);

  ----------
  submitCount++;
  ----------
}

static void thread3()
{
  const uint64_t waitValue3 = 7; // Wait for thread2's CPU work to complete.
  const uint64_t signalValue3 = 8; // Signal completion of pre-present work.
  
  ----------
  const uint64_t signalSemaphoreValues[2] = {
     signalValue3, // value for "timeline"
     0 // ignored, binary semaphore.
  };
  ----------

  VkTimelineSemaphoreSubmitInfo timelineInfo3;
  timelineInfo3.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO;
  timelineInfo3.pNext = NULL;
  timelineInfo3.waitSemaphoreValueCount = 1;
  timelineInfo3.pWaitSemaphoreValues = &waitValue3;
  
  ----------
  timelineInfo3.signalSemaphoreValueCount = 2;
  timelineInfo3.pSignalSemaphoreValues = signalSemaphoreValues;
  const VkSemaphore signalSemaphores[2] = {
    timeline, // Track timeline semaphore work completion
    binary// Unblock presentation
  };
  ----------

  VkSubmitInfo info3;
  info3.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  info3.pNext = &timelineInfo3;
  info3.waitSemaphoreCount = 1;
  info3.pWaitSemaphores = &timeline;
  
  ----------
  info3.signalSemaphoreCount  = 2;
  // 注:当指定的命令缓冲区完成执行时,它将被标记(二进制信号量会被标记为已信号)。如果提供了要发出信号的信号量,则它们定义了一个信号量信号操作。
  info3.pSignalSemaphores = signalSemaphores;
  ----------

  // ... Enqueue device work dependent on thread2's CPU work here.
  info3.commandBufferCount = 0;
  info3.pCommandBuffers = 0;
  vkQueueSubmit(queue3, 1, &info3, VK_NULL_HANDLE);

  ----------
  submitCount++;
  ----------
}

int main()
{
  // Create the timeline semaphore object
  VkSemaphoreTypeCreateInfo timelineCreateInfo;
  timelineCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
  timelineCreateInfo.pNext = NULL;
  timelineCreateInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
  timelineCreateInfo.initialValue = 0;

  VkSemaphoreCreateInfo createInfo;
  createInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
  createInfo.pNext = &timelineCreateInfo;
  createInfo.flags = 0;

  vkCreateSemaphore(dev, &createInfo, NULL, &timeline);

  ----------
  // Create the binary semaphore object
  VkSemaphoreCreateInfo binaryCreateInfo;
  binaryCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
  binaryCreateInfo.pNext = NULL;
  binaryCreateInfo.flags = 0;
  vkCreateSemaphore(dev, &binaryCreateInfo, NULL, &binary);
  ----------

  // Spawn three free-running CPU threads using the timeline semaphore
  std::thread t1(thread1);
  std::thread t2(thread2);
  std::thread t3(thread3);

  ----------
  // Wait for submission of all dependencies of the binary semaphore
  while (submitCount < 3);
  // Present results
  VkPresentInfoKHR presentInfo;
  presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
  presentInfo.pNext = NULL;
  presentInfo.waitSemaphoreCount = 1;
  // 当线程3的二进制信号量执行完一个cmdBuffer时,此时二进制信号量会被标记为已信号
  presentInfo.pWaitSemaphores = &binary;
  presentInfo.swapchainCount = 1;
  presentInfo.pSwapchains = &swapchain;
  presentInfo.pImageIndices = &swapchainImageIndex;
  presentInfo.pResults = NULL;
  vkQueuePresentKHR(queue4, &presentInfo);
  ----------


  // Wait for the device and CPU work using the timeline semaphore to idle
  const uint64_t waitValue = 8;

  VkSemaphoreWaitInfo waitInfo;
  waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
  waitInfo.pNext = NULL;
  waitInfo.flags = 0;
  waitInfo.semaphoreCount = 1;
  waitInfo.pSemaphores = &timeline;
  waitInfo.pValues = &waitValue;

  vkWaitSemaphores(dev, &waitInfo, UINT64_MAX);

  // Destroy the timeline semaphore object
  vkDestroySemaphore(dev, timeline, NULL);

  // Clean up the CPU threads.
  t3.join();
  t2.join();
  t1.join();

  return 0;
}

请注意,应用程序现在需要确保所有工作都已经提交,然后再提交依赖于二进制信号量的呈现工作。这是因为二进制信号量的等待操作不仅要求其对应的信号已被提交,还要求任何可能阻塞该信号操作的工作也已提交。因此,除非必要(如上所述),否则不建议混合使用二进制和时间线信号量。

聪明的读者可能会疑惑为什么这里没有使用第二个时间线信号量对象或 C++11 的原子类型来跟踪主机侧的工作提交。这是为了说明另一个问题:虽然时间线信号量的信号和等待操作本身是原子操作,但时间线信号量 API 并未提供增加或减少的操作。因此,使用时间线信号量将会对主机线程施加不必要的执行顺序约束。另一种替代方案是在主线程中的主机等待操作之后再提交呈现请求。虽然这将保持主机工作线程的自由运行特性,但它会在主线程和设备之间引入一个新的序列化点,从而降低整体并行性。

尽管 API 中仍存在一些不便的限制,时间线信号量编程模型应大大减少对主机侧同步的需求以及 Vulkan 应用程序所需跟踪的同步对象数量,从而减少主机侧停滞和应用程序复杂性,进而提高性能和质量。因此,Vulkan 工作组强烈建议所有开发者在所有粗粒度同步场景中切换到时间线信号量。为了帮助实现这一转换,并考虑到需要支持旧版 Vulkan 实现,Vulkan-ExtensionLayer 项目下提供了一个基于宽松的 Apache 2.0 许可的时间线信号量 API 的 Vulkan 1.1 层实现。

应用程序可以直接将此代码集成到它们的项目中,作为层与应用程序一起分发,或以其他方式利用它,在必要的环境中模拟 Vulkan 1.2 的时间线信号量 API,从而使它们能够在所有环境中使用单一的同步 API。

0 Answers