notes/vulkan-and-computer-graphic/timeline-semaphore

Overview

VK_KHR_timeline_semaphore is a core feature of Vulkan 1.2, defines a primitive containing a superset of both the original VkSemaphore and VkFence primitives. In brief, timeline semaphores:

  1. Are a synchronization primitive whose state consists of a monotonically increasing 64-bit integer value.
  2. Enable omnidirectional synchronization between device and host using a single primitive.
  3. Allow wait-before-signal submission order.
  4. Allow applications to ignore signal operations in certain cases.
  5. Eliminate the need to reset after a signal operation before reuse.
  6. Allow multiple wait operations per signal operation.

The Timeline Semaphore API

Creating a timeline semaphore is similar to creating the existing VkSemaphore object, with the new feature commented alongside the following code snippet.

//* Specify the additional state associated with timeline semaphore wait and signal operations.
const uint64_t waitValue = 2; // Wait until semaphore value is >= 2
const uint64_t signalValue = 3; // Set semaphore value to 3

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);

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);

Examples

Example 1: Wait-before-signal

#include <thread>
#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;

// One timeline semaphore object.
VkSemaphore timeline;

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);
}

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);
}

static void thread3()
{
  const uint64_t waitValue3 = 7; // Wait for thread2's CPU work to complete.
  const uint64_t signalValue3 = 8; // Signal completion of all work.

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

  VkSubmitInfo info3;
  info3.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  info3.pNext = &timelineInfo3;
  info3.waitSemaphoreCount = 1;
  info3.pWaitSemaphores = &timeline;
  info3.signalSemaphoreCount  = 1;
  info3.pSignalSemaphores = &timeline;
  // ... Enqueue device work dependent on thread2's CPU work here.
  info3.commandBufferCount = 0;
  info3.pCommandBuffers = 0;

  vkQueueSubmit(queue3, 1, &info3, VK_NULL_HANDLE);
}

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);

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

  // 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;
}

Example 2: Cloth Simulation

Let's look at the CPU version of the cloth simulation.

for physic_mesh in the scene:
    for vertex in the mesh:
        # we need 2 loops because each vertex references other vertices position.
        # First we need to compute the force applied to each vertex,
        compute the force applied to the vertex.
    update the vertex position and store its velocity.
    update the mesh normals and tangents.
    copy the vertices to the GPU.

Implementation

  1. Use a host coherent buffer to update the data on the CPU so that write on the CPU would be visible on the CPU.
  2. Perform simulation on the GPU using a buffer that is marked as device_only.

Reference