图形性能剖析工具链 · 第 四 篇

事件浏览器:一帧 draw-call 列表到底怎么读

2026-09-2312,692 字20 张配图本文由@溯光 出品,转载请注明出处

CSDN 版文章链接: {csdn-url} 知乎专栏版文章链接:{zhihu-url}

题图:RenderDoc 打开真实捕获后的默认界面,左侧正是本节主角——事件浏览器
题图RenderDoc 打开真实捕获后的默认界面,左侧即事件浏览器,本节的绝对主角@ RenderDoc(MIT)

一句话点题:一帧 draw 命令,从这里开始被读懂

要是你已经看完了本系列前两篇,应该记得抓帧(ep02)把"一帧"定格成了 .rdc,回放(ep03)把这台机器从文件重新变成"活得可交互的状态"。那么问题来了:文件回来了、状态活了,你在界面上到底面对什么?

答案是:一个按时间顺序铺开的命令列表,RenderDoc 官方管它叫 事件浏览器(Event Browser),业内也常直呼"draw-call 列表"。官方文档把它定性为"浏览帧并选择不同 action 的主要方法",是第一落点窗口[event_browser]。社区里有文章干脆把它称作"the heart of analysis"和"central nervous system"[cosmiclearn-dx12]。

这一篇,我们就把这块"心脏"拆开:它列出来的到底是什么、树状层级从哪来、为什么 EID 老是不连续、点一下到底发生了什么、过滤器表达式怎么写才不会踩坑、时间轴那根条有什么用。主图取自 RenderDoc 官方仓库的 MIT 许可截图,官方对这些开关的行为声明原本就配了界面图,正好拿来图解;再辅以官方文档口径做旁证。想亲眼看真实的 .rdc 什么效果,自己机器上随便开一份复现即可。

把它排在 ep03 之后、ep05 之前不是顺手起的序号,而是有因果的:ep03 证明了"这台回放引擎能逐帧推进、按需产出资源",可那毕竟是引擎内部的事,我们看不见摸不着。事件浏览器正好是它对外开的第一扇窗——你在左边点一行,右边几十个窗口就跟着变,这个"联动"不是界面廉价效果,正是 ep03 那套"把回放状态设到某事件之后"的能力暴露成了交互。反过来说,你要是只会看整帧最终画面、不会在事件浏览器里定位和切状态,那 ep03 讲的回放确定性,对你来说就只是纸面上的概念。所以这一篇是承上:把 ep03 的能力变成手上能用的动作。

也先划个边界,本文不讲的范围有四块,省得你期望落空:一是 API 检查器(API Inspector)内部的逐字段怎么读,那是另一扇完整窗口,后续单独开题;二是抓帧工具本身的启动、连接、采集流程,那属于 ep02 的范畴;三是多帧/时序分析(怎么抓十帧、看趋势),它用的不是单帧回放这套心智,本系列讲 Perfetto 时再碰;四是 RenderDoc 的 Python/脚本自动化,它面向"批处理验证"而非"人眼排障",另文再说。本文只回答一件事:拿到一份单帧,如何在事件浏览器里把"命令流"读成"可验证的状态流"——范围卡在这,才不会又写成一本按钮说明书。

OK,言归正传,先把最容易混淆的一个词掰清楚。

一、先分清:你看到的是 action,不一概是 draw

1.1 术语澄清:action ≠ 光栅化绘制

事件浏览器里每一行事件,官方叫 action(动作),指的是"draws, dispatches, copies, clears, resolves,以及其他让 GPU 干活、或影响纹理和缓冲区等资源的调用"[event_browser]。也就是说,一次 DrawIndexed 是 action,一次 vkCmdDispatch(compute 派发)、一次 vkCmdClearColorImage(清屏)、一次 vkCmdCopyBuffer(拷贝)同样是 action。

官方文档特别给了一句提醒:"有时被称为 drawcall,但用 action 这个词更不歧义",因为它要和真正的光栅化绘制区分开。这并非咬文嚼字,而是读列表时的基本功:如果你心里只装着"draw",看到列表里一排 Copy、Clear、Dispatch 就会下意识把它们当下级的噪声过滤掉,可它们同样是撑起一帧的关键命令。

把这五类 action 摆成一张对比表,"都是 action、但各干各的活"就一目了然了:

action 类别 典型 API 在帧里承担什么 该不该当噪声滤掉
光栅化 draw Draw / DrawIndexed / DrawIndexedInstanced 真正把三角形画进 RT 绝不,这是主线索
compute dispatch vkCmdDispatch / DispatchMesh 跑 GPU 计算,不经过光栅化 高光/后处理常常是它,别只盯 draw
clear vkCmdClearColorImage / ClearDepth 把 RT 重置成底色/深度 频率高但必要,定位第一笔写时有用
copy vkCmdCopyBuffer / CopyImage 在资源间搬数据、传回 CPU resolve/回读耗时的大头常在这
resolve vkCmdResolveImage 把 MSAA 的中间结果合并成最终像 全屏 pass 后常见,逐像素问题别漏它

读这张表的要点是:一帧的正确心智是"一条 action 流水线",而不是"一串 draw"。compute 负责的东西越来越多(Lumen 式光照、物理模拟、遮挡剔除都往 compute 跑),如果还只习惯看 draw,你会系统性漏掉 GPU 上真正吃时间的部分。这也呼应了第 6 章:那里的过滤表达式里 Dispatch 就是要能被单独筛出来的——它和 Draw、Copy 是平权的 action,不是某棵树下的小跟班。

action 家族分类:draw/dispatch/clear/copy/resolve 同属 action 一类(本系列绘制)
图 4-1action 家族分类:draw/dispatch/clear/copy/resolve 同属 action 一类@ 本系列绘制

1.2 默认只看 action:state 设置藏在别处

事件浏览器默认只列 action,这是很多人第一次打开时觉得"命令是不是被藏起来了一部分"的原因。真相是:状态设置等 CPU 更新调用(比如 vkCmdBindPipeline、vkCmdBindDescriptorSets、Maps)默认不出现,它们被归到另一扇窗口 API 检查器(API Inspector) 里[how_filter_events]。在 Vulkan 教程的说法里,RenderDoc 让你"freeze a single frame…re-execute individual draw calls and compute dispatches in isolation",但你看到的隔离粒度是这些 action[vulkan-tutorial-analysis]。

这意味着第一层读法就是:你想要"这条命令到底干了什么",看事件浏览器;你想要"这条命令之前的渲染状态是怎么配出来的",去 API 检查器。

一次完整的调试里,这两扇窗是轮流用的,分工其实很干净:

想知道什么 去哪扇窗 例子
这条命令画了什么、画了多少 事件浏览器 一次 DrawIndexed(6, 16) 本身
这条命令之前 pipeline / 状态是怎么配的 API 检查器 该 draw 用了哪个 PSO、绑了哪些 descriptor set
一个绑定值是谁在哪个时刻设的 API 检查器 + 事件的调用序 vkCmdBindVertexBuffers 是在第几条 action 之前被调用的

反直觉的地方在于:前者(action 流水)是逐光栅化动作的,后者(state 流水)是逐状态覆盖的。state 调用只有"最后一次生效"有意义,所以浏览器把它按覆盖顺序记录,而不是当成独立的可画东西。这也解释了为什么默认列表里看不到它们——不是被砍了,是它们本就属于另一条时间线。

默认只列 action 的事件列表:状态设置调用不在其中(官方索引,MIT)
图 4-2事件浏览器默认只列 action,状态设置调用不在其中@ RenderDoc(MIT)

二、树状层级从哪来:标记(Marker)把你的帧织成一张可折叠地图

2.1 平铺的调用链,如何变成一棵树

照理说,一次渲染一整帧,命令就是一条平铺的 API call → API call → API call 序列。可你打开事件浏览器,看到的却是可以展开折叠的树。这是怎么来的?

答案在 标记(marker,也叫 region/annotation)。应用可以在录制时用对应 API 的调试标签接口,把一组命令"包"成一个具名区段:Vulkan 用 vkCmdBeginDebugUtilsLabelEXT / vkCmdEndDebugUtilsLabelEXT,D3D 用 PIXBeginEvent / PIXEndEvent,OpenGL 用 glPushDebugGroup / glPopDebugGroup[how_annotate_capture]。RenderDoc 在回放时读这些标签,就把命令按区段组织成树节点。

Vulkan 教程有一段很实在的总结:这些标签"会以具名区段的形式出现在 RenderDoc 的事件列表中,使导航变得 trivial——即使这一帧里有几千条 GPU 命令"[vulkan-guide-debugutils]。

所以第二层读法就建立起来了:树的形状不是你显卡跑出来的,是开发者当初用标签"织"出来的。一个不注解自己帧的引擎,事件浏览器就是一马平川;一个像 UE 那样深度标记的引擎,事件浏览器就是一份目录清晰的"渲染分镜"。

把"织"这个过程落到一棵爱上特别直观。假设某个引擎把一整帧包成这样的标签层级:

帧 begin
├── Scene Render(场景主 pass 的 begin)
│   ├── Shadow Depth(阴影 pass 的 begin)
│   │   └── DrawIndexed(...)  × 若干
│   │   └── (Shadow Depth 的 end)
│   ├── Base Pass(begin)
│   │   └── DrawIndexed(...)  × 大量物件
│   └── (Scene Render 的 end)
└── Post Process(begin)
    └── 各 blur / grade 的 draw
    └── (Post Process 的 end)

展开这份树时,你真正在做的不是看渲染,而是在对着引擎当时给每段代码起的名字做导航:哪一段是阴影、哪一段是主场景、哪一段是后处理,全靠标签命名。那些 draw 全都撞在同一层里,就很难一眼分出"这条属于哪个 pass"——名字才是你的地图。所以接手一个陌生引擎的帧,第一步往往不是找那条最长的 draw,而是先把树的根摸清,给每个 marker 的命名"建立索引"。

标注力度差异也直接决定排查成本:UE 这类引擎在源码里调用了 PIXBeginEvent 式接口,把每个 pass、每类 mesh 都点名,树能一路层层展开;而有的自研引擎只在最外层包一个"Frame",中间全平铺——这种帧里要定位某个对象只能靠 numIndices、pipeline 之类的属性盲搜,效率天差地别。这也是为什么很多团队在接调试器时,会顺手给自家录制代码补注解,本质上是在"给未来的调试铺路"[how_annotate_capture]。

平铺调用链被 Begin/End 标记织成可折叠树(本系列绘制)
图 4-3平铺调用链被 Begin/End 标记织成可折叠树@ 本系列绘制

2.2 marker 的着色与"所在区段"指示条

标记不只决定树的形状,还带来两处视觉标记:一是区段的颜色会作用到该区段的整行行首,二是沿树左侧会画一条色条,标示当前事件正处于哪些区段里[event_browser]。这两点对读图特别有用:你点选一个很深的 draw,看左侧色条,就知道它一路处于哪些 pass 之下,一条垂直的"血缘"立刻可见。

这跟一棵树"看祖先"的直觉并不完全相同,值得说明白。单独看树,你能分清每个节点的父子;但一次 draw 到底是"Scene Render 内 Shading 内"的哪一级,有时要展开好几层才看清。而着色机制把"当前事件处在哪几个区段"一次性压进了最左侧那条竖条:每一段颜色代表一个尚在开启中的区段,上浅下深的堆叠顺序正好是区段的嵌套外到内。所以读深 draw 时,扫一眼竖条就能在两三个 PASS 之间快速建立"这条命令的血统",比逐层展开高效得多。这对不注解帧格外有用——一个只包了一层"Frame"的树,左侧几乎只有一抹颜色,你一眼就能判断这帧的标注力度在哪一级,从而决定下一步是直接搜属性还是先补注解[how_annotate_capture]。

同时,marker 的名字也不只是给人看的:RenderDoc 的事件浏览器把自定义名当成本质上"可搜索、可过滤"的一样东西。第 6 章会把这一点变成表达式里的 $parent(render scene),第 5 章会说到"自定义名其实反映驱动真正提交的东西"。所以 2.2 这一段先把结论立住——命名即索引:你给 marker 起的名字,会成为之后你通过书签、过滤器、EID 反查这整棵树的入口。这也是为什么很多引擎在 release 版也保留 marker:比起暴漏内部实现,它们更在意让你快速回到问题的现场。树、着色、命名这三件事合成一块,事件浏览器才能从"平铺命令表"升级成"可导航的帧地图"。

具名区段着色:marker 区段以颜色区分(官方索引,MIT)
图 4-4具名区段以颜色着色,并在树左侧为当前事件画所在区段指示条@ RenderDoc(MIT)

三、EID 语义:为什么事件编号老是不连续

3.1 EID 从 1 递增,每次 API 调用都 +1

事件浏览器第一列是 事件编号(Event ID,EID)。官方对它的定义非常朴素:"EID 从 1 开始,每发生一次 API 调用就增加一次"[event_browser]。

这句朴素的话暗含一个反直觉结论:EID 数的是"API 调用次数",不是"action 条数"。因为抓帧器会在每次 API 调用时都分配一个 EID,无论这次调用是 action(draw/clear/copy)还是 state 设置(bind pipeline / bind descriptor set)。而默认列表只显示 action,因此你看到的 action EID 之间会"缺号"——缺掉的那些序号,正是被隐藏的 state 事件占掉的。

举个例子:假设事件浏览器里某行的 EID 是 72,下一行直接跳到 75,那不是抓帧抓丢了,而是 73、74 分别是两次你默认看不到的状态绑定调用。

把这个"缺口"和"谁占用了缺口"摆成一张对照,读完整帧的 EID 就能心里有数了:

列表里显示的 占的 EID 你看不看得见
一条 DrawIndexed 假想 EID 72 默认可见
一条 vkCmdBindPipeline 假想 EID 73 默认不可见(state,跑 API 检查器里)
一条 vkCmdBindDescriptorSets 假想 EID 74 默认不可见(state)
下一条 DrawIndexed 假想 EID 75 默认可见

所以第 72 行和第 75 行中间"跳掉"的其实从来不是丢了命令,而是这 73、74 号正藏在你关掉的 state 视图里。读 EID 的第一原则就是:相邻 action 之间的差,才能告诉你中间埋了多少条状态设置——差距越大,说明这条 draw 之前的状态越"翻箱倒柜",常常也越值得留意(比如是不是每帧都在重建 pipeline)。这条"用 EID 缺口反推状态活动"的读法,在 6.5 想专门把绑定调用调出来看时,直接就能对上号。

把 EID 当成"回放指针"来看就顺了:它既是列表里的行号,也是回放引擎内部可寻址的稳定句柄。ep03 讲过回放引擎会递增地"推进帧事件",这里用的正是同一个 EID 序列——你在界面上点 74、点 75,回放引擎就把状态分别推到"第 74 条调用之后"和"第 75 条调用之后"。正因为每次运行回放都会重新得到同一套 EID,它才稳定到能被其他窗口引用:一旦某个工具告诉你"问题出在 draw 9120",无论你关掉重开、换机器复现,只要还是这份 .rdc,9120 永远指同一条命令。这个"全局稳定"正是后面 3.2 能搜 EID 直达、以及各窗口能通过 EID 联动的根因。

EID 每次 API 调用 +1,action 之间夹杂 state 调用故不连续(本系列绘制)
图 4-5EID 每次 API 调用 +1,action 之间夹杂 state 调用故不连续@ 本系列绘制

3.2 用 EID 直达:搜数字跳跃到目标事件

正因为 EID 是"绝对时间次序"的锚点,事件浏览器提供了 Ctrl-F 查找,可以直接搜一个 EID 跳到对应事件[event_browser]。这在对接其他窗口时尤其好用:例如 Meta 官方文档提到,在性能计数器窗口里"每一行代表一个 draw call,双击这一行就能在事件浏览器里选中对应的 draw call"[meta-renderdoc-oculus]。其背后正是用一个 EID 把两个窗口串起来。

这个"搜数字直达"要比表面看起来有分量:它把 EID 从一个"列表行号"升级成了横跨整个调试工具的通信协议。性能计数器报"draw 9120 异常",你 Ctrl-F 输 9120,就落到那条命令;Pixel History 列出的每笔触达,也带 EID 供你反查;甚至你在脚本或团队工具里导出的每一条嫌疑记录,其实都靠 EID 承载"位置"。所以退化一步想:当你在任何窗口看到一串数字、一段能定位的 EID,别把它当普通序号丢掉,那很可能是这条线索的通关文牒。等到 9.2、9.3 讲查找和复制时,你会发现"搜 EID、拷 EID、报 EID"是同一枚硬币的三面。

EID 列:事件间可见被隐藏 state 调用占掉的不连续序号(官方索引,MIT)
图 4-6EID 列:事件之间的不连续序号,正是被隐藏的 state 调用占掉的@ RenderDoc(MIT)

四、点选一个 draw,到底发生了什么

4.1 选中 = 把回放状态设到"那条命令执行完的微秒之后"

这是事件浏览器最容易被低估的一个机理。社区文章的比喻是:"点选某个事件,就是告诉 RenderDoc:把整条图形管线的状态设到该命令执行完毕的那一个瞬时之后"[cosmiclearn-opengl]。官方更严谨:点选某事件会让其成为 current event,并体现在 UI 的所有其他窗口(管线状态、资源、纹理、网格……)[event_browser]。

再往细里走一点:如果你选中一个带子节点的节点,等于"视作它所有子事件都执行完"来显示状态。也就是说,你把一个 pass 的收尾节点选住,看到的就是整个 pass 收尾后的状态。

举个例子帮助记忆:假设 Scene Render 是一个标签节点,底下挂着一串 draw。你点选它,看到的是"这串 draw 全跑完之后"的 R/W 状态、输出资源的样子;你要点选它最底下那条 draw,看到的才只推进到那条命令的末尾。同一个节点里,点层级本身和点叶子命令,得到的状态是不同的——区别就在"包含我下面的所有,还是只含我一个"。这个差异在排查"整 pass 画面出问题"还是"单条 draw 出问题"时,是第一条要分清的判断闸。

"其余窗口跟着变"具体是哪些、各看到什么,值得列一张清单,读图时才不会左手找纹理右手找管线:

窗口 点选后给你看什么
管线状态(Pipeline State) 当前事件用到的 PSO、各阶段着色器、顶点/索引布局
纹理查看器 选中纹理在当前事件之后的样子(R/W 状态同步到该点)
网格查看器 当前 draw 喂进去的顶点/索引缓冲,解析成几何再呈现
资源/描述符(Descriptor)窗口 该事件引用的纹理、缓冲、采样器把手,可直接反查
常量缓冲(CB/UBO)查看器 当前事件绑定的常量缓冲内容,实时抓到这一版的数值

有了这张表,你"点一下边看"的路径就变成了:先在事件浏览器选行定状态,再跳到具体资源窗口追问细节。所有窗口都以同一个 current event 为基准——这正是 ep03"把回放状态设到某事件之后"能力在界面上的集体暴露[event_browser]。

选中=把回放状态推进到该命令执行完的瞬时,其余窗口联动(本系列绘制)
图 4-7点选事件=把回放状态推进到该命令执行完的瞬时,其余窗口联动@ 本系列绘制

4.2 逐 draw 隔离:re-execute in isolation

与"设到某瞬时"并行的,还有隔离重放:官方文档确认 RenderDoc 可以 "re-execute individual draw calls and compute dispatches in isolation"[vulkan-tutorial-analysis]。这正是"这到底画了什么"这类问题的标准答案:不再依赖整帧共同作用,而是单独放一条 draw,看这条命令自己产出的结果。于是"逐 draw 断点验证"就成了本系列自 ep03 引入回放确定性之后,真正落到界面上的第一把验证工具。

它真正用起来是这样一步步走的,整套动作就一条逻辑链:

  1. 在事件浏览器里选中一条可疑的 draw(比如那片不该出现在这里的高光);
  2. 打开纹理查看器(Texture Viewer),选中它写入的那张 RT;
  3. 点该 draw 的"isolate"动作——RenderDoc 会回放这一条 draw 并把它作为帧里唯一一次写操作的状态单独呈现;
  4. 盯着纹理看:这张 RT 上现在还剩下什么?是你怀疑的那片高光,就实锤是这一条画出来的;如果是空的,那高光多半来自后面某一笔叠加。
  5. 再前后移动 current event,配合第 4 步,就能逐步逼近"到底是第几条 draw 造成这个效果"。

这五步看着简单,实则吃透了 ep03 的两个机制:一是回放非破坏性——隔离执行不会把真帧弄脏,你随时能切回,所以可以放心大胆反复试;二是按需出声——隔离的默认状态来自"该 draw 之前的资源内容",正因为 ep03 里资源和状态可以按事件精确推进,这一步才成立。隔离验证的每一步,其实都是在替 ep03 的回放引擎做一次"状态求值"。也正因如此,事件浏览器 + 纹理查看器 + 网格查看器(Mesh Viewer) 这三扇窗口常常是排一条 draw 嫌疑时全程联动的那一组。官方文档里 isolate 专属的描述、以及各查看器与事件浏览器的联动,都在对应窗口的文档里写了[video_viewer]。

逐 draw 隔离与联动:选中事件后其余窗口同步到该命令之后(官方索引,MIT)
图 4-8逐 draw 隔离:选中事件后其余窗口同步到该命令之后@ RenderDoc(MIT)

五、读参数:三档显示开关,把名字和外貌调出来

5.1 默认只显示"最有意义"的参数

事件浏览器里每一行的文字是"从该 API 调用的参数推导出来的"。默认情况下只展示最有意义的参数,参数名也不显示,只给值[event_browser]。所以你会看到空荡荡的 DrawIndexed(6, 16) 这种行——幸运的是,应用若设置了 marker 或对象名,名称就会更友好。

有一个很容易掉进去的读图误区要在这里先钉死:"最有意义"不等于"看起来顺眼",它取的是开发者在写这个 action 时认为最能定义它量级的那组值。正因为如此,DrawIndexed(6, 16) 的法则是"索引起点、索引数量、实例起点、实例数量",但很多图形 API 的 draw 参数顺序并不统一——OpenGL 的 glDrawElements 是 mode + count + type + indices[gl_draw_elements],Vulkan 的 vkCmdDrawIndexed 是 indexCount + instanceCount + firstIndex + vertexOffset[vk_cmd_draw_indexed]。所以别默认"第一个数就是顶点数",要先想清楚这个 action 用的是哪套 API 的哪个函数,它的第几个参数才是你关心的量。把参数名单个显示档打开(5.2 里有开关),正是为了在分辨不清时把"第几号是索引起点"这个歧义消掉——这一点放到排障里,能避免"把 firstIndex 当成 indexCount"这种低级却致命的误判。

"最有意义"四个字背后是有取舍逻辑的,值得读透。一次 DrawIndexed(6, 16),真正决定"这画了多少"的,是索引数量 6 和实例数量 16 这两个值;至于绑定的是哪个 vertex buffer、用哪份 descriptor set,那些属于"跟这台命令配的状态",默认不放进来是对的,因为塞进来你会被几十个 handle 淹没。所以默认档的核心设计是只保留"一看就知道这条命令量级"的参数,把"连接关系"留给 API 检查器和后续查看器去追问。读懂这个取舍,你才不会理所当然地以为"默认看不到就代表不存在"——它只是刻意不把噪音摆到你眼前。

5.2 两档开关把"谁"和"怎么配的"调出来

事件浏览器在 Settings & Help 里提供几个显示开关,读懂它们能迅速改变你看到的信息量:

  • Show custom action names(显示自定义 action 名,默认开):决定是否用自定义名(比如 indirect draw 显示 GPU 侧实际 draw 参数、renderpass 的 load/store op)而不是原始调用名[event_browser]。
  • Show parameter names and values(显示参数名和值,默认关):在值之外把参数名也列出来,例如不再是 (6, 16) 而是 (numIndices=6, instanceCount=16)。官方提示:对带自定义名的 action 此开关无效。
  • Show all parameters(显示全部参数,默认关):把所有参数都铺开——官方的说法是"全展开后可能很难看清,因为有些函数有不重要的隐式参数(比如 command buffer)"[event_browser]。

第三层读法的价值就在这:默认状态是为了"干净",但当你需要确认一次 draw 到底绑了多少个实例、是从哪个 baseVertex 开始的,就要在默认名开、参数名开、必要时全参数开之间切换。一个常见场景:日本一支优化团队在 UE5 优化系列里就靠读 DrawIndexedInstanced(6, 16) 反推"这是小网格(树叶)×16 一起画的粒子或实例草"[furcraea]。

下面三张图是同一份捕获在三种显示档位下的实际样子:第一张只开默认名,行文字是最精简的自定义/API 形态,适合扫一眼整个列表的命令骨架;第二张打开参数名与值,numIndices、instanceCount 这类关键参数直接出现在行内,能快速确认一次 draw 到底投了多少实例、从哪个索引段画起;第三张把所有参数全部铺开,连 command buffer 这类隐式参数也现身,信息量最大、行也最长。同一个 draw 在三种档位下读出来的广度完全不同,这也解释了为什么默认档位刻意保持"干净"。

把三档摆成一张速查表,什么时候切哪一档就很清楚了:

档位 行的样子 什么时候用
默认名(默认开) Scene_Render、DrawIndexed(6,16) 扫命令骨架,看有哪些 pass / 哪些大对象
参数名+值 DrawIndexed(numIndices=6, instanceCount=16) 确认一次 draw 的规模、实例数、顶点索引段
全参数 所有参数铺开(含隐式参数) 定位你跟的那条 draw 具体绑到哪个 handle、哪个 load/op

注意一个反直觉点:默认名档开的是"自定义名",不是"原始 API 名"。所以当某次 draw 看起来"诡异"到你怀疑是不是引擎写错了参数时,先别急着信行里的数字,想一想这个名字是引擎自定义的、还是 API 原生的。indirect draw 这类自定义名甚至会显示 GPU 侧实际算出来的 draw 参数——那反而是你要的真相,因为它反映的是驱动真正提交的那份。判断"这个名代表什么",比"数字叫什么"更接近问题的根[event_browser]。

默认名开的显示:行文字是最精简的自定义/API 形态(官方索引,MIT)
图 4-9默认名档:行文字是最精简的自定义/API 形态@ RenderDoc(MIT)
参数名+值显示:numIndices/instanceCount 等参数名出现(官方索引,MIT)
图 4-10参数名档:numIndices/instanceCount 等参数名出现在行内@ RenderDoc(MIT)
全参数显示:所有参数铺开,含 command buffer 等隐式参数(官方索引,MIT)
图 4-11全参数档:所有参数铺开,含 command buffer 等隐式参数@ RenderDoc(MIT)

六、过滤表达式:把几千条命令变成想看的十几条

6.1 现代帧的量级问题

一个现代游戏的一帧可以有 5,000 条乃至更多事件[cosmiclearn-dx12]。官方文档和引擎教程都强调,frames 里"未知哪一个 draw 是罪魁祸首"时,手工翻找又慢又费劲[vulkan-ai-analysis]。所以事件浏览器顶部提供了一个过滤器输入框,配一套比普通搜索更强的表达式语法[how_filter_events]。这一节我们把语法规则翻译成人话,并标出最容易踩的坑。

6.2 基础规则:空格=OR,加号=AND,减号=排除,括号=分组

表达式 语义 举例说明
Draw Clear Copy 空格分隔词之间是 OR Draw、Clear、Copy 任一命中即显示;Dispatch 因不匹配任何词而被滤掉
-Depth 前缀减号 = 排除 上个表达式加 -Depth 后,ClearColor 保留、ClearDepth 被排除
+Draw +Indexed -Instanced 加号 = 必须含,减号 = 必须不含 必须含 Draw 且含 Indexed 且不含 Instanced
+Draw +(Indexed Instanced) -Indirect 括号 = 分组,内部仍是 OR 必须含 Draw,且 Indexed/Instanced 至少一个,且不含 Indirect

这四行规则已经覆盖了绝大多数日常过滤需求,把它们排成一张判定图能看得更清楚:空格与加号都表示"词要出现",区别在于空格是把多个备选词并列(命中任意一个即显示),加号则是把约束一步步收紧成 AND;减号在前面画一道排除线,括号则把一组 OR 词先捆成一个整体再去匹配。尤其要对比第三、四行:+Draw +Indexed -Instanced 要求 Draw 与 Indexed 同时命中且不含 Instanced,而 +Draw +(Indexed Instanced) 里括号把 Indexed 和 Instanced 捆成一组,命中其中任意一个就算满足。从 OR 换成 AND 的瞬间,显示的行会明显收窄——这正是从"扫一眼所有 draw"跨到"精确锁定某一类命令"的关键动作。

在给语法规则记熟了之前,先搞清楚一件事:这些词到底匹配的是事件的哪一个字段。官方口径是,过滤器的默认匹配对象是事件的 label(行文字),也就是我们在第 5 章调出来的那串名字(自定义名或 API 名加上参数)。所以 Draw 能命中 DrawIndexed,是因为它的名字里带着"Draw"这几个字;Copy 命中 CopyBuffer 同理。想靠原始 action 类型(比如只挑 Dispatch)来过滤时,就直接匹配类型名——Dispatch 这个词出现(或不出现)在名字里,就达到了"只看 compute"或"排除 compute"的目的[how_filter_events]。

另一件容易被忽略的事是:optional 词(不带 +)不是"匹配就能命中一把"的唯一条件,它是"事件只要 name 里出现过这个词就展示"的宽松匹配;required(带 +)和 excluded(带 -)才是硬约束。把这句记进心里,6.3 那个"坑"就顺理成章了。

过滤表达式语义对照:空格OR 加号AND 减号排除 括号分组(本系列绘制)
图 4-12过滤表达式语义:空格OR / 加号AND / 减号排除 / 括号分组@ 本系列绘制

6.3 一个大坑:required 词一出现,optional 词就被整组忽略

官方文档专门用了一整段 warning 强调:一旦过滤里出现了 required 词(带 +),那么所有 optional 词都会被完全忽略[how_filter_events]。原文给出的例子是:

+Draw -Instanced

与

+Draw Dispatch -Instanced

匹配的是完全相同的一组事件。为什么?因为官方语义下,"是否包含某事件"只由"它是否满足 required 词、且不违反 excluded 词"决定,optional 词在这条路径里根本排不上用场。这跟我们直觉的"权重越高越靠前"完全相反——事件永远是按显式顺序列出的,没有按匹配度重排这回事[how_filter_events]。

一句话记忆:要精确过滤,把想要的全挂在 + 后面;optional 词留给"纯想偷懒多带一带"的场景。

这个"坑"值得用一个具体推演钉死,免得看完就忘。假设事件列表长这样:DrawIndexed、DrawInstanced、CopyBuffer、Dispatch。你分别敲三个表达式,看命中集会怎么变:

表达式 命中 为什么会这样
Draw Copy DrawIndexed、DrawInstanced、CopyBuffer optional 任一命中即展示
+Draw Copy DrawIndexed、DrawInstanced 有 required 后,optional 全被忽略,只有含 Draw 的留下
+Draw -Instanced、+Draw Copy -Instanced DrawIndexed(两边完全一样) required/excluded 决定一切,optional 不参与

第三行最有迷惑性:+Draw Copy -Instanced 里那个 Copy 明明写在中间,看起来像是"要么 Draw 要么 Copy",结果 CopyBuffer 压根没出现。一旦你心里装着"optional 在 required 出现后等于不存在",就不会被写错还找不到原因。排查时只要发现"我明明加了词却没变多",先怀疑是不是踩了这个 required/optional 的坑。官方那句原话是给足体面的,但意思是绝对的:带 + 的词一出现,optional 那一组的全体就退场了[how_filter_events]。

6.4 过滤函数:$action() 与 $parent()

字符串匹配之外,还有过滤函数,能按属性做更精细的查询[how_filter_events]:

  • $action() 是默认过滤器本身——匹配所有 action(draw/dispatch/copy/clear…)。
  • $action(numIndices > 1000):只匹配"索引或顶点数超过 1000"的 action。这是"找大 draw"的利器,比如排查某个吞掉大量 verts 的对象。
  • $parent(render scene):匹配"以某个标记为父"的事件。官方给的经典用法是把 draw 收窄到场景内、剔除 off-screen pass:$action(numIndices > 1000) 且 $parent(render scene)——两个条件都用上,才能兼顾"数量大"和"只在渲染场景里"[how_filter_events]。

官方还提醒一个联动细节:任何不含(递归地)任何被包含子事件的标记区段,会被整体隐藏[how_filter_events]。也就是说过滤器作用于整棵树,空区段自动折叠,画面更干净。

这个"递归折叠"是把双刃剑,值得单独点一下:好处是命中之外全打码,一眼只剩你要的那十几条;代价是你可能误以为某些 pass 不存在。举一反例:你想找"为什么阴影 pass 这么贵",敲了 $parent(Shadow Depth),结果列表里啥都没有——别急着下"阴影没画"的结论,先看这条过滤是不是把 Shadow Depth 的子 draw 全端掉了。真正要反向定位的画法,是把 $parent` 的实参换成不带过滤条件、或配合 `$parent(...) 去找该类下的所有 draw,再倒过来看它挂在哪个 marker 下。过滤器回答的是"现在剩下什么",而不是"原本有什么"——这个边界不清,很容易把我们带偏。

下面两张图取自官方的过滤效果文档,正好对应这两个函数的实际样子。

numIndices 字段过滤:只显示索引或顶点数超 1000 的 draw(官方索引,MIT)
图 4-13$action(numIndices>1000) 过滤:只显示索引或顶点数超 1000 的 draw@ RenderDoc(MIT)
parent 标记过滤:只显示位于指定标记区段下的 draw(官方索引,MIT)
图 4-14$parent(render scene) 过滤:只显示位于指定标记区段下的 draw@ RenderDoc(MIT)

6.5 一个真实案例:把 bind 找出来

官方给出了完整案例:想在 action 之外也看绑定调用,就加 Bind 作为 optional 词,出现 pipeline 和 vertex/index buffer 绑定;只想看 buffer 绑定,把 Bind 改成 (+Bind +Buffer);再想让 marker 的 set 噪声消失,就按 action flags 排除掉 set marker[how_filter_events]。组合之下,"只留下我们想要的事件"这件事是完全可以调出来的——这也是过滤器设置窗口那个"保存/加载过滤器"按钮存在的意义。

把这个案例拆成一张三层推进的表,操作就非常具体了:

你想看 过滤器怎么敲 会看到
所有绑定调用 Bind(optional) 各种 pipeline / vertex / index buffer 绑定
只看 buffer 绑定 (+Bind +Buffer) 以 Buffer 结尾的那几类绑定,pipeline 绑定被挤掉
排除 marker 的 set 噪声 在上一条基础上按 action flags 排除 set marker 干净到只剩真正关心的绑定

最后一行的"set marker"指的是那类只用来"设当前标记"、本身不产生绘制的调用。它们和绑定调用混在一起会稀释你的注意力,官方特意给了"按 action flags 排除"这个手段,就是要让你看清:过滤器的能力边界,足够把"噪音"和"信号"在树上切开。这也是"保存/加载过滤器"按钮存在的意义——你调好一套"排障配方"(比如"只要场景内大 draw 的绑定"),下次打开同一工程或同类问题直接复用,不用重新敲一遍[how_filter_events]。

七、Timeline Bar:站在整帧上方鸟瞰

7.1 时间轴上的另一张地图

除了树状列表,还有一个 时间线栏(Timeline Bar),把帧在水平时间轴上展开,标记之间上下可展开,是"整帧鸟瞰"[timeline_bar]。它有两个明显用途:快速跳转到帧内差异很大的位置;以及(下文 7.2)展示资源依赖。

当前选中事件在时间线上显示为一个绿色 pip,配一条浅灰竖线指示当前位置——即使所在区段没展开也能看到;左键点击时间线上某一点就直接跳到该处事件[timeline_bar]。滚轮缩放、横向滚动条移动,和树视图互补。

把 Timeline 缩到最顶,你得到的是整帧的"骨相":哪些位置 draw 密集、哪些位置一马平川、瓶颈大概卡在哪一段——都不用先展开树就能有个概貌。它的本地折叠也很有用:marker 区段在时间线上可以上下展开/收起,意味着你可以把"Scene Render 里到底有几根 draw 的长条"单独抖开,数一数这一 pass 的 draw 密度,比在树里一格一格点高效。下面这张图就是 Timeline 的典型样子,右边堆叠的一个个色块,正是被 marker 圈起来的若干区段,越靠上的区段包含的 duration 越长。

Timeline Bar 鸟瞰:整帧在水平时间轴上展开,当前事件亮绿 pip(官方索引,MIT)
图 4-15Timeline Bar:整帧在水平时间轴上展开,当前事件为绿色 pip@ RenderDoc(MIT)

7.2 资源访问可视化:一张图看出一帧里纹理被谁用过

Timeline 另一个被低估的能力是资源访问可视化:只要纹理查看器或资源检查器里有选中资源,Timeline 就会把该资源在整帧中的使用摊铺上来。官方描述是:对当前纹理/资源,"usage 展示整个帧的读/写",读会画成小三角形;"不用费力一格格搜,就能直接看一个纹理在帧里被谁用、用到哪"[timeline_bar]。

这对渲染目标尤其有用——一个 RT 被 clear 了、又被写入、又被读取,DAG 般的依赖关系一眼可见,而不用去翻树里每一处引用。

把读法再拆细一点,这张"条带图"看着密集但信息很有序:开头通常是一段"清除"(把它画成第一条写),随后是若干"写入它的 draw"(读/写三角各自对应它们的 EID),再往后是"读它的后处理"。你要做的三件事,照顺序来:

  1. 找它的第一笔写——是谁把这张 RT 从"脏"变成"有内容",这是依赖链的起点;
  2. 找它的最后一次写——那是它的"成品态"被谁敲定,后面再读的都消费这一版;
  3. 数读它的区间——如果一段明显的读密集区没有任何写穿插,却能正常显示,就说明资源在复用/缓存,读的是上一帧或更早的内容。

对资源竞争/重复使用类问题,这张图几乎是直给的:一旦你看到同一资源在帧内被读写的区间重叠,或写入方和读取方相隔很远,就值得怀疑是不是出现了跨 pass 的别名。这类"谁在什么时候碰了这张图"的诉求,正是 Timeline 相对树视图的最大优势——树按提交顺序排,Timeline 按依赖关系摊,二者互补才把一帧看全[timeline_bar]。

资源访问可视化:对选中纹理画读(三角)写(条)标记(官方索引,MIT)
图 4-16资源访问可视化:对选中纹理画读(三角)与写(条)标记@ RenderDoc(MIT)

7.3 Pixel History 联动:回到具体 draw 的"红绿审判"

当 像素历史(Pixel History) 窗口聚焦时,Timeline 会在每个 EID 上显示该像素历史的判定结果:红色三角 = 被拒绝的像素,绿色三角 = 通过的像素[timeline_bar]。这是从"一个像素的视角"回溯到"具体哪些 draw 触碰了它"的通道——和 4.2 的逐 draw 隔离验证正好一正一反:向前看是"这条 draw 画了啥",向后看是"这个像素被哪些 draw 画过"。

社区指南里有对应操作:对着某像素右键 → Pixel History,RenderDoc 会列出触碰该像素的每个 draw call[yoyonel-guide]。

完整走一遍这个"从像素回溯到 draw"的动作,分四步:先在纹理查看器里对某个可疑颜色区域右键,选 Pixel History;RenderDoc 随即列出触碰该像素的所有 draw,并按"谁先谁后"排好;每一行都会注明它是被遮挡、还是通过、还是被丢弃,以及分别是 depth / stencil / alpha 里的哪一种原因;最后把这些行的 EID 和在事件浏览器里对上的 EID 一对照,你就能把"这个像素为什么是这个颜色"翻译成"这条 draw 在这里把像素写成了这样"。这一连串动作里,真正让"像素→draw→EID"能闭环的,正是 EID 这个贯穿始终的稳定句柄——Pixel History 里每一笔登记的 EID,就是事件浏览器里你可以直达的那一行。

进到 Pixel History 明细之后,"红/绿"之外还要多读一层:被拒绝(rejected)有很多种死法。同样是红色三角,一条是 depth 测试没通过(被前面更近的像素挡了),一条是 stencil 没放行,一条是 alpha 混合后没贡献,背后的语义完全不同。你要判断"这个像素为什么不是预期的颜色",得先分清到底是"这条 draw 根本没画上去",还是"画了但被后来的覆盖了"。前者查 depth/stencil,后者查顺序。而 Timeline 上红绿三角按 EID 铺开,正好让你一眼看出"从哪条 draw 开始这个像素定了生死"——通常就是红色区间第一次变成绿色的那个转折点[timeline_bar]。

Pixel History 聚焦时 Timeline 按 EID 画红(拒)/绿(过)三角(官方索引,MIT)
图 4-17Pixel History 聚焦时 Timeline 按 EID 画红(拒)/绿(过)三角@ RenderDoc(MIT)

八、时长列与性能视角:从"画了什么"到"哪个最贵"

8.1 一点 timer,逐 action 的 GPU 时长就出来了

默认事件浏览器没有时长列;当你点 timer 按钮,RenderDoc 会跑一遍"给每个 action 计时"的过程,自动加上 duration(时长)列,随后即可按该列排序,一眼看到哪些 draw/dispatch 最贵[event_browser]。时间单位可以在设置里切换。

这正是本系列"性能剖析"题眼与事件浏览器交汇的地方:在 5,000 条命令里,性能问题往往不是"某一条画错了",而是"某一条画太久了"。计时列让"最贵"从感叹变成可排序的数字。

这一列数字是怎么来的,值得多想一层。RenderDoc 的计时不是拿 CPU 秒表去近似,而是在提交给 GPU 的命令里插桩(instrument)一段"为每个 action 记录开始与结束时间戳"的机制,回放时把这几对时间戳相减,得到每个 draw/dispatch 在 GPU 上真正花掉的微秒数。理解了"它是回放时算出来的",两个性质就顺理成章了:其一,这是帧内相对成本,不是绝对帧预算,因为回放机器的驱动、降频、后台负载都会影响绝对值,你应关注"哪条命令占了这个 draw 的大头",而非把单条数字当标尺;其二,计时本身有开销,开启后 debug 层下每组 marker 都多记录一次,所以遇到"开了计时现象变了"时,要意识到是桩子让它变慢,而不是命令本身更慢。把这两条记在心里,才不至于把一张相对成本表误读成 CPU 侧的负载真相[event_browser]。

另一个常被踩的认知坑:贵 ≠ 一定是 GPU 光栅化慢。一次 draw 的 duration 里,可能大头是顶点爬取(vertex fetch)、可能是着色器的 ALU、也可能是它喂出去的带宽让后面整个 RT 都被动变贵。所以拿到"这条 draw 3.2ms"后,别急着改这条命令,先回到 4.2 的隔离再验一次——把这条 draw 单独回放,看它是不是真的单独就慢;如果是和其他命令拼在一起才慢,那瓶颈往往在"它产生了多大、多脏的中间资源",而不在这条命令自己的计算量。时长列给的是候选,隔离回放给的是证词,两者配对才是这条"性能线"的完整读法。

注:duration 列是点击 timer 按钮后才动态生成的运行时数据,官方仓库没有针对性的静态示意截图。本文以官方行为声明 + 文字描述说明其用法,配官方 timer 按钮图标示意;你也可以在自己机器上打开任意 .rdc,点计时按钮亲手复现这一列。

8.2 装上"时间"的读法:看 pass 而不是逐条扫

kokkugames 的 UE 优化文章给了一个很好的性能读法:把事件浏览器当作帧的"脚本",20ms 的帧逐条看哪个命令最长;重点盯 "Time" 列,看某个 pass(Base Pass / Shadow Depth)时长是否突然跳升,那就是排查切入点[kokkugames]。反过来,配合 6.4 的 $action(numIndices>1000),你可以先把"大" draw 筛出来,再上计时列看"大"和"贵"是不是同一个。

把"大"和"贵"分开看,其实藏着性能分析最朴素的一句判断:大 draw(顶点多)不等于贵 draw(时间长)。一个 20 万顶点的静态地形,GPU 一次光栅化可能只花几十微秒;而一个顶点不多、但在 Fullscreen 上反复跑的后处理,反而可能吃掉大头。所以正确姿势是"双列对读":

  1. 点 timer 生成时长列,先按时长排序,抓出 TOP N 的贵命令;
  2. 再用 $action(numIndices X) 按规模筛,看这些大 draw 的时长在不在 TOP 里;
  3. 两者交集往往才值得优化,只大不贵说明 GPU 带宽没被它吃掉,只贵不大说明问题在光栅/ps 开销而不在顶点量。

时长单位的切换别看漏了:Settings 里能切微秒/毫秒,习惯用 ms 做预算时切过去,读数才直观[event_browser]。另外预告一句,本系列后面讲 Perfetto 时会再碰"时长"这个词——但那边的粒度是 CPU/GPU 帧与卡顿的宏观时钟,跟这里的逐 draw 微观计时正好一宏观一微观,配着看才能把"这帧卡在哪"钉死。

九、书签、查找与跳转:给帧里的关键位置贴上锚

9.1 书签:Ctrl-B 加入,Ctrl-1…0 跳转

一份复杂的捕获里,你往往要反复回到若干关键 draw。事件浏览器提供书签:Ctrl-B 把当前事件加入书签,书签会以 EID 列表出现在窗口顶部工具栏,Ctrl-1 到 Ctrl-0 可跳到对应书签,且这些快捷键在应用任何位置都有效[event_browser]。等于给帧内的关键位置贴上了全局锚点。

书签和过滤器组合起来,能沉淀出一套很实用的"嫌疑集合"工作流:先用外部指标(时长列、pixel history)定位十几个可疑 draw,把它们逐条 Ctrl-B 收进书签栏;之后无论你在纹理查看器、Mesh Viewer 还是 Timeline 里,都能靠 Ctrl-1…0 在嫌疑点之间瞬移,反复来回对照,不必每次重新翻树。这个"把帧里的重点钉成一条书签清单"的动作,和 9.3 的复制连成一条链路后,就是一份可交给同事/未来的自己复现的调试笔记骨架。

还有一个容易被忽略的通配:书签是"跟随 EID 走"的,不是"记住那一时刻在屏幕上长什么样"。这意味着你切走了过滤器、折叠了区段、甚至把 action 显示档位从全参数切回默认,书签仍稳稳指向同一个事件;反过来,如果原本那个位置的 action 因为过滤条件变化而在列表里消失了,跳转也依然有效。这个"锚在 EID 上"的性质,和第 3 章讲的 EID 稳定性是一起出现的——正因为 EID 是回放引擎内稳定的地址,书签才能跨视图、跨栏位地反复直达。也正因如此,书签里那一串数字本身值得留意:Ctrl-B 之后你完全可以把书签条的 EID 顺手记进笔记,那串数字在下一份捕获、下一个问题里,很可能就是线索的开头[event_browser]。

9.2 查找:Ctrl-F,还能搜 EID

Ctrl-F 打开 find 工具,可输入部分文本,同时匹配 label 与 action 事件,回车即搜[event_browser]。更有用的是与 EID 结合:若想直达某事件,可以直接搜它的 EID——查找结果只显示匹配事件,不会因子串撞号把多余的行带进来[event_browser]。未展开区段会自动展开到命中事件可见;反复回车可在多个命中之间跳转,左右箭头在同批结果里前后切换。

书签条与跳转:Ctrl-B 加书签,Ctrl+数字直达(官方索引,MIT)
图 4-18书签条与跳转:Ctrl-B 加书签,Ctrl+数字直达@ RenderDoc(MIT)

9.3 复制:Ctrl-C 带走一整行

事件浏览器支持复制:选中条目后 Ctrl-C 拷入剪贴板,方便把某个 draw 的 EID 与参数名贴进笔记或 issue[event_browser]。

为什么这事值得专门占一格?因为别人(或几天后的你)拿到这条记录,能不能无障碍复现,取决于你存的是什么。Ctrl-C 带走的是"该事件此时的 label + 关键参数",既包含 EID 这个稳定句柄,也包含名字。写 issue 时建议的格式是:事件浏览器里 Ctrl-C 一段 + 当前选中的资源名 + .rdc 里能定位它的上下文(比如所在 marker 名),三者缺一不可。光报"某帧颜色不对",对方要自己翻几千条 draw;报"EID 9120 的 DrawIndexedInstanced(6,16) 画进 RT A 时颜色异常",对方就能直奔现场。调试记录的价值,一半在"当时查到了什么",另一半在"别人能否凭它复现"——而后者几乎全靠 EID + 名字 + 资源这三样撑起来。

十、结语:把"按钮清单"升级成"帧的结构化索引 + 逐 action 验证台"

走到这里,事件浏览器的全貌应该清楚了。它不只是"draw-call 列表"这么简单:

  • 它是一份按时间次序、由 marker 织成树的函数调用清单(action 而非一律 draw);
  • EID 是它的锚点,只要知道计数口径,就能理解为何不连续、如何搜数字直达;
  • 点选 = 把回放状态推进到某命令结束瞬时,配逐 draw 隔离成为"这到底画了什么"的验证台;
  • 三档显示开关 + 过滤表达式,让它从几千条命令里筛出你关心的十几条;
  • Timeline 与资源/Pixel History 联动,给资源依赖、像素归宿画了 DAG 般的图;
  • 计时列让"哪个最贵"可排序。

一句话收束:事件浏览器 = 帧的结构化索引(怎么读)+ 逐 action 验证台(怎么验)。读,靠树、靠 EID、靠过滤;验,靠点选设状态、靠逐 draw 隔离。这块"前台"看懂了,下一站就是它背后真正存放资源的地方——Resource / Descriptor 检查(ep05),去看看一条 draw 到底喂给了 GPU 哪些纹理和缓冲。


事件浏览器读法总览:树、EID、过滤、Timeline、时长列共同构成帧的地图(本系列绘制)
图 4-19事件浏览器读法总览:树、EID、过滤、Timeline、时长列构成帧的地图@ 本系列绘制

相关链接 - 上一篇:【性能剖析工具链】(三)一帧被抓下来之后:.rdc 如何被回放引擎重放与分析 https://yyy3d.online/blog/graphics-profiling-ep03-replay/ - 下一篇(预告):【性能剖析工具链】(五)Resource / Descriptor 检查 - RenderDoc 官方文档:https://renderdoc.org/docs/ - RenderDoc 源码:https://github.com/baldurk/renderdoc

With Best Wishes.