CSDN 版文章链接: {csdn-url} 知乎专栏版文章链接:{zhihu-url}
一句话点题:事件列表讲了"画了什么",这里讲"用什么画的"
前面 ep04 我们把事件浏览器读成了一帧 draw-call 的结构化索引:哪条 action、哪个 marker、EID 为什么不连续、过滤表达式怎么筛、timeline 怎么鸟瞰,你都拿下了。但你有没有想过一个问题:事件流里那一行 DrawIndexed 当真是凭空画出来的吗? 不是的,任何 draw 都要喂数据,这些数据从哪来?答:从 GPU 资源里来,再通过描述符(descriptor)绑定到这个 draw 的图形管线上。
如果用破案来打比方,ep04 的事件浏览器给了你"作案时间线"(哪一个时刻画了什么),这一篇补上"作案工具清单"(用了哪些 buffer、哪些纹理,怎么绑上的)。官方对这两个窗口的定性很直白:Resource Inspector 列出捕获里全部的 API 对象,并显示对象间的父子关系与创建细节[res_inspector];而每个 draw 绑定资源的全貌,集中在 Pipeline State(管线状态)窗口[pipeline_state]。这一篇,就是把这两个窗口讲透,并且回答那个最该问的问题:资源、绑定、描述符,它们和命令流是怎么互相反查的? 最后用一部"双闭环"收住,让你既会从 draw 看资源,也会从资源反查 draw。
我们照旧先把边界划清楚,免得期望落空。本文不涉及五块:一是 Mesh Viewer(顶点/实例/图元查看)与 shader 反汇编,那属本系列下一篇的完整主题;二是抓帧与回放机制本身,那是 ep02/ep03 的范畴;三是渲染算法本身的数据流细节(某个具体光照技术怎么用纹理做计算),那是渲染管线系列的内容;四是 Perfetto 那套系统级内存/带宽跟踪,本系列不涉及它,等系统剖析时再讲;五是 RenderDoc 的 API Inspector 内部逐字段翻译,它和资源绑定不同,另文再说。本文只回答一件事:拿到一份单帧,如何在 RenderDoc 里把"资源与描述符绑定"读成"可验证的命令流另一半"。范围卡在这,才不会又写成一份按钮说明书。
OK,言归正传,先从"资源到底是什么"说起。
一、资源清单:一份 .rdc 里到底有哪些"物件"
1.1 Resource Inspector:全部 API 对象的一张总表
打开任何一个捕获,你通常会先注意到左边那个写满条目的窗口,那就是 Resource Inspector(资源检视窗)。官方说法很简洁:"允许你看到捕获帧里包含的全部 API 对象,看到对象之间的关系(比如父子链接),以及任何对象的完整创建细节"[res_inspector]。
这一句话背后有三层意思,值得拆开讲。
其一,"全部 API 对象"不是我们常说的"纹理加缓冲"那么简单。它包含纹理(texture)、缓冲(buffer)、shader,以及 Vulkan 特有的各种对象(device、queue、command buffer 等等)。凡是捕获里被创建的、被引用的 GPU 对象,几乎都会在这里挂一份。它不是一份简单的资源列表,而是一份捕获视角的类型清单。
其二,对象间有父子关系。比如一个"渲染到纹理"的目标,可能由颜色纹理加深度纹理组成,这两张纹理又共享同一个 render pass;这些关系在资源面板里是层层可展开的树。看懂这棵树,你就知道"谁是谁的组成部分",对理清一大堆纹理里谁在服务谁特别有用。
其三,每个对象都记着"创建时的初始化调用"。官方文档明确:任何对象在被使用前,都必须按捕获时的状态初始化,这些初始化函数(至少一个创建函数)会列在详情里[res_inspector]。这句话对调试至关重要。当你怀疑"这个 buffer 到底装的是什么格式的数据",答案不在某个神秘状态里,而在它被 vkCreateBuffer 或 CreateTexture2D 创建时的那一串参数里。创建参数就是资源的第一手档案。
所以我的建议是:Resource Inspector 不要当"列表"看,要当索引看。它是整个捕获的资源总账本,其他所有窗口(Texture Viewer、Buffer Viewer、Pipeline State)都从这里像查表一样跳转出去。你在大脑里建立的心智,应该是"资源面板是目录,具体窗口是正文页"。
为了把"总账本 + 父子树"的形态讲清楚,我画了一张示意:它展示资源清单里,一张"渲染到纹理"的 RT 目标如何作为"父",把颜色纹理、深度纹理、颜色查看器这些"子"资源收在树下。
这种树的实用场景很多。比如你看到一个场景有几千张纹理,第一反应不该是挨个翻,而是先找父子关系:哪几张属于同一个 render target、哪些是同一份数据的不同 view。父子树直接把"哪些资源是一个整体的零件"摆在你面前,省去大量无头苍蝇式的翻找。
1.2 Usage in Frame:每个资源在帧里的使用区间
资源经常被当成静态东西:一块分配好的显存呗,摆在那儿不动。但 RenderDoc 给了每个资源一个动态维度,官方叫 Usage in Frame。文档说:像纹理和缓冲这类资源,有"它们在哪里、如何被使用"的信息;而且在列表里,每个条目都记录了一个 EID 区间和一种 usage 类型[res_inspector]。
这一点值得停下想想。EID 我们在 ep04 讲过,是按命令顺序递增的事件编号。现在 Resource Inspector 给每个资源一个 EID 区间,意思就是:这块纹理这一帧里,是从第几号命令开始被用、用到第几号命令结束。顺着这个区间,你就能回到事件浏览器,定位"这个资源到底被哪些 draw 使用过"。这就是资源反查命令流的第一个、也是最直接的入口。
再配合第 6 章要讲的 Timeline usage bar,资源就不只是一块静止的内存,而是一个"在一帧里有自己的生命轨迹"的活对象。读懂这条轨迹,很多"它怎么变成这样"的问题会迎刃而解。
这里我要特别讲一个常见误区,帮你区分"资源的创建属性"和"资源的使用轨迹"这两件不同的事:创建属性(creation attributes)是它"生来是什么",使用轨迹(usage)是它"这一帧里被折腾成什么"。打个比方,创建属性像一个人的出生证明(性别、血型这些不变项),使用轨迹像他一整天的行程(几点在哪干了啥)。定位问题时,先分清你问的是"它该长什么样"(看创建),还是"它被谁改过"(看使用区间),才不会查错窗口。Resource Inspector 恰好把这两样都给了你:创建参数在对象详情里,使用区间就在列表的 usage 栏里[res_inspector]。
把"创建属性"和"使用轨迹"分开以后,还差最后一步:怎么把这份"轨迹"真正用起来。我给的答案是顺着 usage 栏的 EID 区间回查命令流。比如你看到 ShadowMap 这个资源在 usage 栏写着 EID 340 → 420,你就知道这一帧里碰过它的命令都落在 340 到 420 之间;回到事件浏览器,把 timeline 框到这一区间,就能精确定位哪条 draw 在写它、哪条在读它。这一步把"资源"和"命令流"第一次真正接上,也正是后面第 6 章"反向闭环"的种子。下面这张截图就是 Resource Inspector 里资源 usage 的呈现形态:紧跟在资源名下,记录它本帧被哪一段命令用到。
需要再强调一次:这段区间是"这一帧里"的记录,不是创建属性的固化值。一个在帧首被清除、帧尾被当下一次的 RT(常见于各种 ping-pong 双缓冲),它的 usage 栏会标出它两次被用的区间;你要问"它被画过几次",读 usage 的 EID 段数就能数出来,而不必靠肉眼在事件列表里点数。
1.3 重命名:把可读性还给自己
工具默认的资源名往往是 Texture 0x1f40 这种内存地址,少的时候还能硬记,一多起来彻底没法认。RenderDoc 提供 Rename Resource 按钮:点它,输入一个有语义的名,按下回车,这个名字会在整个 UI 里所有被引用到的地方保持更新、全局一致[res_inspector]。官方文档这一步讲得很细:回车确认、Escape 取消、Reset name 还原原名。
这一招在实战里价值极高。一份复杂场景的 .rdc 可能有几百个对象,靠地址认脸会疯掉。给关键资源命名,等于给这幅"工具清单"标上可读标签;之后你讲"这一帧卡在阴影贴图那步",指的是一张叫 ShadowMap 的图,而不是 Texture 0x29a0。我甚至建议把命名当成 Debug 前的一个固定动作,和给变量起名一样重要。
命名还有个容易被忽略的附带收益:让跨资源反查变得可说。当你在 Pipeline State 里看到一个绑定叫 SceneDepth,在 Timeline usage 里再看到同一名字,你就能顺着名字在多个窗口之间顺畅跳转,而不必反复核对地址辨认同不同一块。名字一旦统一,正反向闭环每一步之间的"衔接语言"就有了,后面第 6 章讲的双闭环才会跑得顺。所以我把重命名单独当成一门手艺,而不是顺手起个名就完事。
二、点一个 draw,它到底绑了哪些资源
2.1 Pipeline State:整条图形管的"接线总板"
资源有了,也在清单里了,可它们是怎么"喂"给某个具体 draw 的?中间的桥就是 Pipeline State(管线状态)窗口。官方定位:它显示图形管线的全部有状态设置,包括绑定的资源、光栅化设置等[pipeline_state]。
窗口顶部是一条管线流程图,分块显示 Input Assembler → Vertex Shader → Rasterizer → Pixel Shader → Output Merger 这些阶段,每一块是一个独立页面,装着这一阶段相关的状态和绑定。当前选中的块用红框标出,浅灰的部分表示这一阶段正在参与(active),深灰表示未启用、可当直通。官方文档原话是"每个块都是一个独立页面,内容随 API 和数据类型不同"[pipeline_state]。
这是一张值得刻进脑子里的"总接线图",因为它回答的是"数据从哪进、往哪去"。想查"这个 draw 的顶点数据从哪来",就进 Input Assembler 页看 vertex buffers;想查"颜色从哪来",进 Pixel Shader 页看纹理绑定;想查"写到哪里去",看 Output Merger 的颜色和深度输出。六个字总结这一章:按阶段找绑定。
顺着这个"按阶段找"的思路,我把几个高频调试场景和对应的页面列成一张对照表,遇到问题就直接查:
| 你想排查的问题 | 进哪个 block | 看什么 |
|---|---|---|
| 顶点数据 / 索引来自哪 | Input Assembler | vertex buffers、index buffer 绑定 |
| VS 阶段读了什么常量/纹理 | Vertex Shader | 常量 buffer、纹理槽 |
| 像素颜色怎么来的 | Pixel Shader | 纹理绑定、采样器 |
| 结果写到哪张 RT | Output Merger | 颜色/深度目标 |
| 有没有用 compute 阶段 | Dispatch / CS | compute 绑定、storage buffer |
| 光栅化规则(剔除/裁剪/深度偏置) | Rasterizer / Output Merger | 光栅化设置 |
这张表值得贴在手边,它把"管线状态页是接线总板"从一句口号,变成可执行的查表动作。遇到"颜色不对",你第一反应该是"去 PS 页看看纹理绑对没",而不是满屏扫。
管线状态页的每个 block 还能折叠:点标题行把它折成一条,只留块名,省下垂直空间,让不相关的阶段让位。官方文档明确说块可以折叠[pipeline_state],调试长管线时把暂时不看的块全折起来,只留你正在查的那一段,画面立刻清爽不少。
2.2 shader reflection:只给你看真正用到的槽位
管线状态页给出绑定,但"给出绑定"和"给出所有可能绑定"是两码事。RenderDoc 在这里做了一个很聪明的默认过滤:用 shader reflection 数据,只显示 shader 实际用到的槽位,把没用到的一律隐藏。官方原话是它"试图只显示相关的,而不显示所有可能的状态数据"[pipeline_state]。
这句话的调试含义很深。一个绑定了一大堆资源的复杂管线,你的 shader 可能只用其中一两个槽,RenderDoc 就只给你看那一两个。好处是不会被无关绑定神烦;坏处是偶尔你想看全量,得手动点开 Show Unused Items 按钮。记住这个默认值的取舍,你就不会疑惑"为什么少了几张图"。
它背后的机制值得画出来看一眼:左侧是"真正被绑定到管线的一串资源槽",右侧是"shader 源码里实际声明并读取的槽位",中间有一条 filter 层,把"绑了但 shader 不读"的槽过滤掉,只把交集端给用户。用了一张图说明这件事。
理解了这条过滤,你在排障时就不容易误判。看到绑定页"很干净"时,要先想"这是 shader 真没用,还是被 reflection 藏起来了"。排查"我明明绑了,为什么页面里没有",就要去点 Show Unused Items,而不是怀疑绑定丢了。
下面这张官方截图就是过滤后绑定页的真实样子:页面上通常只剩 shader 真要用到的几个槽,右上角能展开 Show Unused Items 拉出全量。看到这种"就这几个槽"的页面,先别急着下结论说管线简单,先确认是不是 reflection 帮你把没用到的藏了。
2.3 bindless 时代的处理:dynamic feedback
现在 GPU 编程越来越爱 bindless:一个巨大的描述符数组里绑了几百上千个资源,但一个 draw 真正访问的只有几个。RenderDoc 对这种场景的处理,官方文档专门写了一句:在 Vulkan 等 API 上,它用 dynamic feedback(动态反馈) 试图判断实际访问到的资源集,并且只显示这一点;同样可用 Show Unused Items 覆盖[pipeline_state]。
这里藏着一个容易漏的点,务必记住:dynamic feedback 是"试图判断",是推断而不是事实。也就是说,它显示的资源集是 RenderDoc 根据反馈猜出来的,某些间接访问(比如从 buffer 里读出来的描述符索引)可能判断不全。遇到 bindless 内容,如果觉得资源集对不上,要有这个意识:不是工具坏了,是它的推断边界就在那儿。必要时对着 shader 源码确认。这一层属于本系列"工具机制深挖"的核心,讲透它,你就能区分"工具显示的"和"客观存在的"。
为了讲清 bindless 下的不同,我画了一张对比:左边是传统逐槽绑定(每个槽绑一个资源,页面上直接可见),右边是 bindless(一个大数组绑了几百个,只有当前 draw 真正访问的几个被推断显示,用虚线标出"推断区域")。
2.4 时序:始终是 action 执行之后的管线状态
还有一条最容易弄反的时序:Pipeline State 显示的始终是 action 执行之后的状态。官方文档原话是"pipeline state viewer 总是显示 action 执行后的管线状态,和 RenderDoc 里其他 viewer 一样"[pipeline_state]。
这条和 ep04 讲的"选中事件等于把回放状态设到那条命令执行完的微秒之后"完全一致。你点中第 100 号 draw,看到的绑定,是第 100 号 draw 画完之后这条管线上还挂着的东西。对多数调试场景("这个 draw 用了什么"),这是对的心智;但你要知道它存在这个拍板时点,别拿"执行前"的预期去套。
它的微妙之处在于:绑定在你看到的这一刻,可能不是"这个 draw 用到的",而是"它用完还残留的"。这两者绝大多数时候相同,但也有反例。典型反例是"读后即改"的资源:一个 UAV 在第 100 号 draw 里既被当输入读,又在同一 draw 末尾被写入新值,你看到的是写完之后的新内容;若你的目标是想看"它读进去时"的旧内容,就要退一步,把 timeline 调到这个 draw 之前的某个点。理解了这个时点,你才不会对着"执行后"的纹理去反推"执行前"应该是什么。
三、从绑定反查内容:纹理与 buffer 的真身
绑定是"接线",接线那头接的才是真东西。这一章把两个最常用的看真身窗口讲清楚。
3.1 Texture Viewer:看纹理到底长什么样
官方说 Texture Viewer"很可能是 RenderDoc 里最直观、也最常被用的窗口",它显示任何纹理在给定事件当前状态下的内容,并提供各种查看与检视控件[texture_viewer]。
它由几部分组成,我按看图的顺序讲:
- 主显示区:一个大 tab 控件,第一个 tab 总在,显示当前管线状态上某个纹理槽,比如输入纹理,或颜色/深度输出。其他锁定的 tab(Locked Tab)可以钉住单个资源,不受状态变化影响[texture_viewer]。
- 缩略图条(Thumbnail strip):列出当前绑定或使用的输出目标和着色器输入,红色轮廓标出当前选中项。
- 查看控制:滚轮缩放、左键拖动平移、右键悬停或按住拾取像素值,状态栏实时显示。
"在给定事件当前状态"怎么理解?还是接上一章那条时序:你选中第 100 号 draw,看到的纹理,是该 draw 执行后、这个纹理内容的状态。同一个纹理,在 100 号前后不同的 draw 处点选,看到的内容可能不同,因为前面的 action 可能改过它。这恰恰是查"这个纹理为什么长这样"的正确姿势:选中"改它的那一步"时再看它。
Texture Viewer 的查看能力还不止"看图",它覆盖了几种排障刚需,我顺次讲:
- 通道与 mip 查看:它可以分开看 R/G/B/A 各个通道,也可以切换到不同 mip 层级。这对排查"某个通道是不是被写坏了""低 mip 是不是糊了"特别有用。
- slice / array layer 查看:对立方体贴图、纹理数组,它可以在 array 的某一层之间切换。常用于查"我读的到底是 6 面里的哪一面"。
- 放大镜拾取:滚轮放大到像素级,右键拾取,状态栏直接显示该像素的各通道值和坐标。这是查"某个具体像素为什么是这个颜色"的最直接手段。
把这几种查看能力画成一张图,Texture Viewer 的"弹药库"就很清楚了。
其中"放大拾取"值得单独再讲,因为它是"这个像素为什么长这样"的最直接答案:把视图滚到像素级,右键点住目标像素,状态栏会实时给出它的坐标和各通道数值。下面这张官方截图展示的正是拾取时状态栏的反馈。配合前面的通道拆分,你先拾取得出 RGBA,再切到单通道重看一遍,几乎能一步定位"是不是某一通道被单独改坏了",从而把"颜色看着不对"收敛成"蓝色通道写成了 0"这样的具体结论。
拾取之外,悬停本身就能读出一堆元信息:鼠标悬停在纹理上,状态栏会浮出一条 tooltip,把资源名、分辨率、mip 数、像素格式、光标坐标、缩放级别一次报全。下面这张截图展示的正是悬停时的 tooltip 形态。先悬停扫一眼格式,再决定要不要放大拾取,往往能省一步"切窗口查格式"的动作。
需要提醒一点:渲染目标这类纹理默认可能带后缀(如颜色/深度),缩略图条用红色轮廓区分当前选中。如果你在缩略图条里看到的不是红色轮廓在想要的那张上,先点选对目标,再去分析它的内容,不然很容易盯着错误的那张图分析半天。
3.2 TexList:怎么打开指定那一个纹理
纹理多了以后,怎么精确打开你想要的那张?Texture Viewer 工具栏有个 TexList 图标,点开一个可过滤的纹理列表。官方文档教的方法是按字符串过滤,或按创建标志过滤[how_view_texture]。
实战价值在于:你想看的那张纹理,通常你也不知道它在哪个槽位,但你至少知道它大概该叫什么、或是什么类型。用 TexList 按名字一筛,直接翻出它,不必在"当前管线绑定的那张"里碰运气。它本质上是给"纹理"这个维度也造了一个 Resource Inspector 式的过滤入口。
3.3 Locked Tab:钉住一张图,别跟着状态走
默认的第一个 tab 会随选中事件变化,你选到哪个 draw,它就切到那个槽位的纹理。这有时正是你想要的("跟着看当前绑定"),有时却烦人:你想盯着同一张图,看它在后面几个 draw 里怎么变化。这时就要用 Locked Tab(锁定标签)。
官方画的对比很清楚:跟随(default)tab 和锁定(locked)tab 是两回事,打开 Locked Tab 后它固定显示某一个资源,不再受后续状态变化的影响[how_view_texture]。
记一个心法:想看"当时绑的是谁"用 default tab,想看"这张图后面怎么变"用 Locked Tab。一个查静态绑定,一个查动态演化,用错场景就很容易看花眼。锁模页还有一个进阶用法:把它和事件浏览器一起用,边切 draw 边看同一张图怎么被逐步写入,这是排查"中间帧内容不对"的高效率组合。
怎么打开 Locked Tab?在缩略图条上对目标资源右键,菜单里就有 Open new Locked Tab,旁边还有 Open in Resource Inspector 直通资源清单。这张官方截图展示的就是右键菜单本身,顺带把该资源的 usage history 也列在菜单里:它被哪几段 EID 用过、分别当什么角色(Clear / FBO Color / PS-Texture)一屏看全。这个"右键一处、入口与历史全给"的设计,正好把"打开资源"和"了解它被谁用"两件事合并成一步。
3.4 Buffer Viewer:把 buffer 读成人话
如果说纹理是"图",那 buffer 是"数据",没法用眼睛直接看,得解析着看。Buffer Viewer 就是干这个的。官方说它"既可以显示固定的、不重复的 struct-of-arrays(SoA)数据,也可以显示重复的 array-of-structs(AoS)数据,或两者都显示"[buffer_viewer]。
两个容易踩的概念先掰清楚,这俩名字极像,但内存排布完全不同:
- AoS 是 array-of-structs:数组元素是结构体,例如
struct { float x, y, z; } positions[1000]。数组里每个元素连续放着 xyz 三个分量。相邻两个顶点,其 x 之间隔着 y 和 z。 - SoA 是 struct-of-arrays:结构体的每个成员是一整段连续数组,例如
struct { float xs[1000]; float ys[1000]; float zs[1000]; }。所有 x 挨在一起、所有 y 挨在一起。
RenderDoc 对这两者的默认判定很有规律:Constant Buffer 一律按 AoS 处理;而 UAV、storage buffer,或直接从 Resource Inspector 打开的 buffer,默认趋向 SoA[buffer_viewer]。
为什么必须理解 SoA/AoS?因为它们决定了"一个顶点或一个元素占几个字、隔多远是下一个"。一开始如果你不知道它是 AoS 还是 SoA,格式又没反射数据,显示就会是一堆对不上的乱码,根本分不清哪四个字节是 x、哪四个是 y。理解了布局,你对"这是顶点数组、实例数据、还是常量"立刻就有一个第一判断。
3.5 自定义 buffer 格式:reflection 不够,你来定义
RenderDoc 解析 buffer 的初始格式来自 shader reflection(shader 编译产出的元数据)。但 reflection 有它的边界:有些 buffer,尤其 storage 和 indirect buffer,shader 怎么读它,reflection 能猜个大概,但更精细的布局,比如 struct 内部字段的名字、类型、顺序,最好由你亲手定。官方给了一条路:用 C / HLSL 风格的 struct 声明来自定义显示布局,How do I specify a buffer format? 页面有完整写法[how_buffer_format]。
这条的价值在于"把数据读成语义"。CPU 侧往 buffer 里塞的是一堆字节,shader 侧按 struct 解读,RenderDoc 也按同样的 struct 解读才能对得上。你写出和游戏里一致的 struct,buffer 就会从"十六进制天书"变成"一行行有名字的字段"。这个习惯一旦养成,排查 CPU 塞入和 GPU 读出的错位,效率会是天差地别。
举一个看得见的例子,你就知道自定义格式长什么样了。你有一个 per-instance 的 buffer,每个实例是 struct { float4 color; uint meshId; uint _pad0; }。若 reflection 只辨出"每项 24 字节、没捞到字段名",你看到的仍是一堆十六进制,根本分不清哪四个字节是 color、哪四个是 meshId。这时你在 Buffer Viewer 的自定义格式里声明一遍同款 struct,RenderDoc 立刻把它布局成"一个实例一行,color 四个浮点、meshId 一个整数",字段名也一并标出来。从此 CPU 一侧往哪几个字段写了什么,一眼就能对上去,错位当场现形。
需要提醒的是:自定义 struct 的字段顺序、对齐都必须和游戏里实际写入 buffer 的排布完全一致,多写或少写一个 _pad0 都会让后续所有字段错位。这也正好呼应了 3.4 里 AoS/SoA 那层心法:格式说白了就是"这一串字节怎么切成有意义的字段",谁说了算由你在 UI 里拍板。排布对了,buffer 读人话;排布错了,越解析越乱。
四、术语对齐:D3D 的 SRV/UAV/CBV,和 Vulkan 的 Descriptor,是一条东西
到这里你可能会犯嘀咕:上面讲的窗口全是概念,可真到各种 API 下,资源绑定叫法五花八门。这一章把它捋直:DX 里的 view,和 Vulkan 里的 descriptor,在 RenderDoc 里是同一回事、用同一种窗口呈现。
4.1 D3D 侧:CBV / SRV / UAV
D3D 把"怎么解读一块资源"叫做 view(视图)。最常见的三种:
- CBV(Constant Buffer View):常量缓冲区视图,给 shader 只读的常量子集,资源本身是常量 buffer。
- SRV(Shader Resource View):着色器资源视图,shader 只读,能读 buffer,也能读纹理。
- UAV(Unordered Access View):无序访问视图,读写皆可,通常给 compute shader 或需要原地改的读改写。
科普向教程把三者讲得很清楚:SRV 只读、UAV 可读写、CBV 承载常量[cbv_srv_uav]。记住"只读 / 读写 / 常量"这个三字诀,D3D 侧就抓稳了。
4.2 Vulkan 侧:Descriptor 与描述符类型
Vulkan 不叫"view 绑定资源",它用 descriptor(描述符) 描述 shader 资源怎么被绑定。官方给出了对照关系[vk_mutable_desc]:
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、UNIFORM_BUFFER_DYNAMIC对应 CBV;VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE、UNIFORM_TEXEL_BUFFER对应 SRV;VK_DESCRIPTOR_TYPE_STORAGE_IMAGE、STORAGE_TEXEL_BUFFER、STORAGE_BUFFER对应 UAV。
Vulkan 官方引导文档也提示,把数据映射给 shader 时用哪类 descriptor,核心就是这套映射[vk_mapping]。
这里要补一个背景,帮你理解 Vulkan 捕获里"绑定槽"的排布:Vulkan 里这些 descriptor 不是零散摆放的,而是被组织进 descriptor set(描述符集),先由 descriptor pool 分配资源,再按 set layout 绑定到渲染管线。官方引导文档把"怎么把数据接进 shader"讲成一套标准流程[descriptor_sets],官方教程也演示了从创建 set layout 到更新 descriptor 的完整写法[vk_tuto_desc]。
对你看 RenderDoc 的意义在于:Vulkan 捕获里你看到的"绑定槽",本质是某个 descriptor set 里的某几个 binding。当页面里出现 set 0, binding 2 这样的标注,就能直接对应到源码里那句 layout(set=0, binding=2)。把 D3D 那条"view 直接挂在槽上"的心智,扩展成"descriptor 装在集合里再绑",跨 API 时就不会对不上位。而 D3D 侧对此也有权威定义:view 就是"一块资源如何被 shader 解读"的对外接口[d3d_views],D3D12 下则用 descriptor 表(descriptor table) 把一堆 view 打包挂进根签名[d3d12_desc][d3d12_desctab]。你看,两边底层其实是同一套"集合 + 槽位 + 引用"的组织,只是 API 的命名和入口不同。
我把这张映射做成一张自绘对照图。它的意义在于:无论你用的是 D3D 还是 Vulkan,打开 RenderDoc 的 Pipeline State,看到的是同一种"绑定槽 + 资源"的结构。RenderDoc 在底层把两边翻译成一套内部表示,你观察到的都是"某槽位上绑了某个资源,带某类权限"。
4.3 为什么理解这层映射对用 RenderDoc 调试重要
落到操作上,这层映射的作用是:别被术语吓到。新手看 Vulkan 捕获,满屏 VK_DESCRIPTOR_TYPE_*,可能觉得"这是另一个世界";但只要把它对应到你已经熟的 DX 那套 CBV/SRV/UAV 心智,立刻就能接上:常量进来走 uniform buffer,纹理读走 sampled image,读写后处理走 storage。术语是壳,绑定语义是核。这套映射也是理解"为什么同一个 draw,在 D3D 和 Vulkan 下的绑定页长得很像"的根本原因。
除了在 DX 和 Vulkan 之间切换时要用这套心智,同一天里你接触不同 log(一个 D3D11 项目、一个 Vulkan 引擎)也很常见。我的经验是:把"权限只读/读写 + 数据是常量/纹理/buffer"当成你的内部词汇,不要让 SRV、UAV、STORAGE_IMAGE 这些外壳混淆你。D3D 的 SRV 和 Vulkan 的 SAMPLED_IMAGE 在 RenderDoc 里描述的是同一种东西,你只要心里有自己的统一说法,跨项目时就永远不乱。
把"我看不懂某个 descriptor 是干嘛的"变成一套追问,也很好用,顺着问三步就能定位:
- 它是只读还是读写?(SRV/UAV 或 SAMPLED/STORAGE 一眼可辨)
- 它绑的是纹理还是 buffer?(看绑定条目指向的资源类型)
- 它被哪个阶段的 shader 怎么用?(进对应 shader 的绑定详情,看它在代码里怎么被采样/读写)
这套"三步追问"本质是把你从术语拉到语义:你先确认权限和数据性质,再落到使用处。它和前后文的窗口用法完全自洽,也是我推荐给你的一套可复用排障思维。
五、空绑定、禁用与特殊参数:那些"异常颜色"在说什么
Pipeline State 页里的条目不是白板一片,RenderDoc 用颜色表达状态,官方文档都交代了,值得逐条记下:
- 红底 = 空绑定:当绑定为空,条目以红色背景显示。而且只有在"默认只显示 shader 可能引用的绑定"前提下,红底才意味着"这个 shader 可能正在读一块没绑定的资源"[pipeline_state]。这是查"为什么画面黑了、某一处闪了"的高价值信号。
- 蓝底 / 绿底 = 特殊参数绑定:官方说,绑定以蓝或绿背景显示,说明它带了 underlying resource 之上的特殊参数。例如一张带很多 mip 或 slice 的图,其中不是全部被绑定进 image view;或这个 view 把图转成了另一种兼容格式[pipeline_state]。蓝底常见于"只绑了部分 mip",绿底常见于格式转换。
- 灰 / 斜体 = 未用或禁用:不可用、未启用的特性,RenderDoc 默认连段落都省掉;用
Show Unused Items会以斜体列出未用但有绑定的绑定。
官方还给了两个开关帮你切换视角:Show Unused Items 把未用但有绑定的槽列出来(斜体);Show Empty Items 进一步把空绑定也列出来[pipeline_state]。默认两者都隐藏是为了清爽,但排查"是不是有个槽根本没绑"时,就一定要把它们打开。记住一句话:颜色是信号,不是装饰。
以红底为例,我把它锻成一个可复现的小流程,遇到"某处画面黑/闪"就直接套:先打开 Show Empty Items,让空绑定现身;再逐个看红底条目,哪些是 shader 真在用、却没绑的;确认后进对应资源槽,看该绑的纹理或 buffer 到底在不在;若在,再看它绑错没绑错位置。走完这一步,绝大多数"空绑定导致的异常画面"都能收口成"我把 X 绑到了 Y 槽,本应在 Z 槽"。这也是把抽象颜色翻译成具体操作的最好示范:红底只是一个起点,真正的结论在"顺着红底追到绑定"那一步。
把这套颜色语义整理成一张对照表,开窗口时就能对号入座:
| 颜色 / 样式 | 含义 | 该往哪查 |
|---|---|---|
| 红色背景 | 空绑定,shader 可能读未绑定资源 | 打开 Show Empty Items 确认,查是否真漏绑 |
| 蓝色背景 | 带特殊参数的绑定(常用是只绑了部分 mip/slice) | 看绑定详情里的 mip/array 范围 |
| 绿色背景 | 视图把资源转成了另一种兼容格式 | 对照 shader 期望格式 vs 实际创建格式 |
| 斜体 | 未用但有绑定的槽 | 打开 Show Unused Items |
| 灰色阶段 | 该管线阶段未启用(pass-through) | 确认本条 draw 是否真的走了这级 |
这张表的价值在于让"异常颜色"从"看起来怪"变成"可操作的信号"。你看到红底,就知道该去查"是不是 shader 试图读一张没绑的纹理",而不是停在"哦它红了"。
六、资源在帧里被谁用:Timeline 上的"资源使用条"
前面我们两次提到"资源在帧里的使用"。这一章把这条链走通:从资源回到命令流的双向反查。
6.1 Timeline Bar 的 usage 显示
RenderDoc 的 Timeline Bar 不只是事件浏览器的另一张图,它还能画资源的使用情况。官方文档:如果 Texture Viewer 活跃,被显示的纹理会在 timeline 上显示一条 usage bar;如果 Resource Inspector 活跃,被检查的资源同样显示 usage[timeline_bar]。
这个 usage bar 是什么意思?就是把一个资源在整个帧里的读写访问,沿时间轴画出来:它被哪些 action 读过、哪些写过、分别在第几个时间点。对于"这块纹理到底被谁改了"这类问题,它是直接的答案:不用一格格在事件列表里翻,timeline 一条 bar 全给你标好。这也是本系列强调"鸟瞰优先于逐条扫"的又一体现。
这里值得把"读"和"写"两类访问分开看,它们在排障里的含义完全不同:
- 读(read):某个 action 把这块资源当输入。重点关心"谁靠它出结果",常用于"这个效果依赖哪张图造成"。官方描述里 usage bar 用三角形标出读取点[timeline_bar]。
- 写(write):某个 action 往这块资源里写数据。重点关心"谁把它改了",常用于"这张图为什么被涂成一团糟"。
上面这两条,官方在 Timeline 上的呈现会落到一条具体的 usage bar 上。下面这张截图就是被检视资源在 Timeline Bar 上的 usage 形态:三角形标出读取点、特殊标记标出写入。有了它,"这块纹理在哪一段被谁改过"就不用靠记忆在事件列表里一格格翻了。
这条 usage bar 还有一个进阶用法:和 Pixel History 配合,形成"资源视角 + 像素视角"的交叉验证。Pixel History 回答"某个像素被哪些 draw 碰过"[pixel_history],usage bar 回答"某块资源被谁读过写过"。当你怀疑"这个残影是不是某帧没清屏所致",一边用 usage 看谁写了这张 RT、一边用 Pixel History 看这个像素被哪几个 draw 叠加,两个方向证据一对上,结论就跑不掉了。
顺带说一个和 ep04 联动的心智:ep04 讲 timeline 能联动 Pixel History(某个像素被哪些 draw 碰过);这里 resource usage bar 是资源的视角,Pixel History 是像素的视角,两者放在一起,就同时有了"资源被谁写"和"像素被谁画"两个答案。
6.2 双闭环:从命令到资源,再从资源回命令
把前面的章节串起来,ep05 的核心方法论就是双闭环。我画一张图把这条双向路标出来:
- 正向闭环(命令 → 资源):在事件浏览器选中一个 draw,到 Pipeline State 看绑定,点绑定进 Texture / Buffer Viewer 看真身。回答的是"这个 draw 用了什么"。
- 反向闭环(资源 → 命令):在 Resource Inspector 看到某资源及其 EID 使用区间,沿 Timeline usage bar 看到它被哪些 action 用到,再跳回事件浏览器定位那些 draw。回答的是"这块资源被谁用过或改过"。
这张图是本文的题眼:不止要会"从 draw 进资源",还要会"从资源回 draw"。前者是大多数教程教的单向读法,后者才是解决"为什么这块纹理变成这样"的关键。一个完整闭环下来,你会从"看懂工具"升级成"会用工具破案"。
光讲两条闭环,你可能还是觉得抽象。我用一个真实场景把它跑一遍,你就明白反向那条循环为什么救场:场景里有一面镜子,期待镜面反射,可渲染结果里这面镜子永远是暗的、没有倒影。很多人会惯性走正向闭环,去事件浏览器找画镜子那个 draw,看它绑了什么,却常常卡在"这帧几千条 action,我根本不知道画镜子的那条叫啥"。换成反向闭环就顺了:
- 先在 Resource Inspector 里留意有没有一张疑似环境贴图的资源,比如叫
Cubemap; - 看它的 usage EID 区间,确认本帧有没有命令真的读过它;
- 沿 usage bar 找到读它的那个 action。若这套操作走到一半发现
Cubemap压根没被任何 draw 读过,直接锁定"反射没采样"这个方向; - 再跳回事件浏览器选中那个相关的 draw,进 Pipeline State 看它对
Cubemap到底绑在哪个槽、采样器对不对; - 若发现采样描述符是红底(空绑定),或误绑到了别的槽,问题当场定位。
这条路径正中要害的地方在于:你不需要预先知道"是哪个 draw",也能开始查。从"资源是不是真被用上"问起,你几乎总能一步步走到结论,而不必在一堆 action 里大海捞针。所以我的建议是:正向闭环适合"我知道画了什么、想看它用什么画";反向闭环适合"我看到一个现象、但不知道是谁造成的"。两套交替用,才是完整的破案心法。
七、把数据拉出来自己验:导出与自算
RenderDoc 不只能看,还能把你关心的数据原样导出,让你离开 UI 自己算一遍。这是我很看重的一点:本系列一直强调"自证式验证",导出就是自证的最小入口。
7.1 Buffer 导出:CSV 或 raw bytes
Buffer Viewer 支持把数据导出。官方文档说得清楚:buffer data 可以导出为 CSV 值;如果缓冲区是字节内存,还能导出为 raw bytes[buffer_viewer]。
这有多香?比如你怀疑一个顶点 buffer 里的位移数据算错了,一键导出 raw bytes,拿脚本按 vertex struct 解析,算出来和预期一对,就知道是不是贴图采样错位或数据塞错。这比在 UI 里一格格瞪眼靠谱得多,也是"自算数据"的实证入口。
两种导出格式各有一处适用面,顺手记下:CSV 适合数值型小数据,比如常量 buffer 里的几个矩阵分量或参数,导出即是一张可直接读的表;raw bytes 适合字节级数据,比如顶点/索引 buffer,导出后按 struct 手工解析。判断用哪种,就看"我要的数能不能装进表格",能就 CSV,不能就 raw。别为了"统一"强行用一种,徒增解析成本。
7.2 Pipeline 状态导出:HTML,专门为对比而生
如果你要在两个 draw、或两个 log 之间比较管线状态,RenderDoc 留了杀手锏:点导出按钮,把整份管线状态导出成 HTML 文件。官方说法是"这样你能更轻松地比较不同 log 和不同事件"[pipeline_state]。
这个功能对"排查状态差异"极其有用。想验证"为什么第 50 号 draw 和 60 号 draw 渲染效果不同",把两个状态的 pipeline 都导出,逐段 diff 绑定、diff 光栅化设置、diff shader 槽位,差异一清二楚。这比来回切窗口把状态记在脑子里,强了一个量级。
具体 diff 时有个小技巧,值得养成:不要肉眼 diff 整份 HTML,而是先管住"分块"。一份导出 HTML 通常按管线阶段分块,你心里先假设"问题大概率出在纹理绑定或光栅化",就先只扫这两块,别的先忽略。等这两块确认无误,再去比下一块。这样能避免被无关差异淹没,把"差异很多"收敛成"这份差异里哪几处是我要的"。等你看多了,看到两块之间垫了张无关的 shader 槽位差异,也能从容判断它跟当前 bug 无关,径直跳过。
7.3 自算一条链路:从导出到结论
我推荐一套"导出、自算、归因"的模板,和本系列"自跑闭环"的价值观一脉相承:
- 在事件浏览器选中目标 draw;
- 进 Pipeline State,确认它绑了哪几张图、哪几个 buffer;
- 对疑点资源导出 raw / CSV;
- 用脚本按游戏的 struct 解析,算出你要的数字(顶点位置、法线、资源 ID 等);
- 与"预期正确值"对比,得出是或否、差在哪的归因结论。
这里藏着这套模板最容易漏的一环:"预期正确值"从哪来? 我的建议是三个来源按需取。一是引擎侧:直接问引擎暴露的参数或日志,CPU 侧算出并打印那份数据,这是最权威的基准。二是独立推导:不依赖引擎,用数学/逻辑自己推一遍该有的结果,比如"这个顶点的世界坐标应该落在哪",适合验证引擎内部算法是否自洽。三是交叉来源:用另一套工具或另一帧 log 的同一份数据对,靠"两处独立得出同一值"来互相背书。缺了预期值,导出的数据只是一堆没有倾角的数字;有了预期值,对比才有方向,归因才有依托。这也是我把导出和自算绑定在一起,而不是只教"怎么导出"的原因。
这套流程的好处是可复现、可复核,而不是"看个大概"。这也正是本系列把工具用成"验证台"而不是"观感工具"的分水岭。一旦你养成了"看不懂就先导出自己算"的习惯,RenderDoc 就不再是黑盒,而是你手中的量具。
为了说明"自己算到底能算出什么",我很想给你具体的例子。假设你在排查一个阴影不匹配的问题:你在屏幕上看投影位置总偏差半个像素,直觉是采样器或 UV 缩放出了问题。这时你走一遍模板:
- 选中画阴影的那个 draw(通常叫 ShadowPass 或类似),用过滤表达式
$parent(marker名)快速定位(ep04 讲过过滤函数); - 进 Pipeline State,确认它绑的深度图、矩阵常量 buffer、采样器都在;
- 把矩阵常量 buffer 导出成 raw bytes,按 shader 里的 matrix struct 解析出那 16 个浮点;
- 拿这 16 个数和 CPU 侧算出的投影矩阵比,若是投影范围或 near/far 对不上,问题就锁在矩阵生成,而不是采样;
- 若矩阵没问题,再回到纹理侧,看那张深度图在 ShadowPass 前后是不是自己期望的 mip 层。
这套从"现象(偏差半像素)"到"举证(矩阵/深度图导出自算)"的路径,正是"自证式验证"的精神内核。前四步几乎不依赖你懂引擎内部,只依赖你会用管线状态页加 buffer 导出,因此它是可复制、可教的方法论,而不是只能靠经验的一次性灵光。
7.4 让自算规模化:脚本接管导出
前面那套模板,单点手工导出已经够用;但一帧里要验的资源往往几十个,挨个点会显得笨重。此时就该让脚本接管。RenderDoc 提供一整套 Python 脚本 API:可以程序化打开捕获、遍历 EID、读取 pipeline 状态、导出 buffer 数据,甚至把两帧的绑定差异整理成表格[python_api]。这也是"自证式验证"往规模化走的那一步:把"人肉点一遍"升级成"脚本跑一遍",结果天然可复现、可回归。
它和前面模板的分工是这样:遇到单个疑点,用 UI 手工导出最快;验证一批规律性数据,用脚本最稳。两条路通向同一个目标,都是把"看个大概"变成"有依据的结论"。动手前先想清楚走哪条,别让手段反客为主,为写脚本而写脚本。这里点到为止,因为脚本化本身足够开一个专题,本系列后续单开一篇讲,此处只需要你知道"工具支持你规模化自证"就够了。
八、落到引擎:UE 与 Unity 里的资源可见性
抽象的窗口讲完了,这一章看看真实引擎里的坑。资源可不可见、名字可不可读,很大程度上取决于引擎怎么把资源信息喂给 RenderDoc。
8.1 UE:插件内置,默认启用
Unreal 集成 RenderDoc 是内置插件、默认启用的。官方文档说明:可用命令行 -AttachRenderDoc 直接拉起 RenderDoc 附加到 UE,项目设置里还有 Ref All Resources、Save All Initial States 等项[ue_renderdoc]。所以 UE 用户基本开箱即用,抓帧后资源正常进 RenderDoc 的账本,这一节说说 UE 的两个设置点就够:Ref All Resources 让你在抓帧时把"本帧没被引用的资源"也一并收录,Save All Initial States 让资源名和初始状态都被存下来。前者解决"少了资源",后者解决"名字全可读",正好对应前面讲的资源可读性。
把三套主流来源拉一张表,资源可见性的差异就一目了然:
| 来源 | 默认是否带资源名 | 是否需要额外步骤 | 关键坑 |
|---|---|---|---|
| UE(Engine) | 是(插件内置默认启用) | -AttachRenderDoc 拉起 |
主要是相关设置项去勾才会丢资源 |
| Unity(Editor) | 否(D3D11 调试信息默认剥离) | shader 加 #pragma enable_d3d11_debug_symbols |
不加就看不到常量/资源命名与 shader 源码 |
| 自研引擎(Vulkan/D3D) | 由开发者决定 | 主动调 DebugUtils/TrackedResource 接口命名 | 不调就全是地址 |
这张表的本质结论是:资源名"默认有没有",决定权在引擎,不在 RenderDoc。所以当你打开别人的 .rdc,发现资源全是 Texture 0x...,别急着怪工具,先问"引擎有没有把名字喂出来",再决定要不要用 Rename Resource 手动补。
8.2 Unity 的坑:debug symbols 默认剥离
Unity 这里有个你八成会踩的坑。官方文档:Unity 集成支持在编辑器启动并捕获 RenderDoc,D3D11 下调试信息默认被剥离,要看到常量、资源命名和 shader 源码,必须给 shader 加一行 #pragma enable_d3d11_debug_symbols[unity_renderdoc]。
这条和前面的"重命名资源""shader reflection"环环相扣。如果 Unity 把调试信息剥了,shader reflection 可能就拿不到名字,你在 Pipeline State 里看到的绑定和 buffer 格式就可能是编号而不是语义名。理解了这一条,你就明白为什么偶尔"资源名全没了",那不一定是你不会用 RenderDoc,更可能是引擎没把名字传出来。
8.3 命名资源的通用经验:debug utils
Vulkan 官方教程演示了一个可跨引擎复用的技巧:用 vkSetDebugUtilsObjectNameEXT 命名对象,命名后 RenderDoc 里就能看到这个名字[vulkan_renderdoc]。同一套思路,D3D 下有 SetPrivateData,GL 下有 glObjectLabel。给资源起名这件事,本质是"把可读性注回工具",和引擎集成度无关,是每个能接触调试接口的人都该养成的习惯。这也是官方把 In-Application API 单独开一章讲的原因[inapp_api]。
我把三个 API 的命名入口收成一张速查,方便你贴脸用:
| API | 命名入口 | 一句话 |
|---|---|---|
| Vulkan | vkSetDebugUtilsObjectNameEXT |
官方教程推荐,命名后可见 |
| D3D11/12 | SetPrivateData 挂 WKPDID_D3DDebugObjectName |
平台标准做法 |
| OpenGL/GLES | glObjectLabel |
GL 的官方命名扩展 |
用法都一样:在创建对象后顺手调一次,给出有语义的名字。养成这个习惯的回报,正是在 RenderDoc 里打开 .rdc 时,资源不再是一堆 Texture 0x...,而是一排 ShadowMap、GBufferAlbedo,一眼认得出谁是谁。别小看这一步,它可能是你从"看天书"过渡到"读档案"最便宜的一次投资。
九、结语:把"读资源"升级成"命令、资源双向验证台"
我们走完了一圈:从 Resource Inspector 的全量清单,到 Pipeline State 的绑定总板,到 Texture / Buffer Viewer 看真身,再到术语对齐、异常颜色、Timeline usage 反查命令流,最后到导出自算。你会发现,ep04 讲的是命令流这条"时间线",ep05 讲的是资源和绑定这条"物证链",而真正支撑"破案"的是那条由 Timeline 串起来的双向反查。
把这两篇合起来,你就拥有了"一帧"的两副地图:事件浏览器是时间轴地图(什么时候、画了什么),资源与描述符是物证地图(用了什么、绑在哪、长什么样)。两者互相反查,才构成完整的"可验证读法"。
我再用一句话总结这一篇想种下的习惯:看到绑定先问"接的是谁",看到资源先问"谁在用我"。前一个问题是正向查,后一个问题是反向查。等这套双向心智长出来,RenderDoc 就不再是几个孤立窗口,而是一台能不断自我追问的调试引擎。
下一篇我们沿着这条链继续往下走:把"物证"再放大到 Mesh Viewer 与 shader 反汇编。Mesh Viewer 会按图元/实例/顶点把几何数据一格格喂给你看[mesh_viewer],配合反汇编去读 GPU 在顶点级和像素级到底算了什么,把"桶工"读进几何和指令的更细一层。而这一切的底座,仍是本系列开场立的规矩:所有结论都建在可复现、可自算的真图之上。
参考资料来源:本系列技术说明均以官方文档、源码、引擎官方文档为一手依据,未使用来源不明或"搬运配图声称自跑"的素材;具体引用见正文各条。
下篇预告:(六) Mesh Viewer 与反汇编:读懂 GPU 在顶点与像素级到底算了什么。
开篇快捷键:想快速上手,先记住三样,Resource Inspector 是全量资源总账,Pipeline State 是绑定总板,TexList 是打开指定纹理的钥匙。
With Best Wishes.