之前在开发 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 源码提供了一些思路和参考。