Ch3-1 同步原语

除去隐式同步外,Vulkan中一共有六种同步方式:
1.等待物理设备/队列闲置
2.栅栏,用于在程序(CPU一侧)和队列间同步
3.信号量,Vulkan1.0以来的二值信号量用于队列之间(可为队列与其自身)同步
4.管线屏障(及可附带的内存屏障),用于执行命令时的同步,同步范围通常为同一队列中的命令之间
5.子通道依赖,相当于在渲染通道的子通道开始和结束时自动执行的内存屏障,见子通道依赖
6.事件,其主要用法类似于拆分成两条命令的管线屏障,同步范围通常为同一队列中的命令之间,也可以是主机(即CPU一侧)与队列之间(但是不安全)

等待物理设备/队列闲置的方法已经在第一章里涵盖,本节不再赘述。这种同步方式开销很大,应当只在需要大规模销毁Vulkan对象时使用。

隐式同步保证

并非所有操作都需要手动同步。
首先需要明确,Vulkan中的命令(特指命令缓冲区中的命令)不会立刻被执行,而是显式地记录下来之后,被提交到队列上执行。
Vulkan官方标准中提到,Vulkan的命令按顺序开始执行,但未必按顺序结束。
然而,尽管很多显卡驱动所提供的Vulkan实现确实会按顺序开始执行,但实际上有些命令可能会被重排,但有些绝对不可能 —— 因为它们根本不会被队列执行。
命令缓冲中的命令可以分为三种性质:动作(action)、状态(state)、同步(synchronization)。
有些命令可能会附带多个性质,比如vkCmdBeginRenderPass(...),它即设置了状态,同时也会在队列上实行子通道依赖(是个同步操作)。
同步命令通常不会被重排,但是本该在子通道依赖后执行的部分操作可能会提前,这是少数例外。
很可能会被重排的是动作命令。
而压根不会被队列执行的是状态命令,状态命令在CPU上切换了状态,而具体的参数会被记录在之后的动作命令中。
因为vkCmdBeginRenderPass(...)涉及到了同步操作,所以在单一命令缓冲区内若有多个渲染通道,每个渲染通道会依序而来。
那么每个渲染通道内的动作命令是否需要同步呢?
如果每一条绘制命令都需要同步,那还得了!
事实是,在同一渲染通道中,无论你的绘制命令何时结束,深度模板测试和混色仍旧按照你提交命令的顺序而来 —— 所以画家算法(从后往前绘制,前面的物体覆盖后面的)依旧有效!

Fence

栅栏(VkFence)用于在程序(CPU一侧)和队列间同步,只有置位(singaled)和未置位(unsingaled)两种状态。
栅栏是一种使用得非常频繁的同步机制,每次提交命令缓冲区都应当至少附带一个栅栏或信号量,而栅栏用的更多一些,因为你能在CPU一侧等待它,或查询其状态。

创建栅栏

vkCreateFence(...)创建栅栏:

VkResult VKAPI_CALL vkCreateFence(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkFenceCreateInfo* pCreateInfo

指向VkFence的创建信息

const VkAllocationCallbacks* pAllocator

VkFence* pFence

若执行成功,将栅栏的handle写入*pFence

struct VkFenceCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_FENCE_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkFenceCreateFlags flags

若填入VK_FENCE_CREATE_SIGNALED_BIT,则以置位状态创建栅栏

等待栅栏被置位

vkWaitForFences(...)等待栅栏被置位:

VkResult VKAPI_CALL vkWaitForFences(...) 的参数说明

VkDevice device

逻辑设备的handle

uint32_t fenceCount

要等待的栅栏的个数

const VkFence* pFences

指向所需等待的栅栏的数组

VkBool32 waitAll

若为VK_TRUE,等待所有栅栏,若为VK_FALSE,只需一个栅栏被置位便结束等待

uint64_t timeout

超时时间,单位为纳秒,若无限制,将其指定为UINT64_MAX

  • 若等待成功,返回VK_SUCCESS,超时则返回VK_TIMEOUT

  • 容许的最大超时时间取决于具体实现,即便将timeout指定为UINT64_MAX,实际的超时时间可能为一有限值。

  • 若命令的计算量过大(或其他各种实现特定的原因),有可能发生逻辑设备丢失,此时函数返回VK_ERROR_DEVICE_LOST

将栅栏重置为未置位状态

vkResetFences(...)将栅栏重置为未置位状态:

VkResult VKAPI_CALL vkResetFences(...) 的参数说明

VkDevice device

逻辑设备的handle

uint32_t fenceCount

要等待的栅栏的个数

const VkFence* pFences

指向所需重置的栅栏的数组

查询栅栏状态

vkGetFenceStatus(...)查询栅栏状态:

VkResult VKAPI_CALL vkGetFenceStatus(...) 的参数说明

VkDevice device

逻辑设备的handle

VkFence fence

要被查询状态的栅栏的handle

  • 该函数的返回值即查询结果,栅栏被置位时返回VK_SUCCESS,未被置位则返回VK_NOT_READY
    逻辑设备丢失时应当返回VK_ERROR_DEVICE_LOST,但根据具体实现,也可能返回上述三个数值中的任意一个。

封装为fence类

VKBase.h,vulkan命名空间中添加以下代码:

class fence {
    VkFence handle = VK_NULL_HANDLE;
public:
    //fence() = default;
    fence(VkFenceCreateInfo& createInfo) {
        Create(createInfo);
    }
    //默认构造器创建未置位的栅栏
    fence(VkFenceCreateFlags flags = 0) {
        Create(flags);
    }
    fence(fence&& other) noexcept { MoveHandle; }
    ~fence() { DestroyHandleBy(vkDestroyFence); }
    //Getter
    DefineHandleTypeOperator;
    DefineAddressFunction;
    //Const Function
    result_t Wait() const {
        VkResult result = vkWaitForFences(graphicsBase::Base().Device(), 1, &handle, false, UINT64_MAX);
        if (result)
            outStream << std::format("[ fence ] ERROR\nFailed to wait for the fence!\nError code: {}\n", int32_t(result));
        return result;
    }
    result_t Reset() const {
        VkResult result = vkResetFences(graphicsBase::Base().Device(), 1, &handle);
        if (result)
            outStream << std::format("[ fence ] ERROR\nFailed to reset the fence!\nError code: {}\n", int32_t(result));
        return result;
    }
    //因为“等待后立刻重置”的情形经常出现,定义此函数
    result_t WaitAndReset() const {
        VkResult result = Wait();
        result || (result = Reset());
        return result;
    }
    result_t Status() const {
        VkResult result = vkGetFenceStatus(graphicsBase::Base().Device(), handle);
        if (result < 0) //vkGetFenceStatus(...)成功时有两种结果,所以不能仅仅判断result是否非0
            outStream << std::format("[ fence ] ERROR\nFailed to get the status of the fence!\nError code: {}\n", int32_t(result));
        return result;
    }
    //Non-const Function
    result_t Create(VkFenceCreateInfo& createInfo) {
        createInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
        VkResult result = vkCreateFence(graphicsBase::Base().Device(), &createInfo, nullptr, &handle);
        if (result)
            outStream << std::format("[ fence ] ERROR\nFailed to create a fence!\nError code: {}\n", int32_t(result));
        return result;
    }
    result_t Create(VkFenceCreateFlags flags = 0) {
        VkFenceCreateInfo createInfo = {
            .flags = flags
        };
        return Create(createInfo);
    }
};
  • 这套教程里,其他类的默认构造函数不会自动创建相应的Vulkan对象。出于方便,且因为可以不指定任何参数,我令fence和semaphore的默认构造函数自动创建相应的Vulkan对象。

Semaphore

信号量有两种:
1.二值信号量,只有置位(singaled)和未置位(unsingaled)两种状态,用于在队列间同步
2.时间线信号量(需Vulkan1.2或相应扩展),即可以计数的信号量
可以在CPU一侧等待、重置时间线信号量,换言之时间线信号量兼具栅栏的功能。
时间线信号量并不完全涵盖二值信号量的作用,在提交命令缓冲区时,时间线信号量可以替代二值信号量,但在渲染循环中获取下一张交换连图像时,或呈现图像时,必须使用二值信号量,见后文。
本套教程不会使用时间线信号量,仅对其创建和使用方式做简单提要。

创建信号量

vkCreateSemaphore(...)创建信号量:

VkResult VKAPI_CALL vkCreateSemaphore(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkSemaphoreCreateInfo* pCreateInfo

指向VkSemaphore的创建信息

const VkAllocationCallbacks* pAllocator

VkSemaphore* pSemaphore

若执行成功,将信号量的handle写入*pSemaphore

struct VkSemaphoreCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkSemaphoreCreateFlags flags

到Vulkan1.3为止没用

要创建时间线信号量,那么需让VkSemaphoreCreateInfo中的pNext指向VkSemaphoreTypeCreateInfo,将其中的semaphoreType成员填写为VK_SEMAPHORE_TYPE_TIMELINE

等待信号量被置位

信号量通常用于三种情形:
1.在渲染循环中获取下一张交换连图像时,可以置位信号量
2.提交命令缓冲区时,可以等待信号量、置位信号量
3.呈现图像时,可以等待信号量
所有等待二值信号量的操作都是由上述情形相关的函数附带的,没有专用于等待二值信号量的函数。
二值信号量在被等待后会自动重置,因此也没有专用于重置二值信号量的函数,而时间线信号量在被等待后数值不变,必须通过vkSignalSemaphore(...)将其手动重置。
下文简单讲解下为何时间线信号量不完全涵盖二值信号量的作用,如果你不打算使用时间线信号量,你可以跳过这一部分。

情形1:在渲染循环中获取下一张交换连图像
通过vkAcquireNextImageKHR(...)获取图像,参见获取交换链图像索引,这个函数没有pNext参数。
Vulkan1.1中提供了vkAcquireNextImage2KHR(...),其参数之一为VkAcquireNextImageInfoKHR类型,该结构体中有pNext,但标准中规定其必须为nullptr(即目前该pNext无用处),因此在渲染循环中获取下一张交换连图像时,必须使用二值信号量。

情形2:提交命令缓冲区
以自Vulkan1.0起提供的vkQueueSubmit(...)为例,参见提交命令缓冲区
如果要使用时间线信号量,须使得VkSubmitInfo的pNext指向VkTimelineSemaphoreSubmitInfoKHR,该结构体记录了VkSubmitInfo中各个需等待的信号量的目标计数值,提供的目标值的总数可以少于需等待信号量的总数,但应该涵盖到最后一个时间线信号量,如果某个目标值对应的信号量是二值信号量,那么该目标值被无视。这意味着,如有必要,在提交命令缓冲区时混用二值信号量和时间线信号量是可行的。

情形3:呈现图像 调用vkQueuePresentKHR(...)呈现图像,具体参见呈现图像
标准中所规定的VkPresentInfoKHR的pNext链中容许的结构体类型中,不包括VkTimelineSemaphoreSubmitInfoKHR,因此呈现图像时只能等待二值信号量,但是可以在vkQueuePresentKHR(...)之前使用vkWaitSemaphores(...)手动等待时间线信号量。

封装为semaphore类

VKBase.h,vulkan命名空间中添加以下代码:

class semaphore {
    VkSemaphore handle = VK_NULL_HANDLE;
public:
    //semaphore() = default;
    semaphore(VkSemaphoreCreateInfo& createInfo) {
        Create(createInfo);
    }
    //默认构造器创建未置位的信号量
    semaphore(/*VkSemaphoreCreateFlags flags*/) {
        Create();
    }
    semaphore(semaphore&& other) noexcept { MoveHandle; }
    ~semaphore() { DestroyHandleBy(vkDestroySemaphore); }
    //Getter
    DefineHandleTypeOperator;
    DefineAddressFunction;
    //Non-const Function
    result_t Create(VkSemaphoreCreateInfo& createInfo) {
        createInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
        VkResult result = vkCreateSemaphore(graphicsBase::Base().Device(), &createInfo, nullptr, &handle);
        if (result)
            outStream << std::format("[ semaphore ] ERROR\nFailed to create a semaphore!\nError code: {}\n", int32_t(result));
        return result;
    }
    result_t Create(/*VkSemaphoreCreateFlags flags*/) {
        VkSemaphoreCreateInfo createInfo = {};
        return Create(createInfo);
    }
};
  • 如前文所言,VkSemaphoreCreateFlags到Vulkan1.3为止没用。VkBufferViewCreateFlagsVkShaderModuleCreateFlagsVkQueryPoolCreateFlags也都是类似的情况。

虽然这套教程里预计不会使用,我在VKBase+.h中也提供了时间线信号量的封装。

Pipeline Barrier

管线屏障(pipeline barrier)是以命令的形式录制在命令缓冲区中的一种同步方式,使用管线屏障不需要创建Vulkan对象。
虽然管线屏障是作为命令录制的,但是其同步范围不只是包含该管线屏障的命令缓冲区,执行该命令缓冲区的队列上的所有命令都能被纳入同步范围。
自Vulkan1.0以来,可以通过vkCmdPipelineBarrier(...)设置管线屏障。从Vulkan1.3开始(在之前版本中也可以通过扩展使用),也可以使用vkCmdPipelineBarrier2(...)。
本套教程就Vulkan1.0版本中的用法进行解说:

void VKAPI_CALL vkCmdPipelineBarrier(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkPipelineStageFlags srcStageMask

源管线阶段,见后文

VkPipelineStageFlags dstStageMask

目标管线阶段,见后文

VkDependencyFlags dependencyFlags

见后文

uint32_t memoryBarrierCount

全局内存屏障的个数

const VkMemoryBarrier* pMemoryBarriers

指向全局内存屏障的数组

uint32_t bufferMemoryBarrierCount

缓冲区内存屏障的个数

const VkBufferMemoryBarrier* pBufferMemoryBarriers

指向缓冲区内存屏障的数组

uint32_t imageMemoryBarrierCount

图像内存屏障的个数

const VkImageMemoryBarrier* pImageMemoryBarriers

指向图像内存屏障的数组

版本要求

VkPipelineStageFlagBits 的枚举项

1.3

VK_PIPELINE_STAGE_NONE 不表示任何阶段(值为0,作用见后文),要使该值有效,须开启硬件特性中的synchronization2

1.0

VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT 表示各类命令的起始

1.0

VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT 表示读取间接绘制命令的参数缓冲区的阶段

1.0

VK_PIPELINE_STAGE_VERTEX_INPUT_BIT 表示图形管线中读取顶点缓冲区和索引缓冲区的阶段

1.0

VK_PIPELINE_STAGE_VERTEX_SHADER_BIT 表示图形管线中的顶点着色器阶段

1.0

VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT 表示图形管线中的细分控制着色器阶段

1.0

VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT 表示图形管线中的细分求值着色器阶段

1.0

VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT 表示图形管线中的几何着色器阶段

1.0

VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT 表示图形管线中的片段着色器阶段

1.0

VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT 表示图形管线中的前期片段测试阶段(发生在片段着色器阶段前),此阶段可读取深度模板值

1.0

VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT 表示图形管线中的后期片段测试阶段(发生在片段着色器阶段后),此阶段可写入深度模板值

1.0

VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 表示图形管线中进行混色,并输出色值的阶段,用vkCmdClearAttachments(...)手动清空颜色/深度模板附件也算在这一阶段。

1.0

VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT 表示计算管线中的计算着色器阶段

1.0

VK_PIPELINE_STAGE_TRANSFER_BIT 表示表示通过数据转移命令进行的写入操作

1.0

VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT 表示各类命令的结束

1.0

VK_PIPELINE_STAGE_HOST_BIT 伪管线阶段,对应主机(指CPU侧)对设备内存和同步对象的读写

1.0

VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT 表示所有图形管线阶段

1.0

VK_PIPELINE_STAGE_ALL_COMMANDS_BIT 表示队列上执行的所有命令涉及的所有阶段

  • 在srcStageMask使用VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT为在先前的命令完成前,阻止后续的所有命令达到dstStageMask所示阶段。
    在dstStageMask使用VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BITVK_PIPELINE_STAGE_NONE为不阻塞后续的命令。

  • 在srcStageMask使用VK_PIPELINE_STAGE_TOP_OF_PIPE_BITVK_PIPELINE_STAGE_NONE为不等待先前的命令。
    在dstStageMask使用VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT为在先前的命令完成srcStageMask所示阶段前,阻塞后续的所有命令。

  • 前文所谓的“不阻塞后续的命令”和“不等待先前的命令”并不意味管线屏障无意义,srcStageMask和dstStageMask限定了队列族所有权转移和图像内存布局转换的时机(相关概念见后文)。

版本要求

VkDependencyFlagBits 的枚举项

1.0

VK_DEPENDENCY_BY_REGION_BIT 表示该管线屏障所定义的依赖是framebuffer-local的

1.1

VK_DEPENDENCY_DEVICE_GROUP_BIT 表示该管线屏障所定义的依赖涉及到多个物理设备

1.1

VK_DEPENDENCY_VIEW_LOCAL_BIT 简短说明见下方链接,对于管线屏障,仅适用于在渲染通道内使用的管线屏障

不考虑各个内存屏障结构体(或者说若memoryBarrierCount、bufferMemoryBarrierCount、imageMemoryBarrierCount皆为0),那么vkCmdPipelineBarrier(...)的效果是:
在执行该命令的队列中,于该命令前(即按提交和录制顺序先于该管线屏障命令)的命令中由srcStageMask注明的阶段,在该命令后的命令中由dstStageMask注明的阶段前完成,这意味着管线屏障的作用之一是满足执行依赖。简而言之,管线屏障注明其前后的命令在执行时,有哪些阶段不能重叠
你能在Ch7-6 拷贝图像到屏幕找到解说较为详细的使用例。

在渲染通道中使用管线屏障时,必须在一个自依赖的子通道中使用,且由srcStageMask、dstStageMask和srcAccessMask、dstAccessMask(见后文内存屏障)指定的同步范围必须是子通道自依赖中同步范围的子集,framebuffer-local和view-local与否(由dependencyFlags注明)也必须与子通道依赖一致(官方标准出处见此,如我阅读理解有误请指出)。

内存屏障的作用

后述的三种内存屏障都有满足内存依赖(memory dependency)的作用,即确保可获性(availability)和可见性(visibility)。
需要先提一下:VkMemoryBarrierVkBufferMemoryBarrierVkImageMemoryBarrier,都有srcAccessMask和dstAccessMask这俩成员。

所谓可获性,指的是已完成资源的写入,内存屏障确保在其之前的命令中srcStageMask注明的阶段中srcAccessMask注明的写入操作已完成,然后等待被后续操作读写
可见性指的是能正确读取资源的内容,内存屏障确保在其之前的命令中srcStageMask注明的阶段中srcAccessMask注明的写入操作的结果,能被内存屏障后的命令中dstStageMask注明的阶段中dstAccessMask注明的读写操作正确读取。

后续的读操作当然要考虑先前的写入,但后续的写操作跟先前写入的结果有什么关系?
这其实是一个并发访问的问题。无论GPU还是CPU都不是只有单个构造简单的缓存,处理器具有多级存储结构,每个处理单元都可以有其专用的高速缓存。数据被更新后要在多级结构中传递,而且很可能是一块块而不是逐个字节依次更新的,也就是说,写入也是要看基于什么来写的。
Vulkan的官方标准中并没有提及数据是被如何传递的(它只是个API标准,底层的事情取决于物理设备的具体构造)。确保可获性和可见性,是显式地告诉物理设备你将要如何使用资源,确保可获性可能意味着数据被复制到更远离处理单元的存储中,这跟单纯的执行依赖存在差别。

为什么不把可获性和可见性合并成一个概念?
其一,两个概念适用范围不同,可获性是对于内存领域(memory domain)的概念,而可见性是对于代理和引用对(agent & reference)的概念,这几个名词搞不懂也无妨,至少这套教程里不会再出现。其二,可见必须在可获后确保,即便一个操作能确保可见性,如果资源不可获那也就不会可见。

可获性和可见性可以分别通过两个内存屏障来确保,即使用两次内存屏障,前一次的srcAccessMask注明写操作,dstAccessMask为0(仅确保可获性),后一次的srcAccessMask为0,dstAccessMask非0(使得dstAccessMask注明的操作获得资源可见性)。

Note

可以在srcAccessMask中填入诸如VK_ACCESS_COLOR_ATTACHMENT_READ_BIT之类的读操作,但没意义。要确保先前的读操作已结束,正确填写srcStageMask,即仅满足执行依赖便已足够。

内存屏障并非确保可获性和可见性的唯一方式(因而你不必因为知道了这俩概念而过分注意和担心),比如:

总结地讲,在提交进行数据传输的命令缓冲区(比如用来加载资源)时只要带上一个栅栏并等待,就足以确保各类缓冲区能被之后的命令正确读写了(但对于图像,你仍旧可能需要图像内存屏障以确保内存布局转换在正确的时机发生)。

注意虽然官方标准中没有到处提及,同步操作的作用若无明确说明,默认不适用于不同队列族间,这时候得(虽然你应该已经读过了):队列族所有权转移

写到这里我真的得吐槽下:那些说C++官方标准太学术化太难啃的程序员,真该看看Vulkan官方标准!

全局内存屏障

全局内存屏障相当简单,它作用于所有资源(缓冲区/图像),仅定义内存依赖。

struct VkMemoryBarrier 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_MEMORY_BARRIER

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkAccessFlags srcAccessMask

源操作

VkAccessFlags dstAccessMask

目标操作

版本要求

VkAccessFlagBits 的枚举项

1.3

VK_ACCESS_NONE 表示无访问

1.0

VK_ACCESS_INDIRECT_COMMAND_READ_BIT 表示对间接绘制命令参数的读取操作

1.0

VK_ACCESS_INDEX_READ_BIT 表示对索引数据的读取操作

1.0

VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT 表示对顶点数据的读取操作

1.0

VK_ACCESS_UNIFORM_READ_BIT 表示着色器中对uniform缓冲区的读取操作

1.0

VK_ACCESS_INPUT_ATTACHMENT_READ_BIT 表示着色器中对输入附件的读取操作

1.0

VK_ACCESS_SHADER_READ_BIT 表示着色器中对附件以外的读取操作(含uniform缓冲区)

1.0

VK_ACCESS_SHADER_WRITE_BIT 表示着色器中对附件以外的写入操作

1.0

VK_ACCESS_COLOR_ATTACHMENT_READ_BIT 表示对颜色附件的读取操作

1.0

VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT 表示对颜色附件的写入操作

1.0

VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT 表示对深度模板附件的读取操作

1.0

VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT 表示对深度模板附件的写入操作

1.0

VK_ACCESS_TRANSFER_READ_BIT 表示表示通过数据转移命令(vkCmdCopyBuffervkCmdBlitImage等)进行的读操作

1.0

VK_ACCESS_TRANSFER_WRITE_BIT 表示表示通过数据转移命令进行的写入操作

1.0

VK_ACCESS_HOST_READ_BIT 表示主机(指CPU侧)的读取操作

1.0

VK_ACCESS_HOST_WRITE_BIT 表示主机(指CPU侧)的写入操作

1.0

VK_ACCESS_MEMORY_READ_BIT 表示任何读取操作

1.0

VK_ACCESS_MEMORY_WRITE_BIT 表示任何写入操作

  • 关于各个管线阶段可以发生哪些访问操作,请参考官方文档中的Table 4. Supported access types

  • 当srcStageMask为VK_PIPELINE_STAGE_TOP_OF_PIPE_BITVK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT时,srcAccessMask必须为0(VK_ACCESS_NONE),dstStageMask和dstAccessMask也是同理。

缓冲区内存屏障

缓冲区内存屏障作用于特定缓冲区的指定资源范围,相比全局内存屏障,它还能进行资源的队列族所有权转移。

struct VkBufferMemoryBarrier 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkAccessFlags srcAccessMask

源操作

VkAccessFlags dstAccessMask

目标操作

uint32_t srcQueueFamilyIndex

源队列族

uint32_t dstQueueFamilyIndex

目标队列族

VkBuffer buffer

缓冲区的handle

VkDeviceSize offset

被同步的数据块距离缓冲区起始位置的字节数

VkDeviceSize size

被同步的数据块的大小,单位是字节

  • 关于资源的队列族所有权转移,具体说明参见队列族所有权转移,若不需要转移资源的队列族所有权,则srcQueueFamilyIndex和dstQueueFamilyIndex应当皆为VK_QUEUE_FAMILY_IGNORED

图像内存屏障

图像内存屏障作用于特定图像的指定资源范围,相比缓冲区内存屏障,它还能转换图像的内存布局。
图像内存布局的转换发生在可获后、可见前。
内存布局转换具有隐式同步保证:即使两次图像内存布局转换间,没有任何命令使用该图像干任何事,转换也必定依序发生(官方文档出处)。比如,先后使用两个除了内存布局外参数完全一致的图像内存屏障,那么结果等效于将内存布局转换到第二个屏障指定的目标布局。在Ch5-2 2D贴图及生成Mipmap有利用到这一点。

struct VkBufferMemoryBarrier 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkAccessFlags srcAccessMask

源操作

VkAccessFlags dstAccessMask

目标操作

VkImageLayout oldLayout

旧的内存布局

VkImageLayout newLayout

新的内存布局

uint32_t srcQueueFamilyIndex

源队列族

uint32_t dstQueueFamilyIndex

目标队列族

VkImage image

图像的handle

VkImageSubresourceRange subresourceRange

被同步的图像的子资源范围

版本要求

VkImageLayout 的枚举项

1.0

VK_IMAGE_LAYOUT_UNDEFINED 表示不关心图像的原有内容,注意不能转换到该布局

1.0

VK_IMAGE_LAYOUT_GENERAL 该布局支持所有物理设备上的访问操作,但效率未必最佳

1.0

VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 表示最适合用于图像附件的布局

1.0

VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL 表示最适合用于深度模板附件的布局

1.0

VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL 表示最适合仅读取深度模板值的布局(作为附件只读不写,或用于采样)

1.0

VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 表示最适用于仅让着色器读取的布局(用于被采样图像和输入附件)

1.0

VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL 表示最适用于作为数据传送的来源的布局

1.0

VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 表示最适用于作为数据传送的目标的布局

1.0

VK_IMAGE_LAYOUT_PREINITIALIZED 表示图像已经由CPU侧直接写入了数据(应当是线性排列的),注意不能转换到该布局

1.1

VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL 表示最适合仅读取深度值(作为附件只读不写,或用于采样)和读写模板附件的布局

1.1

VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL 表示最适合仅读取模板值(作为附件只读不写,或用于采样)和读写深度附件的布局

1.2

VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL 表示最适合用于深度附件的布局

1.2

VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL 表示最适合仅读取深度值的布局(作为附件只读不写,或用于采样)

1.2

VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL 表示最适合用于模板附件的布局

1.2

VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL 表示最适合仅读取模板值的布局(作为附件只读不写,或用于采样)

1.3

VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL 表示适合用于只读的布局(作为附件只读不写,或用于采样)

1.3

VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL 表示适合用于渲染管线中附件的布局

1.0 + VK_KHR_swapchain

VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 表示最适用于呈现的布局

  • 对只有深度/模板值的深度模板附件使用VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL而非VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMALVK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL无妨。

  • Vulkan1.2中仅注明深度/模板的布局,适用于在动态渲染中,使用两张不同的图像各自作为深度/模板附件的情况。

struct VkImageSubresourceRange 的成员说明

VkImageAspectFlags aspectMask

所使用图像的层面(即aspect)

uint32_t baseMipLevel

初始mip等级

uint32_t levelCount

mip等级总数

uint32_t baseArrayLayer

初始图层

uint32_t layerCount

图层总数

Event

事件(VkEvent)是具有管线屏障功能的同步对象,在某些场景下也可以替代信号量。
事件跟二值信号量一样,有置位(singaled)和未置位(unsingaled)两种状态。

事件同管线屏障的不同在于,它可以使得部分命令不被纳入同步范围:置位事件前的命令,和等待事件被置位后的命令被纳入同步范围,中间的命令不受影响。
如果在置位事件后立刻等待事件被置位,那么效果等效于管线屏障。

创建事件

vkCreateEvent(...)创建事件:

VkResult VKAPI_CALL vkCreateEvent(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkEventCreateInfo* pCreateInfo

指向VkEvent的创建信息

const VkAllocationCallbacks* pAllocator

VkEvent* pEvent

若执行成功,将事件的handle写入*pEvent

struct VkEventCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_EVENT_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkEventCreateFlags flags

若填入VK_EVENT_CREATE_DEVICE_ONLY_BIT(需Vulkan1.3),则说明不会在CPU一侧置位/重置事件,或查询事件状态

置位事件

在命令缓冲区中,用vkCmdSetEvent(...)置位事件:

void VKAPI_CALL vkCmdSetEvent(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkEvent event

要被置位的事件的handle

VkPipelineStageFlags stageMask

源管线阶段,相当于内存屏障参数中的srcStageMask

  • 该命令定义执行依赖:于该命令前的命令到达stageMask所注明的(但凡到达得了的)阶段后,置位事件。

  • 若事件已被置位,该函数没有效果,不会定义执行依赖。

在主机(CPU)一侧,用vkSetEvent(...)置位事件:

VkResult VKAPI_CALL vkSetEvent(...) 的参数说明

VkDevice device

逻辑设备的handle

VkEvent event

要被置位的事件的handle

  • 若事件已被置位,该函数没有效果。

Note

Vulkan官方标准中不推荐使用vkSetEvent(...),要使用的话最好在提交命令前:
If a command buffer is waiting for an event to be signaled from the host, the application must signal the event before submitting the command buffer, as described in the queue forward progress section.

实际上在提交命令后是可以使用的,在一定时间内置位事件的话不会有问题,但如果设备长时间等不到事件被置位,可能导致VK_ERROR_DEVICE_LOST。而Vulkan标准对这个“一定时间”并没有定义,因此除去在提交命令前,没有安全地使用该函数的方法。
存在可能使用该函数的应用场景,比如vkCmdSetEvent(...)和vkCmdWaitEvent(...)被分别录制在不同的命令缓冲区中,但有时只需要提交后者所在的命令缓冲区的情况。

等待事件被置位

在命令缓冲区中,用vkCmdWaitEvents(...)等待多个事件被置位:

void VKAPI_CALL vkCmdWaitEvents(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

uint32_t eventCount

要等待的事件的个数

const VkEvent* pEvents

指向所需等待的事件的数组

VkPipelineStageFlags srcStageMask

源管线阶段

VkPipelineStageFlags dstStageMask

目标管线阶段

uint32_t memoryBarrierCount

全局内存屏障的个数

const VkMemoryBarrier* pMemoryBarriers

指向全局内存屏障的数组

uint32_t bufferMemoryBarrierCount

缓冲区内存屏障的个数

const VkBufferMemoryBarrier* pBufferMemoryBarriers

指向缓冲区内存屏障的数组

uint32_t imageMemoryBarrierCount

图像内存屏障的个数

const VkImageMemoryBarrier* pImageMemoryBarriers

指向图像内存屏障的数组

  • 置位事件后,你只能在同一队列上等待该事件。

  • 若需要等待主机一侧置位事件(即通过vkSetEvent(...)置位),则srcStageMask中必须包含VK_PIPELINE_STAGE_HOST_BIT

  • srcStageMask中最晚的阶段,应不早于通过vkCmdSetEvent(...)置位pEvents所指各个事件时指定的stageMask中的最晚阶段。

  • 该命令定义执行依赖:必须等待pEvents所指各个事件被置位后,该命令后的命令才能到达由dstStageMask注明的阶段。

  • 该命令定义内存依赖:确保在置位各个事件之前的命令中srcStageMask注明的阶段中(这里各个内存屏障结构体指定的)srcAccessMask注明的写入操作的结果可获(这条仅说明源操作对应的同步范围,后略)。

将事件重置为未置位状态

在命令缓冲区中,用vkCmdResetEvent(...)将事件重置为未置位状态:

void VKAPI_CALL vkCmdResetEvent(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkEvent event

要被重置的事件的handle

VkPipelineStageFlags stageMask

源管线阶段,相当于内存屏障参数中的srcStageMask

  • 该命令定义执行依赖:于该命令前的命令到达stageMask所注明的(但凡到达得了的)阶段后,将事件重置为未置位状态。

  • 若事件未被置位,该函数没有效果,不会定义执行依赖。

在主机(CPU)一侧,用vkResetEvent(...)将事件重置为未置位状态:

VkResult VKAPI_CALL vkResetEvent(...) 的参数说明

VkDevice device

逻辑设备的handle

VkEvent event

要被重置的事件的handle

  • 若事件未被置位,该函数没有效果。

查询事件状态

在主机(CPU)一侧,用vkGetEventStatus(...)查询事件状态:

VkResult VKAPI_CALL vkGetEventStatus(...) 的参数说明

VkDevice device

逻辑设备的handle

VkEvent event

要被查询状态的事件的handle

  • 该函数的返回值即查询结果,事件被置位时返回VK_EVENT_SET,未被置位则返回VK_EVENT_RESET,其余返回值皆为错误代码。

封装为event类

VKBase.h,vulkan命名空间中添加以下代码:

class event {
    VkEvent handle = VK_NULL_HANDLE;
public:
    //event() = default;
    event(VkEventCreateInfo& createInfo) {
        Create(createInfo);
    }
    event(VkEventCreateFlags flags = 0) {
        Create(flags);
    }
    event(event&& other) noexcept { MoveHandle; }
    ~event() { DestroyHandleBy(vkDestroyEvent); }
    //Getter
    DefineHandleTypeOperator;
    DefineAddressFunction;
    //Const Function
    void CmdSet(VkCommandBuffer commandBuffer, VkPipelineStageFlags stage_from) const {
        vkCmdSetEvent(commandBuffer, handle, stage_from);
    }
    void CmdReset(VkCommandBuffer commandBuffer, VkPipelineStageFlags stage_from) const {
        vkCmdResetEvent(commandBuffer, handle, stage_from);
    }
    void CmdWait(VkCommandBuffer commandBuffer, VkPipelineStageFlags stage_from, VkPipelineStageFlags stage_to,
        arrayRef<VkMemoryBarrier> memoryBarriers
        arrayRef<VkBufferMemoryBarrier> bufferMemoryBarriers
        arrayRef<VkImageMemoryBarrier> imageMemoryBarriers) const {
        for (auto& i : memoryBarriers)
            i.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
        for (auto& i : bufferMemoryBarriers)
            i.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
        for (auto& i : imageMemoryBarriers)
            i.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
        vkCmdWaitEvents(commandBuffer, 1, &handle, stage_from, stage_to,
            memoryBarriers.Count(), memoryBarriers.Pointer(),
            bufferMemoryBarriers.Count(), bufferMemoryBarriers.Pointer(),
            imageMemoryBarriers.Count(), imageMemoryBarriers.Pointer());
    }
    result_t Set() const {
        VkResult result = vkSetEvent(graphicsBase::Base().Device(), handle);
        if (result)
            outStream << std::format("[ event ] ERROR\nFailed to singal the event!\nError code: {}\n", int32_t(result));
        return result;
    }
    result_t Reset() const {
        VkResult result = vkResetEvent(graphicsBase::Base().Device(), handle);
        if (result)
            outStream << std::format("[ event ] ERROR\nFailed to unsingal the event!\nError code: {}\n", int32_t(result));
        return result;
    }
    result_t Status() const {
        VkResult result = vkGetEventStatus(graphicsBase::Base().Device(), handle);
        if (result < 0) //vkGetEventStatus(...)成功时有两种结果
            outStream << std::format("[ event ] ERROR\nFailed to get the status of the event!\nError code: {}\n", int32_t(result));
        return result;
    }
    //Non-const Function
    result_t Create(VkEventCreateInfo& createInfo) {
        createInfo.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO;
        VkResult result = vkCreateEvent(graphicsBase::Base().Device(), &createInfo, nullptr, &handle);
        if (result)
            outStream << std::format("[ event ] ERROR\nFailed to create a event!\nError code: {}\n", int32_t(result));
        return result;
    }
    result_t Create(VkEventCreateFlags flags = 0) {
        VkEventCreateInfo createInfo = {
            .flags = flags
        };
        return Create(createInfo);
    }
};