之前在开发 GGE Vulkan Rendering Backend 的过程中,有时仅仅通过 API 文档难以深入理解其背后的运行机制,于是产生了深入 Vulkan 驱动内部的想法,本文通过深入解析 AMD Vulkan 驱动源码,尝试了解 Vulkan 以及 GPU 硬件的运行机制。同时本文也是 Vulkan 探秘系列的第一篇。
AMD 在 2017年底放出了针对 AMD GPU 的 Linux 平台的 Vulkan 驱动程序源码:AMDVLK,github 源码地址:
https://github.com/GPUOpen-Drivers/AMDVLKgithub.com
通过阅读 AMDVLK 源码,可以更深入的了解 Vulkan API 的实现机制和 GPU 内部细节,尽管 AMDVLK 是针对的是 linux 平台,但对移动平台的 Vulkan 深入学习也有一定的帮助作用。
本文分为 2 个大部分,第一部分讲解 AMDVLK 的整体架构以及核心接口说明,第二部分以 VkCmdDraw API 为切入点,深入 AMDVLK 源码进行解析。
声明:由于本人不是驱动开发人员,在解析过程中难免会可能产生误解甚至错误,如理解有误,望请及时指出,不吝赐教。
AMDVLK 整体架构及核心接口
下面是 AMDVLK 整体的架构图:
AMDVLK 架构图
XGL
其中 XGL 主要负责将 Vulkan API 命令转换为 PAL 命令。除了转换指令之外,它还使用基于 LLVM Pipeline Compiler(LLPC)库将 VkPipeline 的 Shader 编译为与 PAL 管道 ABI 兼容的代码对象。从 XGL 的职责可以看出,XGL 是比较薄的层次,驱动中大部分的工作代码都集中在 PAL 中。
PAL
PAL 是 AMD Radeon™ (GCN、RDNA及以上) 架构的硬件和操作系统的抽象层,运行在用户模式驱动中(UMD)中,是 UMD 的重要组成部分。PAL 接受来自于 XGL 转换过来的命令,在其内部根据所处硬件平台执行相应的硬件和操作系统指令。PAL 是相对较厚的驱动层,占据了基于 PAL 的 UMD 中的大部分代码量(除 LLPC 之外)。
PAL 内部的抽象级别并不是完全一致的,在功能相近的部分,抽象级别相对较高,在和硬件相关的不同部分,抽象级别相对较低。总体的原则是在不影响驱动性能的前提下尽可能共享更多的代码。
在架构上 XGL 是 PAL 的 Client,PAL 是 XGL 的 Server。
PAL 接口源码目录结构
- PAL 使用 C++ 定义接口,.../pal/inc 目录定义了公共接口, Client 端如 XGL 必须包含此目录。在结构上基本上是一个接口对应一个头文件。.../pal/inc 包含三个子目录,分别如下:
- inc/core 定义 PAL Core 核心接口,下文详述。
- inc/util 定义 PAL 工具集,除了其特定于GPU的核心功能外,PAL还在Util名称空间中提供了许多通用的,操作系统抽象的软件实用程序。 PAL核心依赖于这些实用程序,但是它们的Client 也可以使用它们。 Util中提供的功能包括内存管理,调试打印和声明,通用容器,多线程和同步原语,文件系统访问以及加密算法实现。
- inc/gpuUtil 定义 PAL GPU 工具集,除了通用的,不受操作系统限制的软件实用程序外,PAL还在GpuUtil命名空间中提供了GPU特定的实用程序。 这些实用程序提供了建立在核心Pal接口之上的通用,有用的功能。 一些示例包括用于使用GPU编写文本的接口,MLAA实现以及位于Pal :: IPerfExperiment之上的包装器,以简化性能数据收集。
PAL Core
PAL 的核心接口定义在 Pal namespace 中。以面向对象的形式与 GPU 和 OS 进行交互。PAL Core 的核心功能主要有:
- 将所有的 Shader Stage,以及一些附加的“shader adjacent”状态整合成一个单一的 Pipeline Object。
- 显示的、free-threaded 的 Command Buffer 生成。
- 支持多个、异步引擎来执行 GPU 任务(GraphicsComputeTransfer)。
- 显示的 System 和 GPU 的内存管理。
- 灵活的 Shader 资源绑定模型。
- 显示的同步管理,包括 Pipeline Stalls、Cache Flushes 以及 State Changes 压缩等等。
以上这些核心功能说明 PAL Core 是实现现代图形 API 特性的最重要部分。
PAL Core 核心接口
PAL Core 核心接口按照抽象类别分为三个大类,分别为 OS 抽象接口、硬件 IP 抽象接口以及公共基础类,具体如下:
OS 抽象接口
- IPlatform:由与 PAL 交互的 Client 创建的根级对象。 主要负责枚举连接到系统的设备和屏幕,并返回所有系统范围的属性。
- IDevice:用于查询特定 GPU 的属性并与其进行交互的可配置上下文。也是所有其他 PAL 对象的工厂类。
- IQueue:一台设备具有一个或多个能够发布某些类型的工作的引擎。 例如,Tahiti 硬件拥有1个通用引擎(支持图形,计算或复制命令),2个计算引擎(支持计算或复制命令)和2个DMA引擎(仅支持复制命令)。 IQueue对象是用于在特定引擎上提交工作的上下文。 这主要采取提交命令缓冲区并将图像显示到屏幕的形式。 在队列中执行的工作将按顺序开始,但是在不同的队列(即使队列引用相同的引擎)上执行的工作也不能保证在没有显式同步的情况下进行排序。
- IQueueSemaphore:可以从IQueue发出信号并等待队列信号,以便控制队列之间的执行顺序。
- IFence:用于粗粒度CPU / GPU同步。 围栏可以作为队列中命令缓冲区提交的一部分从GPU发出信号,然后从CPU等待。
- IGpuMemory:表示GPU可访问的内存分配。 可以是虚拟的(只能是必须通过IQueue操作显式映射的VA分配)或物理的。 Client 必须通过全局管理设备的物理分配的驻留时间(IDevice :: AddGpuMemoryReferences),或通过指定提交时命令缓冲区引用的分配来管理物理分配的驻留。
- ICmdAllocator:用于支持ICmdBuffer的GPU内存分配池。 Client 可以自由地为每个设备创建一个分配器,或为每个线程创建一个分配器以消除线程争用。
- IScreen:代表连接到系统的显示器。 用于将画面呈现到屏幕上。
- IPrivateScreen:表示OS不可见的显示器,通常是VR头戴式显示器。
硬件 IP 抽象接口
- 所有 IP
- ICmdBuffer:Command Buffer 的抽象接口。
- IImage:Image 的抽象接口。
- 仅限 GFXIP
- IPipeline:包含所有着色器阶段(用于计算的CS,用于图形的VS / HS / DS / GS / PS),描述着色器如何使用用户数据条目的资源映射,以及其他一些固定功能状态,例如depth / 颜色格式,混合启用,MSAA启用等。
- IColorTargetView:允许将图像绑定为颜色目标(即RTV)。
- IDepthStencilView:允许将图像绑定为深度/模板目标(即DSV)。
- IGpuEvent:用于CPU和GPU之间的细粒度(命令内缓冲区)同步。 可以从CPU或GPU设置/重置GPU事件,然后从两者中等待。
- IQueryPool:用于跟踪遮挡或管道统计信息查询结果。
- 动态状态对象:IColorBlendState,IDepthStencilState 和 IMsaaState 定义与 DX11 类似的相关固定功能图形状态的逻辑集合。
- IPerfExperiment:用于收集性能计数器和线程跟踪数据。
- IBorderColorPalette:提供可索引颜色的集合,供钳制到任意边框颜色的采样器使用。
公共基础类
- IDestroyable:所有 PAL 核心接口的基类,定义Destroy 方法。 调用 Destroy 将释放该对象的所有内部分配资源,但是该对象的系统内存需要由 Client 负责释放。
- IGpuMemoryBindable:定义用于将 GPU 内存绑定到对象的一组方法。 继承IGpuMemoryBindable 的接口需要 GPU 内存才能被 GPU 使用。Client 必须查询请求的信息(例如,对齐,大小,堆),并为对象分配/绑定GPU内存。 IGpuMemoryBindable继承自 IDestroyable。
下图是在某个渲染帧中 PAL 的一些核心接口相互之间的交互和关系:
如图所示,IPlatform 中包含了多个 IDevice,每个 IDevice 对应一个物理 GPU 设备。在其中的一个 IDevice 中(左上),存在 4 个执行引擎,有4个 IQueue 对应这 4 个执行引擎,1个 Universal Queue,一个 Compute Queue,两个 DMA Queue,每个 Queue 都在独立的线程中提交。在图的右侧是一些存储 GPU 任务的 ICmdBuffer 接口,其中有执行场景渲染、后处理、纹理上传等等。不同的任务的 ICmdBuffer 在对应类型的 IQueue 中执行,如图中,Scene Rendering 在 Universal Queue 中执行,Post Processing 在 Compute Queue 中执行,而 Texture Upload 在 DMA Queue 中执行。在 Queue 中执行的除了 Command Buffer 以外,还有用于同步的 IQueueSemaphore,如图在 Universal Queue 中先 wait 了前两个 IQueueSemaphore,在执行完 Scene Rendering 之后,signal 最后一个 IQueueSemaphore。在 Compute 的 Queue 中,先 wait 最后一个IQueueSemaphore,在执行 Post Processing,最后 present。以此类推其他两个 Queue 也是如此。
VkCmdDraw 函数驱动源码解析
架构和核心接口讲完,我们从一个具体的 Vulkan API 切入到 AMDVLK 中来解析源码。这里我们从最常见的绘制 API VkCmdDraw 开始。
按照架构,首先分析的是 XGL 层。不过在讲解之前,先介绍下 Vulkan 函数声明格式中的调用约定宏。
如上所示,vkCmdDraw 函数声明中的 VKAPI_ATTR 和 VKAPI_CALL 是平台相关的调用约定宏,根据 Vulkan 规范描述,相应的 Vulkan 实现平台负责定义这些宏,以便 Vulkan 调用方以 Vulkan 实现期望的相同调用约定来调用 Vulkan 命令。
调用约定宏有3种,分别是 VKAPI_ATTR、VKAPI_CALL、VKAPI_PTR,下面分别说明:
- VKAPI_ATTR:放在函数声明的返回类型之前。用于 C++11 和 GCC/Clang 风格的函数属性语法。
- VKAPI_CALL:放在函数声明中的返回类型之后。用于 MSVC 样式的调用约定语法。
- VKAPI_PTR:放在函数指针类型的“(”和 "*" 之间。
根据上述规则,一般的函数声明如下所示:
函数声明: VKAPI_ATTR void VKAPI_CALL vkCommand(void);
函数指针类型: typedef void (VKAPI_PTR *PFN_vkCommand)(void);
在 XGL 中 Vulkan API 内部路由分发是 DispatchTable 辅助类完成,不过分发机制不是本文讨论的内容,所以在这里不再阐述。在 XGL 的 vk_cmdbuffer.cpp 中定义了 vkCmdDraw 函数,函数签名与 Vulkan API 完全一致,代码如下:
namespace vk
{
namespace entry
{
...
VKAPI_ATTR void VKAPI_CALL vkCmdDraw(
VkCommandBuffer cmdBuffer,
uint32_t vertexCount,
uint32_t instanceCount,
uint32_t firstVertex,
uint32_t firstInstance)
{
ApiCmdBuffer::ObjectFromHandle(cmdBuffer)->Draw(
firstVertex,
vertexCount,
firstInstance,
instanceCount);
}
...
vkCmdDraw 的实现非常简单,只有一行代码,只是将 Vulkan API 转发给 XGL 内部 CmdBuffer 对象的 Draw 函数调用。这里的 ApiCmdBuffer 是用于实现转发 VkCommandBuffer API 的辅助类,由 VK_DEFINE_DISPATCHABLE 宏定义,代码如下:
VK_DEFINE_DISPATCHABLE(CmdBuffer);
...
#define VK_DEFINE_DISPATCHABLE(a)
class Dispatchable##a : public Dispatchable<a> {}; // 从 Dispatchable 模板类继承
typedef Dispatchable##a Api##a; // typedef 定义 ApiXXX 类型,在这里就是 ApiCmdBuffer
从代码可以看出,ApiCmdBuffer 继承自 Dispatchable 模板类,Dispatchable 是用于封装 Vulkan 可调度的核心对象(VkPhysicalDevice、VkDevice、VkCommandBuffer、VkQueue)的辅助模板类,主要用于实现 Vulkan 对象到 XGL 内部对象之间的转换,在 Dispatchable 内部保存指向 XGL 内部对象的指针,例如这里的 CmdBuffer。Dispatchable 模板类定义如下:
template <typename C>
class Dispatchable
{
private:
VK_LOADER_DATA m_reservedForLoader;
unsigned char m_C[sizeof(C)];
...
public:
...
// Given pointer to const C, returns the containing Dispatchable<C>.
static VK_FORCEINLINE const Dispatchable<C>* FromObject(const C* it)
{
return reinterpret_cast<const Dispatchable<C>*>(
reinterpret_cast<const uint8_t*>(it) - (sizeof(Dispatchable<C>) - sizeof(C))
);
}
// Non-const version of above.
static VK_FORCEINLINE Dispatchable<C>* FromObject(C* it)
{
return reinterpret_cast<Dispatchable<C>*>(
reinterpret_cast<uint8_t*>(it) - (sizeof(Dispatchable<C>) - sizeof(C))
);
}
// Converts a "Vk*" dispatchable handle to the driver internal object pointer.
static VK_FORCEINLINE C* ObjectFromHandle(typename C::ApiType handle)
{
return reinterpret_cast<C*>(
reinterpret_cast<Dispatchable<C>*>(handle)->m_C);
}
};
不难看出,成员 m_C 实际保存了指向 XGL 内部对象的指针,模版参数 C 则是 XGL 对象类型,在这里就是 CmdBuffer。而 FromObject、ObjectFromHandle 静态函数,就是用于实现从 Vulkan 对象到 XGL 内部对象的转换函数,在这里就是 VkCommandBuffer 到 CmdBuffer 的转换。
在 Dispatchable 类定义的下面是几个用于初始化 XGL 内部对象的宏,以及用于建立 Vulkan 对象和 XGL 内部对象的关联的宏,为了简化阐述,而且也不是本文讨论的内容,这里就不再详述,后续再另发文解析。
下面我们来看 CmdBuffer 的定义:
class CmdBuffer
{
public:
typedef VkCommandBuffer ApiType;
...
void CmdBuffer::Draw(
uint32_t firstVertex,
uint32_t vertexCount,
uint32_t firstInstance,
uint32_t instanceCount)
{
DbgBarrierPreCmd(DbgBarrierDrawNonIndexed);
ValidateStates();
PalCmdDraw(firstVertex,
vertexCount,
firstInstance,
instanceCount);
DbgBarrierPostCmd(DbgBarrierDrawNonIndexed);
}
...
注意 ApiType 这个类型定义,在每个从 Dispatchable 派生的类都必须定义此类型,因为需要在 Dispatchable 中用于对象转换。同样的在其它可调度的核心对象类如 PhysicalDevice、Device、Queue 中也有这样的类型定义。
CmdBuffer 的 Draw 函数中的 DbgBarrierPreCmd、DbgBarrierPostCmd 是用于在调试模式下插入 PAL Barrier,用于在 PAL 层进行校验,为了简化阐述,关于 PAL 层 Barrier 这里不再详述,后续再另发文详解。
ValidateStates 函数主要用于检查 GPU 状态,如果为 Dirty 则在内部遍历每个 GPU 设备,调用 PAL 层 CommandBuffer 设置 Viewport 和 Scissor Rect,最后重置 Dirty 标志。
PalCmdDraw 函数是真正调用 PAL 层的绘制命令所在。代码如下:
void CmdBuffer::PalCmdDraw(
uint32_t firstVertex,
uint32_t vertexCount,
uint32_t firstInstance,
uint32_t instanceCount)
{
// Currently only Vulkan graphics pipelines use PAL graphics pipeline bindings so there's no need to
// add a delayed validation check for graphics.
VK_ASSERT(PalPipelineBindingOwnedBy(Pal::PipelineBindPoint::Graphics, PipelineBindGraphics));
utils::IterateMask deviceGroup(m_curDeviceMask);
do
{
const uint32_t deviceIdx = deviceGroup.Index();
PalCmdBuffer(deviceIdx)->CmdDraw(firstVertex,
vertexCount,
firstInstance,
instanceCount);
}
while (deviceGroup.IterateNext());
}
PalPipelineBindingOwnedBy 校验 XGL 和 PAL 层之间的 Pipeline Binding 是否一致。接下来通过 IterateMask 用于遍历每个 Vulkan Device( Command Buffer 可以运行在多个 Vulkan Device 上),依次调用 PAL 层的核心接口 ICmdBuffer 的 CmdDraw 函数。
PalCmdBuffer 函数用于根据 Device Index 返回对应的 PAL 层的核心接口 ICmdBuffer:
VK_INLINE Pal::ICmdBuffer* PalCmdBuffer(int32_t idx) const
{
if (idx == 0)
{
VK_ASSERT((uintptr_t)m_pPalCmdBuffers[idx] == (uintptr_t)this + sizeof(*this));
return (Pal::ICmdBuffer*)((uintptr_t)this + sizeof(*this));
}
VK_ASSERT((idx >= 0) && (idx < static_cast<int32_t>(MaxPalDevices)));
return m_pPalCmdBuffers[idx];
}
m_pPalCmdBuffers 是 ICmdBuffer 接口指针数组,保存最多 4 个 ICmdBuffer。从代码中可以看出,XGL CmdBuffer 和 ICmdBuffer 的内存分布是紧致排布的,这样是 Cache friendly 的,强化了驱动性能。
得到 ICmdBuffer 指针之后,就调用 ICmdBuffer 的 CmdDraw 函数,从这里开始以下的分析就进入了 PAL 层。
PAL_INLINE void CmdDraw(
uint32 firstVertex,
uint32 vertexCount,
uint32 firstInstance,
uint32 instanceCount)
{
m_funcTable.pfnCmdDraw(this, firstVertex, vertexCount, firstInstance, instanceCount);
}
m_funcTable 是一系列 DrawDispatch 函数指针的结构,pfnCmdDraw 是这个结构中指向 非顶点索引绘制的函数指针。
为了利于后面的讲解,这里先讲下 PAL 核心接口 ICmdBuffer 的 GfxIP 实现。由前面 PAL 层的硬件 IP 抽象接口的阐述可知,ICmdBuffer 是个抽象类,不同 GCN 架构的硬件图形计算的 IP 模块需要实现这个抽象接口,在 PAL 中主要实现了 2 个 GfxIP 的接口,分别是 GfxIP6,对应的是 gfx678 GfxIP;Gfx9 对应的是 gfx910 GfxIP。具体实现代码在 src/core/hw/gfxip/gfx6 和 src/core/hw/gfxip/gfx9 目录下。同理,对于其他 PAL 层的硬件 IP 抽象接口也是如此,可以按照这个路数来查看其他的核心对象代码。GfxIP level 和各个 ISA 的详细对应关系请参考下面这个链接:
https://www.x.org/wiki/RadeonFeature/www.x.org
说回 m_funcTable,这个结构中的函数指针就是由各个具体的 GfxIP 实现来设置的。以 GfxIP6 为例,在 src/core/hw/gfxip/gfx6/gfx6UniversalCmdBuffer.h 中声明了 UniversalCmdBuffer 类,对应的是 Graphics Command Buffer,这个类中的 SwitchDrawFunctions 函数用于根据当前芯片的 gfx level 来切换,SwitchDrawFunctions 函数在 UniversalCmdBuffer 创建时初始化为非 view instance 的函数,以及在 Bind Pipeline 时如果 Pipeline 的 view instance 发生变化则再次调用来切换绘制函数。以下是这个函数的部分实现代码:
switch (m_device.Parent()->ChipProperties().gfxLevel)
{
case GfxIpLevel::GfxIp6:
m_funcTable.pfnCmdDraw = CmdDraw<GfxIpLevel::GfxIp6, false, false, false>;
m_funcTable.pfnCmdDrawOpaque = CmdDrawOpaque<GfxIpLevel::GfxIp6, false, false, false>;
m_funcTable.pfnCmdDrawIndexed = CmdDrawIndexed<GfxIpLevel::GfxIp6, false, false, false>;
m_funcTable.pfnCmdDrawIndirectMulti = CmdDrawIndirectMulti<GfxIpLevel::GfxIp6, false, false, false>;
m_funcTable.pfnCmdDrawIndexedIndirectMulti =
CmdDrawIndexedIndirectMulti<GfxIpLevel::GfxIp6, false, false, false>;
break;
case GfxIpLevel::GfxIp7:
m_funcTable.pfnCmdDraw = CmdDraw<GfxIpLevel::GfxIp7, false, false, false>;
m_funcTable.pfnCmdDrawOpaque = CmdDrawOpaque<GfxIpLevel::GfxIp7, false, false, false>;
m_funcTable.pfnCmdDrawIndexed = CmdDrawIndexed<GfxIpLevel::GfxIp7, false, false, false>;
m_funcTable.pfnCmdDrawIndirectMulti = CmdDrawIndirectMulti<GfxIpLevel::GfxIp7, false, false, false>;
m_funcTable.pfnCmdDrawIndexedIndirectMulti =
CmdDrawIndexedIndirectMulti<GfxIpLevel::GfxIp7, false, false, false>;
break;
case GfxIpLevel::GfxIp8:
m_funcTable.pfnCmdDraw = CmdDraw<GfxIpLevel::GfxIp8, false, false, false>;
m_funcTable.pfnCmdDrawOpaque = CmdDrawOpaque<GfxIpLevel::GfxIp8, false, false, false>;
m_funcTable.pfnCmdDrawIndexed = CmdDrawIndexed<GfxIpLevel::GfxIp8, false, false, false>;
m_funcTable.pfnCmdDrawIndirectMulti = CmdDrawIndirectMulti<GfxIpLevel::GfxIp8, false, false, false>;
m_funcTable.pfnCmdDrawIndexedIndirectMulti =
CmdDrawIndexedIndirectMulti<GfxIpLevel::GfxIp8, false, false, false>;
break;
case GfxIpLevel::GfxIp8_1:
m_funcTable.pfnCmdDraw = CmdDraw<GfxIpLevel::GfxIp8_1, false, false, false>;
m_funcTable.pfnCmdDrawOpaque = CmdDrawOpaque<GfxIpLevel::GfxIp8_1, false, false, false>;
m_funcTable.pfnCmdDrawIndexed = CmdDrawIndexed<GfxIpLevel::GfxIp8_1, false, false, false>;
m_funcTable.pfnCmdDrawIndirectMulti = CmdDrawIndirectMulti<GfxIpLevel::GfxIp8_1, false, false, false>;
m_funcTable.pfnCmdDrawIndexedIndirectMulti =
CmdDrawIndexedIndirectMulti<GfxIpLevel::GfxIp8_1, false, false, false>;
break;
default:
PAL_ASSERT_ALWAYS();
break;
}
可以看出,m_funcTable.pfnCmdDraw 指向的是 UniversalCmdBuffer::CmdDraw 静态模版函数,这是 VkCmdDraw 中最底层的调用函数,代码如下(为了方便说明,只列出关键代码段,并在前面标注了序号):
template <GfxIpLevel gfxLevel, bool issueSqttMarkerEvent, bool viewInstancingEnable, bool DescribeDrawDispatch>
void PAL_STDCALL UniversalCmdBuffer::CmdDraw(
ICmdBuffer* pCmdBuffer,
uint32 firstVertex,
uint32 vertexCount,
uint32 firstInstance,
uint32 instanceCount)
{
if ((gfxLevel >= GfxIpLevel::GfxIp8) || (instanceCount > 0))
{
auto* pThis = static_cast<UniversalCmdBuffer*>(pCmdBuffer);
ValidateDrawInfo drawInfo;
drawInfo.vtxIdxCount = vertexCount;
drawInfo.instanceCount = instanceCount;
drawInfo.firstVertex = firstVertex;
drawInfo.firstInstance = firstInstance;
drawInfo.firstIndex = 0;
drawInfo.useOpaque = false;
1 pThis->ValidateDraw<false, false>(drawInfo);
....
2 uint32* pDeCmdSpace = pThis->m_deCmdStream.ReserveCommands();
....
{
3 pDeCmdSpace += pThis->m_cmdUtil.BuildDrawIndexAuto(vertexCount, false,
pThis->PacketPredicate(),
pDeCmdSpace);
}
....
4 pDeCmdSpace = pThis->m_workaroundState.PostDraw(pThis->m_graphicsState, pDeCmdSpace);
5 pDeCmdSpace = pThis->IncrementDeCounter(pDeCmdSpace);
6 pThis->m_deCmdStream.CommitCommands(pDeCmdSpace);
....
}
}
序号 1 代码调用了 ValidateDraw 函数,主要用于在执行硬件绘制命令之前,根据 dirty state 进行一系列 GPU Pipeline State 的检查和设置。其内部使用的是 CmdStream 类代表单个硬件命令流,在 AMDVLK 中,使用的是 PM4 格式的硬件指令,但由于 PM4 格式未公开,无法了解其内部结构。在 UniversalCmdBuffer 中有两个 CmdStream 成员,分别是 Primary 命令流(DE),和用于并行绘制引擎的常量更新命令流(CE)。
序号 2 代码是分配了一个 PM4 格式的硬件命令流空间,在接下来的流程中用于构建实际的硬件命令。
序号 3 代码调用 CmdUtil 工具类的 BuildDrawIndexAuto 函数用于构建 PM4 格式的非索引绘制命令,CmdUtil 工具类主要用于构建 PM4 命令包。
序号 4 代码用于一些特定硬件上的特殊 draw 后处理,例如针对 Gfx 7/8 上 stream-output 绘制之后所有的 Vertex Grouper Tessellator 都需要挂起等待 stream out 的同步信号,这时需要发出 VGT_STREAMOUT_SYNC 事件同步命令。
序号 5 代码用于增加 DE 指令的计数,其内部通过调用 CmdUtil 工具类的 BuildIncrementDeCounter 函数增加统计 DE 指令计数的指令。
序号 6
至此 VkCmdDraw 完整的驱动内部流程就分析完了,但其实这其中还有许多内容,出于便于阐述的原因,在文中没有列出代码以及详细说明,比如 ValidateDraw 函数,其内部也是比较复杂的流程。另外在 UniversalCmdBuffer::CmdDraw 中也只是创建了 PM4 的 GPU 硬件命令,并没有真正发送到 GPU 上执行,而真正执行则要通过 vkQueueSubmit API 才会提交到硬件中,这部分的内容更加复杂,因此本文只是对庞大的 AMDVLK 源码的初探,希望通过本文对 Vulkan 驱动能了解一些基本概念,同时也为深入阅读 AMDVLK 源码提供了一些思路和参考。