游戏会记录某个api的调用_(转)游戏画面卡顿问题的检测与解决对策

c3060a96b031d19338ff894ed801bc9b.png

Cem Cebenoyan NVIDIA开发者技术总监

画面卡顿 – 游戏体验的杀手

  • 当别人问你:
    • “游戏每隔几秒就卡一下…”
    • “测出来的帧数很高, 但总是觉得不流畅…”
    • “感觉人物动画一顿一顿的…”
    • “有时输入延迟很严重…”
    • ……
  • 你知道这是卡顿的问题, 可是
    • 哪儿出错了?
    • 严重损害游戏体验
    • 难以查出卡顿的根源,更加难以排除

在本讲座中,

  • 我们将讨论:
    • 游戏图形渲染中最常见的卡顿现象
    • 鉴别卡顿根源的方法
    • 消除卡顿的方案
  • 但不会涉及:
    • 由网络/磁盘IO, 音效, 及其它非图形因素造成的卡顿

议程

  • 常见卡顿起因一瞥
  • 卡顿的诊断
  • 起因分析与应对方案
  • 垂直同步, SLI及其它情况

常见卡顿起因一瞥

卡顿的各种表象

  • 帧率停滞(frame hitching)
    • 表象: 每隔一段时间,帧率突然下降或停滞,然后恢复正常
    • 可能起因: shader编译, 资源更新, 或显存分页交换
  • 微型卡顿(micro-stuttering)
    • 表象: 统计出的帧率很高, 但画面整体感觉不流畅
    • 可能起因: 帧与帧的持续时间极度不均匀
  • 计时偏差(timing discrepancy)
    • 表象: 帧率一切正常, 但镜头、动画和模拟等感觉忽快忽慢
    • 可能起因: 不合理测量得来的时间间隔,以及对缓冲帧数的不正确处理。

最常见的5种引起卡顿的原因

1. Shader编译

– 驱动需要把D3D汇编转化成GPU机器指令, 这一过程可能会带来停顿

2. 显存溢出(video memory oversubscription)

– 当显存用尽时, 会出现大量系统内存与显存间的分页交换(paging), 造成拖慢

3. 资源管理不善

– 在运行时创建, 销毁和更新资源容易造成性能颠簸

4. 缓冲帧(queued frames)利用不当

– CPU与GPU间需要用缓冲帧来解决负载平衡的问题, 但对缓冲帧的不合理设置会造成微型卡顿

5. 查询(query)使用不当

– 事件查询(event query)与遮挡查询(occlusion query)可能会改变驱动的行为, 有时会造成渲染管线阻塞

卡顿的诊断

卡顿的定性

• 卡顿的定性是一项艰巨的工作

  • 可能只出现在某一特定环境下, 或某个特定的硬件上
  • 难以取得有效的数据来进行分析

• 需要结合多种工具和多项试验来进行定性

• 在讨论细节之前, 需要理解一些基础知识:

  • CPU/GPU的通讯方式
  • Windows显示驱动模型(Display Driver Model, WDDM)

预备知识: CPU/GPU的通讯方式

8e15f896df839b544ce750252b6d2570.png
  • 每个D3D设备(D3D device)都有一个图形上下文(graphics context)
    • 负责维护一个命令队列(command queue)
    • 驱动把接收到的API调用放入命令缓冲(command buffers), 在合适的时候提交到command queue中
  • GPU的硬件资源由多个应用程序共享
    • 操作系统的全局图形调度器(graphics scheduler) 从多个 command queue中选取合适的命令包, 放入GPU硬件队列

(GPU hardware queue) GPU按顺序处理命令包, 完成后, 从队列中删除

预备知识: CPU/GPU的通讯方式(续)

• Command buffer的提交

  • 通常, 驱动会在Present函数调用后开始提交
  • 偶尔会在其它地方进行提交

• 帧延迟(frame latency)

  • 在没有帧缓冲(frame queuing)时, GPU比CPU慢一帧

d1fc84635bca4e8f62f50fc0a6df7bc2.png
  • 实际上, 驱动可能在提交之前最多缓冲3帧(4帧延迟)

预备知识: WDDM

• Windows显示驱动模型

  • 从Vista开始使用
  • 将显存纳入虚拟内存体系, 改善的容错性, 由操作系统进行图形任务调度, 等等

• 实际上有两个驱动

  • UMD: 用户模式驱动与应用程序和D3D runtime协同工作负责创建和提交command buffers
  • KMD: 内核模式驱动运行于操作系统的内核模式下

为操作系统提供管理硬件资源的接口

  • 操作系统负责操作command queues, 联结UMD和KMD

卡顿诊断所需的工具

• Fraps

  • 记录帧率
  • 快速收集统计数据

• Nsight

  • 静态和动态分析
  • GPU管线调查

• GPUView

– 深层次分析

帧率停滞(Framerate Hitching)的诊断

• 现象

– 每隔一段时间,帧率突然下降或停滞,然后恢复正常

• 先进行帧率的记录

  • 在游戏引擎中加入测时代码, 记录每一帧的持续时间 (每两个 present调用之间)
  • 或者, 使用Fraps的frametimes功能进行记录
  • 可以很容易从结果中找到停滞的帧

• 检查停滞帧的问题:

  • 是否有创建shader? 有新材质加载?
  • 是否首次引用了一块较大的资源? (texture, render target, buffers, etc.)
  • 渲染线程是不是因为资源更新被阻塞了?
  • CPU或GPU处理了不同其它帧的大量任务?

• Nsight可以用来检查停滞帧

  • 使用“Trace Application”来录制有问题的游戏部分

69482644c3c8dab539ac928d47c9b48f.png
  • 可以在时间线上看到shader的编译情况

3d8ce438d8df40e45267058c0c813b16.png
  • 可使用过滤功能来选出有问题的帧, 或者特别关注的API调用

• 对常见的可能起因进行实验

  • 如果发现停滞帧中有shader编译存在, 去除这些shader
  • 如果发现停滞帧中首次引用某种大资源, 去除这些资源
  • 如果发现停滞帧中有阻塞现象的Lock*, Map*, Update*函数, 去除这些函数调用

• 实验结果可以显示卡顿的起因

  • Shader编译的问题, 资源管理的问题, 等等.
  • 我们会在后面详细讨论每一种起因

帧率停滞诊断(续3)

• GPUView是一个更高级的检测工具

  • [优势] 海量信息: 所有command queues和GPU hardware queue 的详细内容, 每个包的内容和时间, 等等在后台录制时不会对应用程序造成影响
  • [缺点] 对新用户来说是个很大的挑战数据量太大
  • GPUView可以监测整个系统. 例如: 可以帮助检查为何某个游戏的Present调用被windows桌面阻塞?

15a5f7ef0eaf50680ff62397e839431a.png

微型卡顿(Micro-Stuttering)的诊断

• 现象

– 统计出的帧率很高, 但画面整体感觉不流畅

• 问题: 帧时间(frametime)严重不均

dcfaf971e25f2c476cb26e8cafe9f7a7.png
  • 可能的起因
    • 负载不均: AI、动画等任务隔帧执行或多帧执行一次,而不是每帧都在CPU上执行一次
    • 游戏引擎通过某些方式限制缓冲帧(queued frames)的数量, 造成驱动无法通过调度来隐藏不均匀的Present调用
    • 大量的资源更新
    • 显存溢出后, 频繁发生显存分页交换(paging)
  • 使用Nsight或GPUView进行监测
    • Nsight: 查看GPU frametime一栏
    • GPUView: 查看GPU hardware queue部分
  • 检查以下问题:
    • 帧与帧的CPU负载是否严重不均匀?
    • 游戏引擎是否通过某些途径来限制缓冲帧的数量?
    • CPU帧负载不均 + 无缓冲帧 -> 微型卡顿

• 检查资源更新时, CPU是否被长时间阻塞

  • 对Lock*, Map*和StretchRect函数, 用测时代码检测其阻塞PU的时间. 对这些函数来说, 如果指定的待更新资源正在被GPU使用, CPU可能被阻塞到GPU使用结束
  • CPU长时间被阻塞 + 缓冲帧->微型卡顿

• 在运行时估算显存的使用量

  • 结合WMI接口和游戏引擎自身的显存统计
  • 显存大幅度溢出 -> 频繁的显存页交换 ->微型卡顿
  • 频繁的显存页交换只会发生在显存大幅度溢出后, 少量的溢出不会带来严重的卡顿

计时偏差(Timing Discrepancy)的诊断

• 现象

– 帧率一切正常, 但镜头、动画和模拟等部分感觉忽快忽慢

• 可能的起因

– 游戏引擎的计时方式有误或有缺陷, 用于镜头和动画更新等的时间间隔不准确

• 检查游戏引擎的计时系统

  • 是不是用了每两个Present调用的时间间隔来作为下一帧动画更新的步进时间?

93a6667804c4e89de7863fc8dec4a96f.png
  • 如上图, CPU的步进时间可能和GPU的步进时间差别很大 -> 动画卡顿
  • 在这种情况下, CPU一端的Present到Present时间间隔并非真正的两帧之间的时间间隔!

起因分析与应对方案

起因

• 卡顿的5种最常见起因:

  1. Shader编译
  2. 显存溢出(video memory oversubscription)
  3. 资源管理不善
  4. 缓冲帧(queued frames)利用不当
  5. 查询(query)使用不当

Shader编译的基本知识

  • 驱动为何在运行时编译shader?
    • D3D汇编必须转化为GPU机器指令才能够执行
    • 每一代GPU的指令集都大相径庭
  • 何时编译? 如何编译?
    • 在调用Create***Shader后
    • 驱动中的编译器生成机器指令, 存储下来为后续使用
    • 对于复杂的shader, 驱动可能会先生成一套优化较少的指令, 然后在晚些时候替换上一套优化的指令
  • 驱动需要多长时间编译?
    • 依shader复杂度的不同, 需要几十到几千毫秒
  • 有什么办法预编译后存盘么?
    • 没有
  • 每个shader只编译一次?
    • 不一定. 有些D3D9的状态改变会引发shader被重新编译

状态相关编译(State Dependent Recompile)

  • D3D9的状态与GPU硬件的状态缺乏良好的对应关系
    • 很多D3D9状态的改变会引发shader的重编译
    • 特别是一些早期的GPU系列(D3D9级别的GPU) 有大量此类问题
    • D3D10.x and D3D11.x没有重编译的问题
  • “危险” 状态 (按严重性排列)
    • 绑定/解除绑定shadow map
    • 在同一个sampler上切换浮点数格式与非浮点数格式的贴图
    • 绑定一个与编译时格式不同的资源
    • 将sRGB状态应用到texture或render target上
    • 对同一个pixel shader, 改变COLORWRITEENABLE状态
    • 含有静态分支的shader(用布尔变量), 每个静态分支的组合都需要一次编译

在D3D9级别的显卡上,

  • 用户自定义的剪裁平面(user clip plane)
  • 固定管线中使用的雾的相关参数
  • MRT相关的状态

Shader编译: 应对方案

  • 老办法依然是好办法:
    • 在加载地图的时候创建所有的shader
    • 把所有的物体至少渲染一遍, 每个至少渲染一个三角形
    • 如果使用动态加载, 可以设置隐藏的物体, 把常用材质放上去, 在加载点将其渲染一遍
  • 如果上述方法无法做到,
    • 可以让驱动在运行时编译, 但不要在创建shader后马上就使用它
    • 在调用Create***Shader和调用Set***Shader之间, 给驱动留出500毫秒 ~ 1000毫秒的时间进行编译
  • 对于状态相关编译:
    • 把物体按照危险状态分组
    • 尽量减少或避免危险状态的变更
    • 确保让shader编译和运行在那些最常用的状态值下
  • 若使用D3D11, 使用异步创建可有效缓解编译带来的开销
    • 但注意, 不要有时用异步创建机制, 有时又不用
    • 最后不要忘了用Nsight来查看shader编译的情况

资源管理的基本知识

  • 资源的创建与销毁
    • 资源未必会在创建时分配显存, 而是在第一次被引用时(在使用WDDM的操作系统上)
    • 在运行时创建大块资源会带来巨大的开销
    • 调用Release函数不会马上销毁资源, 只是把资源的引用计数减1. 当引用计数下降到0时, 资源被销毁
    • 频繁创建/销毁资源会造成内存碎片
    • 尽量重用
  • CPU-GPU同步点(sync point)
    • 如果CPU要求GPU在某个API调用返回之前就完成当前的工作, 就会产生一个同步点
    • 1个糟糕的同步点就可能让游戏的帧数减半
  • 各种同步点
    • CPU立即更新一个GPU还在使用中的资源
    • 从一个render target中读回刚刚渲染的数据
    • 在释放了一大块资源后, 立即分配一大块资源
    • ……
  • 为什么同步点这么糟糕?
    • 理想状态下, 每帧的持续时间应该是max(CPU时间, GPU时间)
    • CPU-GPU同步点把这个持续时间变成CPU Time + GPU Time.
    • 一个长时间的同步点会产生一次卡顿

Ideal With Sync point

2b42d5633f706d0b32e70da81dc48de3.png
  • 回想一下, GPU较CPU有1~4帧的延迟. 一个随机出现的同步点, 对CPU来说意味着:
    • 清空所有的command buffer – 等GPU完成最多4帧的工作!
    • 出现卡顿
  • 在D3D9调用Lock需要额外注意
    • 锁定任何buffer, 如果flags=0, 那极可能是一个同步点(GPU正在使用该buffer).

资源管理: 应对方案

  • 通用原则 1
    • 调用Lock和Map的时候一定要使用DISCARD标记 (在使用DISCARD标记后, 得到的资源可能是一个新创建的资源, 不是原来的资源. 这个新资源在其后会替换原有的资源. 这个优化避免了同步点, 但增加了显存用量.)
    • 对频繁上锁的资源, 使用DYNAMIC类型. 在锁定时, 设法使用NOOVERWRITE标记 (驱动倾向于把DYNAMIC资源存放在系统内存中. 对于vertex/index buffers和小texture来说, 放在系统内存中不会对性能产生很大影响.)
  • 通用原则 2
    • 避免在运行时创建/销毁资源
    • 尽量在启动时分配资源, 运行时进行重用
    • 在重用一个资源之前, 调用查询功能(query)来确保GPU已经使用完毕该资源.
  • 仔细管理小buffer
    • 包括动画, 粒子系统, 界面元素, 等等.
    • 游戏引擎可以自行管理一个显存池, 用于buffer的重用和更新
  • 自行管理的无冲突显存池
    • 游戏引擎分配一块显存用来做缓冲池, 按照堆(heap)或环形缓冲(circular buffer)来管理
    • 设置3个链表: 可用空间, 占用空间和待释放空间
    • 在释放一块显存时, 先把它放入待释放空间链表, 调用一个查询(query)来确认GPU是否已经使用完毕. 确认后, 从待释放空间链表中删除, 放入可用空间链表

显存溢出: 应对方案

  • 如果一定需要在运行时创建/销毁资源:
    • 先销毁, 再创建. 避免内存碎片.
    • 短暂的显存溢出可能会引起驱动在内存管理策略上的改变
  • 显存的分配原则:
    • 先到先占用
  • 按照资源种类的重要性来调整分配顺序:
  1. Depth-stencil surface
  2. Render target
  3. 可随机访问的只读资源: Textures
  4. 流式访问的只读资源: Vertex buffer, index buffer 或尺寸较小的texture
对同类资源来说, 按尺寸和格式来分配:
  • 尺寸越大, 抗锯齿级别越高和浮点格式的资源应优先分配

• 显存溢出并不总是产生卡顿

– 如果关键性的资源(GPU可写资源)能够全部放入显存, 把部分只读资源放入系统内存不会造成太大的性能问题. 在这种情况下, 只读资源不会产生很多显存分页交换.

• GPUView可以用来跟踪显存分页交换

57c3e50cac400ada26ad970fd8130174.png
红色块代表分页交

缓冲帧(Queued Frames)

  • 帧缓冲的必要性
    • 为什么驱动总是试图缓冲更多帧?

f1772a08e6e8df4574e177fb7d9a4dee.png
    • 缓冲的帧越多, CPU被阻塞在Present调用上的机率就越少 (换句话说, CPU时间线上的气泡也越少)
    • 同样, GPU时间线上的气泡也越少
    • 驱动可以提前多帧进行调度, 避免CPU负载不平均带来的颠簸.
    • 在合适的时间提交command buffer
    • 在正常情况下,帧缓冲 -> 更高、更平滑的帧率

缓冲帧的矛盾

• 矛盾#1

  • 缓冲帧增加输入延迟. 降低缓冲帧的数量有益于更快的响应时间
  • 但是, 降低缓冲帧的数量会暴露不均匀的CPU负载, 增大微型卡顿的机率. 同时, 降低CPU和GPU的使用效率

• 矛盾 #2

–增加缓冲帧的数量有益于平滑的帧率

– 但是, 如果有不良的CPU-GPU同步点出现, 会产生更严重的卡顿.

• 如果决定要限制缓冲帧的数量, 那么

  • 应保证游戏引擎能够均匀的分配负载
  • 加强资源管理, 避免出现不良的同步点

缓冲帧: 应对方案

• 对缓冲帧的数量进行实验

– 在NVDIA控制面板中调整“最大与渲染帧数”

–检查卡顿是否有好转

cb003a56d664954eb1643a7f16174951.png

• 限制缓冲帧的方法

  • 不要直接从NVIDIA控制面板中强行设置! 会影响整个系统和其它游戏的性能
  • 使用事件查询(event query, 方法见DXSDK文档) 但这不是最有效的方法. CPU的效率会受损
  • 使用API函数:

IDirect3DDevice9Ex::SetMaximumFrameLatency

IDXGIDevice1:: SetMaximumFrameLatency

计时问题: 解决计时偏差

• CPU帧时间 vs. GPU帧时间

21b5dd1bdde882a06f1263f7f6b8e469.png
  • 游戏在时间点t0, t1, t2, …调用Present
  • 用户在时间点T0, T1, T2, …看到相应的帧
  • ∆t1- ∆t0不能用来作为动画等系统的步进时间, 因为真正的帧时间是∆T1 - ∆T0

• 两种情况

  • CPU是性能瓶颈, 采用CPU帧时间不会带来太大偏差

aad075cf5f4af17f9146437ad28150d4.png
  • GPU是性能瓶颈, 采用CPU帧时间可能会带来极大的偏差

3f75ac3733f9b6a4d0bd315cbfe2f406.png

计时偏差: 应对方案

  • 使用GPU时间戳(time stamps)
    • 调用Present后, 调用时间戳查询(time stamp query)来获取GPU完成Present的确切时间点.
    • 但GPU落后于CPU数帧, 这个时间戳要一定帧数后才能得到. 所以只能用来做估算用途
    • 为更快得到结果, 可在Issue调用后立即调用一次GetData, 并设置 FLUSH标记
  • 估算帧时间
    • 简易方法: 取最近几帧的平均值
    • 高级方法: 对比CPU调用Present的时间点和GPU完成Present 的时间点, 查看最近几帧的性能瓶颈是CPU还是GPU. 然后计算加权平均值.

合理使用查询(Query)

  • D3D中的异步查询(async query)
    • D3D9开始引入异步查询, 主要用来解决GPU落后于 GPU数帧的问题
    • 反复调用GetData来获取异步查询的结果, 会产生一个CPU-GPU同步点

while (S_FALSE == pQuery->GetData(…, D3DGETDATA_FLUSH));

    • 这种做法可能会造成卡顿

事件查询(Event Queries)

  • 事件查询可用于限制缓冲帧
    • 有利于降低输入延迟, 但是…
    • 同时会暴露不均匀的CPU负载, 产生微型卡顿
    • CPU必须等待查询返回, 降低了CPU和GPU的并行性, 导致低性能
    • 驱动无法进行多种优化和调度, 因为没有提前多帧的数据

遮挡查询(Occlusion Query)

  • 遮挡查询往往会有很高的延迟
    • 查询结果在1~3帧后返回
    • 避免反复调用GetData直到结果返回的做法, 因为会到来非常严重的停滞: 首先, CPU等待GPU返回结果, 然后, GPU等待CPU发送当前帧 CPU-GPU处于串行工作的状态。这种做法可能会抵消使用遮挡查询带来的性能提升.

遮挡查询: 应对方案

  • 使用查询时需额外小心
    • 保证查询的使用不会在时间线上引入额外的气泡
    • 理想状态下, 优化资源的管理, 不要使用限制缓冲帧的方法.
    • 有效使用非阻塞方式进行遮挡查询(本讲座中未作讨论)

检查使用的中间件

  • 中间件往往都是在”真空”里开发的
    • 小环境里写出来的系统未必能够适应实际环境
  • 特别需要检查中间件是否造成了CPU-GPU同步点

垂直同步(Vsync)

  • 垂直同步可能会带来微型卡顿
    • 帧率在垂直同步点之间跳跃: 60fps, 30fps, 20fps, …
    • 游戏可以自行实现限帧机制, 避免垂直同步带来的瞬间大幅度帧率跳跃
  • 最新的NVIDIA控制面提供自适应垂直同步选项
    • 当帧率降低到指定的垂直同步点以下时, 自动关闭垂直同步功能

SLI

  • 在多GPU环境下, 微型卡顿更容易出现
    • 多颗GPU可能在不均匀的时间点上完成Present
    • 同步点造成的阻塞问题更加难以消除
    • GPU间的数据传输会增加更多的同步点
  • 驱动有义务为SLI消除卡顿
    • 但游戏也须为此进行优化
    • 深入的多GPU情况讨论不在本讲座的讨论范围内

其它卡顿的起因

  • 一些较为少见的卡顿起因:
    • GPU状态切换如果游戏使用了CUDA, compute shader等利用 GPU通用计算, 那么在图形和通用计算的切换时, command buffer可能会在不合适的事件点被清除
    • 多个D3D设备间竞争硬件资源对于需要多开的游戏, 可能会在多个渲染设备间产生这种问题
    • 系统paged/non-paged pool不够用 :在XP 32bit下, paged/non-paged pool不够用有一定可能造成卡顿, 特别是那些同时使用大量资源的游戏
    • 还有更多可能的起因, 限于篇幅, 此处不作讨论.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值