调试与测量
不开调试器看 DreamGUI 画了什么、花了多少:DreamUI.Capture、DreamUI.Stats 与 Insights 作用域、stat DreamGUI、DreamGUI.Memory、两个校验开关、诊断命令、可供 A/B 的控制台变量和日志分类。
两个问题——"它画了什么"和"它花了多少"——都能不用调试器回答。
它画了什么:DreamUI.Capture
DreamUI.Capture [Directory]把当前世界的 UI 写成 PNG:视口一张,每个渲染到渲染目标的根画布各一张。不给目录时写进
Saved/DreamUI/Captures 下一个新建的、带时间戳的目录。画布直接画在屏幕上时没有自己的图——它是视口那张图的一部分,
截视口就行。
同样的事是一个 Blueprint 库,UDreamUICaptureLibrary:
| 函数 | 作用 |
|---|---|
CaptureAll | 控制台命令做的事:Viewport.png 加每个渲染到目标的根画布的 <widget 名>.png,返回写出的文件 |
SaveViewportToPng | 世界所在的视口——场景加上面的每个面板,玩家看到的样子 |
SaveCanvasToPng | 一个渲染到目标的画布画了什么 |
SaveRenderTargetToPng | 一张渲染目标的像素,保留 alpha |
GetCaptureDirectory | <Project>/Saved/DreamUI/Captures,相对路径从这里算 |
每次调用都要把图从 GPU 读回来,所以会等渲染线程把手上的活做完:这是用来看的工具,不是每帧做的事。没有 GPU
(-nullrhi)时没东西可读,调用在日志里说明并返回 false。在游戏里,视口是直接画进窗口的、帧与帧之间没有纹理,
所以 SaveViewportToPng 返回 false 并提示改用引擎自己的截图请求。
它花了多少:DreamUI.Stats 与 Insights
DreamUI.Stats打印自上一次 DreamUI.Stats 以来的帧逐阶段的花费,然后重新开始计数——所以在想测的那段前后各敲一次。
| 阶段 | 是什么 |
|---|---|
ManagerTick | UI 管理器的 tick |
CanvasUpdate | 画布更新 |
Batching | 合批 |
DrawCallSubmit | 提交绘制调用 |
RenderRecord | 渲染线程录制 |
每个阶段给出每帧毫秒数和运行次数。后面是计数器,每帧平均和总数:BatchesRecorded、VerticesRecorded、
SectionUploads、UploadedBytes、DataTextureUpdates、GeometryCopies、SectionReuses、WidgetsUpdated、
SectionPatches、DrawCallRebuilds、InPlaceRefreshes、渲染层的 RenderLayerMoves / Promotions / Demotions,
以及文字的 TextPaints、TextMoveRepaints、SmallTextPlacements、SharpenSweepTexts、SharpenRepaints、
CoverageItemsDrawn、CoverageGlyphLookups、CoverageRastersSync、CoverageJobs、CoverageFlushes、
FontAtlasUploadBytes。
在 Unreal Insights 里,每个阶段也是一个具名作用域,名字以 DreamUI_ 开头(DreamUI_LayoutPass、
DreamUI_UpdateRootCanvases、DreamUI_TextLayout、DreamUI_RecordScreenSpace、DreamUI_WorldSpaceRaycast……),
在 CPU 通道上。每个画布、每个分段、每个 widget 都要跑一次的细粒度作用域在一个单独的通道 DreamUIDetail 上,默认关闭——
几千个 widget 的墙上它们会跑几千次,每次读两遍时钟,开着的一帧就说明不了没开的一帧。需要时:
-trace=default,DreamUIDetail
Trace.Enable DreamUIDetail前者在命令行上,后者在控制台里。
stat DreamGUI 是插件的 stat 组,显示画布的合批、绘制调用更新、几何体更新、顶点变换、布局、属性绑定轮询、
根画布更新等计时,以及动画播放器的两个计数(自己计时的、由 Sequencer 更新的)。
它占了多少:DreamGUI.Memory
DreamGUI.Memory
DreamGUI.Memory Json
DreamGUI.Memory File=<path>每个字体的字形图集(切片数、GPU 字节数和 CPU 副本、格子、字形、字体文件字节)、精灵图集页,以及每个有 UI 管理器的世界里
画布网格的分段和绘制参数行。Json 改打印 JSON,File= 把 JSON 写进文件(相对于 Saved)。打包测试里的探针就是用它
记录内存报告的。
两个校验开关
为一个"画错了"或"没画出来"的报告准备的:
| 变量 | 打开后 |
|---|---|
r.DreamUI.VerifyPartialPrepare 1 | 画布每次从上一次增量准备(只重做请求了的、新来的、移动了的 widget 画的东西),都和一次全量准备对比;有差异就是一个点名画布的 ensure |
r.DreamUI.VerifyKeptPointers 1 | DreamGUI 不每帧查找而是保留的对象——widget 的画布、画布的渲染层、UI 管理器的画布、动画属性绑定的对象——在每次使用时也查找一遍;不一致就是一个说明是哪个的 ensure |
两者默认都关,开着有代价(全量准备、查找本来省下的开销)。测试宿主在 Config/DefaultEngine.ini 里把它们打开,
所以整个测试套件都在它们下面跑:
[ConsoleVariables]
r.DreamUI.VerifyPartialPrepare=1
r.DreamUI.VerifyKeptPointers=1基准测试(Tools/Bench)在测量的会话里会把它们关掉。文字的增量布局有自己的对应开关:
DreamGUI.Text.VerifyIncremental 1 让每次基于保留状态的布局都再从头布一次并比较,第一处差异写进日志,用新的那份。
诊断命令
| 命令 | 作用 |
|---|---|
dreamgui.ListPendingWidgets | 列出已经创建但还没加到任何地方的 widget(ConstructWidget 之后、AddChild 之前的"停放"状态),以及它们等了多久。把它们放在一个具名数组里是为了不被回收,能列出来是为了不让那里变成东西悄悄堆积的地方 |
DreamGUI.Diag.FindTreeBridges | 列出当前世界的关卡持有的对象(actor、它的组件或子对象)指向某棵 DreamGUI widget 树的每一个引用,以及那是谁的树。播放会话复制关卡时会顺着这种引用进入树 |
DreamGUI.Text.ShapeCacheFlush | 清空文字整形缓存,计数器保留 |
DreamUI.ExportSymbols | 重写 DUI/.dui-symbols.json(编辑器),见 VS Code 扩展 |
追踪类变量,打开后往日志里写:dreamgui.LayoutTrace 1(每次面板排列和它提交的每个矩形)、
dreamgui.ScrollBoxTrace 1(每次改变状态的滚动框拖动增量和物理 tick)、dreamgui.DumpMaterialDraws 1
(每次屏幕空间材质绘制尝试走了哪个分支,直到设回 0)。
可以拿来做 A/B 的开关
这些变量存在是为了测量:一次会话里先测 A,切换,再测 B。默认值就是出厂行为。
日志分类
| 分类 | 谁写 |
|---|---|
DreamGUI | 核心以及输入、控件、扩展、示例模块 |
LogDreamGUIRenderer | 渲染器 |
DreamTween | Tween |
DreamGUIEditor | 设计器和资产工具(文本编译、写回、桥) |
DreamGUIK2Nodes | Blueprint 节点 |
LogDreamGUIReferenceDocs | 参考文档 commandlet |
用引擎自己的方式调详细程度,比如命令行上 -LogCmds="DreamGUI Verbose",或控制台里 Log DreamGUI Verbose。