图形驱动应用分析
图形驱动应用分析
1 视频与合成
在运行GFX/视频播放用例(应用程序的视频流类型)时,查看影响英特尔体系结构下UI体验的特定稳定性问题,行为是冻结一个UI,然后是一个黑屏,然后是系统重新启动(当然是在一段随机的时间间隔之后)。
如果3D客户端应用程序“挂起”GPU,则GPU进程可能会被终止,然后GPU会完全重置。对于复杂的用例,如视频解码,许多帧/对象当前处于运行状态,因此终止GPU进程并重置GPU会导致不受欢迎的效果。
建议的解决方案:超时检测和恢复(Timeout Detection & Recovery,TDR)。Intel GPU的新功能(上游为wip),允许应用程序在单个批处理缓冲区上启用挂起检测,从而提高稳定性和鲁棒性。超时检测和恢复(TDR)允许独立重置GPU中的不同引擎(而不是完全重置GPU)。一般来说,这些实现在i915驱动程序中引入了一个新的IRQ处理程序,以及在gpu的环形缓冲区中发出的批处理缓冲区的启动指令之前和之后引入了两个新的gpu命令指令。TDR的步骤如下:
建议的解决方案:
1、UMD媒体驱动程序在发送批缓冲区后启动定时器。
2、计时器过期后,检测到媒体引擎处于挂起状态。
3、GPU驱动程序仅重置受影响的媒体引擎。
4、由于UMD媒体驱动程序知道提交错误批次的时间,因此可以在媒体驱动程序从重置中恢复的时间内采取措施。
整个机制通过任意阈值工作,该阈值可以通过ioctl从应用程序设置。但阈值不能太低,否则会产生太多误报。
合成器(compositor)如何受益?结合下图加以回答:
1、合成器的基本任务是生成帧。
2、过去,当我们检测到GPU挂起时,合成器恢复(屏幕冻结、绿色或黑色屏幕或系统重新启动)为时已晚。
3、视频客户端应用程序现在可以早期确定“任务”是否导致媒体引擎崩溃,如果是,则向合成器标记以显示当前帧,同时媒体引擎从重置中返回。
2 Rocksolid
Rocksolid最初是GPU基准产品的引擎,后来发展成为一个独立的产品。除了客户需求之外,该开发仍然与基准开发紧密相关。轻量级渲染/计算引擎主要针对非游戏用途,从小型嵌入式系统扩展到现代桌面级硬件。它不是一个成熟的游戏引擎,但它的开销比大型现代游戏引擎低得多,也更容易定制,更稳定,并且可以通过获得安全认证。下图是Rocksolid引擎的架构图,由此可知,它可以直接访问设备驱动,从而提升性能。
图形管线是一种非常好的运行方式,例如图像处理任务。在许多硬件中,如果问题自然映射到全屏光栅化过程,则将其作为图形管道而不是计算管道运行会更快。在一个工业客户案例中,Rocksolid被用于提供GPU加速的图像处理管道。与原始OpenCL版本相比,目标硬件的速度快了好几倍。
在OpenGL中,Rocksolid引擎只是在拓扑排序中运行记录的节点命令列表。在Vulkan中,每个使用的命令队列都有一个提交线程。目前,默认设置只是一个命令队列,所以只有一个提交线程。提交线程连续运行。当提交线程仍在推送命令时,程序的其余部分可以准备下一帧。Vulkan的CPU可见资源由简单的循环围栏系统保护。
3 I/O驱动
将应用程序的输入/输出请求转换为设备的低级命令,并将其发送给设备控制器,获取输入/输出设备的响应并将其发送到应用程序。
输入/输出系统的各层以及各层的主要功能。
如何在硬件中访问IO呢?步骤如下:
- 操作系统需要向设备控制器发送/接收命令和控制以完成输入/输出。
- 设备控制器有一个或多个用于控制和数据的寄存器。
- 处理器通过读/写这些寄存器与控制器通信。
- 如何寻址这些寄存器?
- 基于内存的I/O。
- 基于端口的输入/输出。
- 混合输入/输出。
内存映射/基于端口/混合的IO的方式如下图:
(a)特殊CPU指令(输入/输出)。(b)内存映射:为硬件输入/输出寄存器保留内存区域。标准内存指令会更新它们。(c)混合:一些控制器映射到内存,一些使用I/O指令。
单总线和双总线IO。(a)内存映射I/O只有一个地址空间,内存映射I/O更易于实现和使用。帧缓冲区或类似设备更适合于内存映射的I/O。(b)基于端口的I/O有两个地址空间:一个用于内存,一个用于端口。双总线允许并行读/写数据和设备。
单总线和双总线更详细的对比图如下:
总线:组件(包括CPU)之间的互连,可以连接多个设备:
端口:仅插入一个输入/输出设备的接口:
设备控制器:将物理设备连接到系统总线/端口:
每个设备都有一个设备控制器和一个设备驱动程序来与操作系统通信,设备驱动程序是可以插入操作系统以处理特定设备的软件模块,设备控制器用作设备和设备驱动程序之间的接口,设备控制器可以处理多个设备。作为一个接口,它的主要任务是将串行位流转换为字节块,并根据需要执行纠错。
IO端口寄存器有状态寄存器(由host读取)、命令寄存器(由host写入)、寄存器中的数据(由host读取以获取输入)、数据输出寄存器(由host写入以发送输出)。
直接内存访问(Direct Memory Access,DMA):对于进行大型传输的设备,如磁盘驱动器,使用昂贵的通用处理器来监视状态位并将数据一次输入1字节的控制器寄存器,似乎是一种浪费——这一过程称为编程输入/输出。基于中断的I/O不是一种补救方法,因为每个字节都会创建一个到中断处理程序例程的上下文开关。在基于轮询和基于中断的I/O中,所有字节都需要通过CPU,并且输入/输出设备<->CPU<->内存有很多开销。如果我们可以将这个平凡的任务卸载到一个特殊用途的处理器上,该处理器可以将数据从输入/输出设备直接移动到内存中,那就太好了!这就是直接内存访问(DMA)控制器。
要启动DMA传输,host将DMA命令块写入内存。指向传输源的指针,指向传输目标的指针,以及要传输的字节数的计数。CPU将此命令块的地址写入DMA控制器,然后继续其他工作。DMA控制器继续直接操作内存总线,在总线上放置地址以执行传输,而无需主CPU的帮助。简单的DMA控制器是PC中的标准组件,PC的总线主控输入/输出板通常包含自己的高速DMA硬件。使用DMA的IO示例:
/* Code executed when the print system call is made */
copyFromUser(buffer, p, count);
setupDMAController();
scheduler();
/* Interrupt Service Routine Procedure for the printer */
acknowledgeInterrupt();
unblockUser();
returnFromInterrupt();
请注意,中断是每个I/O任务生成一次,而不是每个字节生成一次(在基于中断的I/O情况下)。
IO硬件接口:设备驱动程序被告知将磁盘数据传输到地址X处的缓冲区,设备驱动程序告诉磁盘控制器将C字节从磁盘传输到地址X处的缓冲区,磁盘控制器启动DMA传输,磁盘控制器将每个字节发送到DMA控制器,DMA控制器将字节传输到缓冲区X,递增内存地址,递减C直到0,当C==0时,DMA中断CPU以完成信号传输。
应用程序IO接口如下图所示:
连接到计算机的每个输入/输出设备都需要一些特定于设备的代码来控制它。设备制造商编写,每个操作系统都需要自己的设备驱动程序,每个设备驱动程序都支持特定类型或类别的输入/输出设备。鼠标驱动程序可以支持不同类型的鼠标,但不能用于网络摄像头。操作系统定义了驱动程序的功能以及它如何与操作系统的其余部分交互。设备驱动程序具有多个功能,要接受来自其上方独立于设备的软件的抽象读写请求,并确保执行这些请求,设备初始化,管理is电源需求和日志事件。
Microsoft Windows使用文件系统上的设备快捷方式来寻址设备,设备访问API为应用程序程序员提供了一个接口,以检查设备并与之交互,Windows设备框架为设备驱动程序开发提供了用户和内核界面。输入/输出分类(操作系统视角)有:
- 字符流和块。
- 顺序访问与随机访问。设备驱动程序允许查找设备中的偏移量。
- 同步与异步。设备驱动程序上的I/O操作与设备控制器上的I/O完成同步,异步I/O更早返回,稍后报告成功/失败。
- 缓冲和直接。报告的操作结果在缓冲区或设备控制器上完成。
- 共享或专用。每个设备实例上的I/O是互斥的。(即打印机)
- 只读、只写、读写。
内核提供了许多与I/O相关的服务:调度、缓冲、缓存、池化、设备保留及错误处理,基于硬件和设备驱动程序基础架构构建。
IO调度用来调度一组I/O请求,意味着确定执行它们的良好顺序。操作系统开发人员通过维护每个设备的请求队列来实现调度,当应用程序发出阻塞I/O系统调用时,该请求将被置于该设备的队列中。I/O调度器重新排列队列的顺序,以提高总体系统效率和应用程序的平均响应时间。输入/输出通常很慢,一些设备的物理特性需要优化,例如硬盘——由磁头移动和旋转引起的机械装置和延迟。如果在FIFO策略中执行输入/输出,机械之字形运动可能会否决输入/输出操作。I/O调度获取设备上的一组I/O请求,并确定在设备上执行请求的最佳顺序和时间。当多个任务竞争要处理的I/O请求时,调度变得复杂。
块设备操作与虚拟内存和分页紧密耦合,一些帧用作页面缓存,并将块设备的数据保存在系统中。块设备中的输入/输出:搜索块是否已在页缓存中(物理内存中):如果找到现有帧的读/写缓冲区,否则,分配一个帧,将设备块读取到帧中,将帧标记为缓存设备块对
从此帧读取/写入缓冲区。脏页定期写入块设备,显著加快I/O操作,尤其是文件系统元数据操作。
页面缓存思想还与内存映射的I/O相结合,mmap()文件采用类似的机制。进程的虚拟内存映射作为文件(而不是块设备)备份的页,更改会在内存上更新,缓存的帧会定期在磁盘上强制执行。VM系统跟踪页面和文件缓存以及其他(常驻和免费)页面的帧,VM系统根据系统的内存状态调整文件和设备缓存的大小。
缓冲区是在两个设备之间或设备与应用程序之间传输数据时存储数据的存储区域。进行缓冲有三个原因:
- 处理数据流的生产者和消费者之间的速度不匹配。通过适配器接收文件以存储在硬盘上。
(a) 无缓冲输入。(b) 用户空间中的缓冲。(c)在内核中进行缓冲,然后复制到用户空间。(d) 内核中的双缓冲。
- 在具有不同数据传输大小的设备之间进行调整。网络:消息通常在发送和接收过程中被分割。
联网可能涉及一个数据包的多个副本。
- 支持应用程序I/O的复制语义。应用程序调用write()系统调用,提供指向缓冲区的指针和指定要写入的字节数的整数。系统调用返回后,如果应用程序更改缓冲区的内容,会发生什么情况?在处理write()系统调用时,操作系统会将应用程序数据复制到内核缓冲区,然后再将控制权返回给应用程序。磁盘写入是从内核缓冲区执行的,因此对应用程序缓冲区的后续更改不会产生任何影响。
缓存在I/O级别完成,以提高I/O效率。缓冲区和缓存之间的区别在于,缓冲区可能只保存数据项的现有副本,而缓存根据定义,只保存位于其他位置的项的更快存储上的副本。缓存和缓冲是不同的功能,但有时一个内存区域可以用于这两个目的。例如,为了保留拷贝语义并实现磁盘I/O的高效调度,操作系统使用主存中的缓冲区来保存磁盘数据。
输入/输出软件通常分为四层,每个层都有一个定义良好的接口。
下面是有(右)无(左)标准驱动接口的对比图:
4 UE图形驱动
虽然原则上应用层不应该关心驱动层和GPU的细节,但往往事与愿违,众多的GPU、系统、图形API、版本造就了众多的驱动程序版本,它们之间可能存在一些奇奇怪怪的问题,而驱动程序有着很长的供应链路,修复问题的周期往往比较漫长。作为应用程序开发者,肯定不能坐以待毙,需主动解决或规避。
想到多年前,博主在工作中遇到一个奇怪的bug:在某个品牌GPU的某个版本的驱动程序采样的纹理颜色有问题。后来经过一周艰苦调试才发现,是那个驱动版本实现的texture.sample的坐标没有偏移到纹素中心,导致LUT纹理颜色出现巨大的偏差。解决的方法也很简单:针对那个驱动版本,自定义实现了texture.sample的shader代码。
UE提供了有限的接口和类型,为我们提供了一些信息,从而可以读取GPU或驱动的信息。相关的主要接口:
// GenericPlatformDriver.h
// 视频驱动细节
struct FGPUDriverInfo
{
uint32 VendorId; // DirectX供应商ID,0如果未设置,请使用以下函数设置/获取
FString DeviceDescription; // e.g. "NVIDIA GeForce GTX 680" or "AMD Radeon R9 200 / HD 7900 Series"
FString ProviderName; // e.g. "NVIDIA" or "Advanced Micro Devices, Inc."
FString InternalDriverVersion; // e.g. "15.200.1062.1004"(AMD), "9.18.13.4788"(NVIDIA)
FString UserDriverVersion; // e.g. "Catalyst 15.7.1"(AMD) or "Crimson 15.7.1"(AMD) or "347.88"(NVIDIA)
FString DriverDate; // e.g. 3-13-2015
FString RHIName; // e.g. D3D11, D3D12
FGPUDriverInfo();
bool IsValid() const;
FString GetUnifiedDriverVersion() const;
bool IsAMD() const { return VendorId == 0x1002; }
bool IsIntel() const { return VendorId == 0x8086; }
bool IsNVIDIA() const { return VendorId == 0x10DE; }
(...)
};
// GPU硬件信息
struct FGPUHardware
{
// 驱动信息
const FGPUDriverInfo DriverInfo;
FGPUHardware(const FGPUDriverInfo InDriverInfo);
FString GetSuggestedDriverVersion(const FString& InRHIName) const;
FBlackListEntry FindDriverBlacklistEntry() const;
bool IsLatestBlacklisted() const;
const TCHAR* GetVendorSectionName() const;
(...)
};
// GenericPlatformMisc.h
struct CORE_API FGenericPlatformMisc
{
// 获取GPU驱动信息
static struct FGPUDriverInfo GetGPUDriverInfo(const FString& DeviceDescription);
(...)
};
有了以上接口,就可以方便地在渲染层、游戏逻辑层获取驱动信息以执行针对性的操作。
除了GPU驱动,还可以访问图形API的驱动,常出现在各个图形API的RHI实现中,例如Android Vulkan:
// AndroidPlatformMisc.cpp
bool FAndroidMisc::HasVulkanDriverSupport()
{
(...)
// this version does not check for VulkanRHI or disabled by cvars!
if (VulkanSupport == EDeviceVulkanSupportStatus::Uninitialized)
{
// assume no
VulkanSupport = EDeviceVulkanSupportStatus::NotSupported;
VulkanVersionString = TEXT("0.0.0");
// check for libvulkan.so
void* VulkanLib = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
if (VulkanLib != nullptr)
{
UE_LOG(LogAndroid, Log, TEXT("Vulkan library detected, checking for available driver"));
// if Nougat, we can check the Vulkan version
if (FAndroidMisc::GetAndroidBuildVersion() >= 24)
{
extern int32 AndroidThunkCpp_GetMetaDataInt(const FString& Key);
int32 VulkanVersion = AndroidThunkCpp_GetMetaDataInt(TEXT("android.hardware.vulkan.version"));
if (VulkanVersion >= UE_VK_API_VERSION)
{
// final check, try initializing the instance
VulkanSupport = AttemptVulkanInit(VulkanLib);
}
}
else
{
// otherwise, we need to try initializing the instance
VulkanSupport = AttemptVulkanInit(VulkanLib);
}
dlclose(VulkanLib);
if (VulkanSupport == EDeviceVulkanSupportStatus::Supported)
{
UE_LOG(LogAndroid, Log, TEXT("VulkanRHI is available, Vulkan capable device detected."));
return true;
}
else
{
UE_LOG(LogAndroid, Log, TEXT("Vulkan driver NOT available."));
}
}
else
{
UE_LOG(LogAndroid, Log, TEXT("Vulkan library NOT detected."));
}
}
return VulkanSupport == EDeviceVulkanSupportStatus::Supported;
}
16.6 本篇总结
本篇主要阐述了图形渲染体系的驱动部分,包含它的概念、技术、架构、机制和相关的硬件部件。
在PC上,开发者可以要求用户更新其驱动程序,这样做相对容易。在移动设备上,驱动需要在多个阶段获得认证,从硬件供应商到客户的链条很长,这样做既昂贵又缓慢,错误修复和新功能可能需要数月时间,意味着bug修复可能需要很长时间才能进入用户的手中。在较旧/低端设备上,它们可能永远不会到来。目前正在努力改进这一点,但如果看到驱动程序错误,行业从业者准备解决它。这一情况正在好转(但进程缓慢)。
说到驱动程序bug,在移动设备上研究Vulkan驱动程序的团队通常没有在PC上那么大,有很多GPU的嵌入式变体需要测试。意味着驱动的质量常常有点落后。再加上发布修复程序的延迟,看看它是否有效或引入其他问题,驱动程序质量成为一个问题就不足为奇了。随着一致性测试的改进,驱动程序的测试变得更好,Khronos的硬件供应商非常清楚这是一个高度优先事项。需要行业从业者齐心协力改善这一点(CTS)。
随着现代图形API的到来,驱动不再进行隐式的优化,可预测的性能是可预测的低!使用新API启动新项目的开发人员具有优势(下图),新的图形API给大型软件安装群带来了更多的问题。构建渲染流,以便以正确的顺序获得所需的信息,在开始时要比改装容易得多。最重要的是,可以根据旧的API执行的驱动程序优化来编写引擎,以获得良好的性能。现在,软件开发人员拥有了控制权,使得性能可以预测,并确保应用了正确的优化,但也确实意味着由开发人员来确保这些优化存在。
DirectX11驱动程序(上)和DirectX12应用程序(下)执行的工作对比图。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
2022-11-22 视频云转码-GPU矩阵乘-ARM CPU 分析
2021-11-22 TVM apps extension示例扩展库