技术博客

Slate 渲染:FSlateDrawElement 如何合并成 Draw Call

第 88 / 100 篇 · 2026-05-24 · ue5 · Slate渲染UI优化

UMG 界面一卡,很多人第一反应是"widget 太多了"。但 widget 数和 GPU 压力之间还隔着一个关键层:批处理。Slate 每帧产出几百上千个绘制图元,真正发给 GPU 的 draw call 可能只有几十个。理解合并规则,才能知道哪些写法在制造 draw call、哪些只是制造图元(图元几乎免费)。代码在 Slate/Private/Rendering/ElementBatcher.cpp。

图元生产端:DrawElement

OnPaint 递归把图元追加到 FSlateWindowElementList:MakeBox(九宫格/普通矩形)、MakeText、MakeShapedText、MakeSpline、MakeGradient……每个 FSlateDrawElement 记录:着色器类型、笔刷资源(纹理或图集槽位)、混合状态、图层ID(元素间的排序键)、顶点数据。

关键认知:Paint 阶段不碰 GPU。它只是往一个数组里追加描述结构,真正的开销在后面的批处理与顶点填充。所以"OnPaint 被调用很多次"本身不等于 GPU 贵——要分开看。

批处理:合并的三条规则

FSlateElementBatcher::DrawElements 按图层 ID 顺序处理元素,遇到可合并的就并进当前 FSlateRenderBatch。两条元素能合并,当且仅当:

  1. 同一着色器类型(Box / Text / ESlateShader 各变体 / UI Material);
  2. 同一渲染资源:普通纹理元素要求纹理指针相同;图集元素要求图集句柄相同且纹理可采样(bRequiresTextureSampling);
  3. 兼容的混合状态与模板参数。

合并后的 batch 一次性填顶点缓冲提交。代价模型因此很清楚:draw call 数 ≈ 不同纹理 × 着色器组合的层内切换次数。一个全用引擎默认圆角框和图集字体的 HUD,全部元素可能合进个位数 batch;一旦插进一张运行时动态纹理(RenderTarget 做的血条、SceneCapture 做的小地图),所有画在它后面的元素被迫断批。

批处理的状态机是单向的:元素按图层顺序流式处理,不能回头重排。所以纹理相同的元素在图层顺序上离得越远,断批越多。这就是为什么布局上相邻的同类元素比散落各处的同纹理元素便宜。

纹理图集:合并的财政基础

Slate 有两类图集资产,是 UI 渲染省 draw call 的基础设施:

  • 笔刷图集(FSlateTextureAtlas):Slate 笔刷引用的纹理在加载时被打进 2048×2048(Slate.AtlasSize)图集,box/border 元素引用的是图集子区域——不同笔刷纹理在图集里同源,可合并。
  • 字体图集(FSlateFontAtlas):字体栅格化的字形按字号打进图集,一段文字所有字形同图集,一次 batch 画完。动态字号(每帧不同缩放)会产生多个图集。

FSlateShaderResourceManager 管理这些图集的生命周期。它的反面是 bRequiresTextureSampling=false 的图集元素可以连纹理采样都省掉。

从数据看优化方向

看 UI 渲染成本用 stat slate(Slate 线程耗时和 update time)和 stat scenerendering 里的 Slate 项。真正要拆 batch 数,得抓一帧看 render batch 列表。经验排序:

  1. RenderTarget 型 UI 元素(材质 UI、动态小地图)是头号断批源,一个全屏 RT 元素前后各断一次。Retainer Box 能救一部分——它把子树画进自己的 RT,子树内部完成批处理,但对它前后的元素没帮助。
  2. UI Material 每个材质一个着色器变体,穿插在普通元素中间制造切换。
  3. 圆角/渐变这类 ESlateShader 特殊路径本身是统一着色器,不算断批源;九宫格边缘渐变参数通过顶点色携带。

DrawElement 流 → 批合并:同着色器 + 同纹理才并批 OnPaint 产出(按图层序): Box / 图集A Text / 字体图集 RT 材质(断批点) Box / 图集A 合并结果: Batch 1(Box 全进) Batch 2(RT 材质) Batch 3(Box 续) 断批源排序:RT 元素 > UI Material 穿插 > 散落的不同纹理 图集(笔刷 + 字体)让异名纹理共享句柄,是低 draw call 的地基

实践结论

widget 数量管理的是 CPU 侧(Paint 遍历、invalidation),draw call 管理的是 GPU 侧,两套优化别混在一起。HUD 优化的起手式:stat slate 确认瓶颈在不在 Slate,在的话先数 RT 元素和 UI Material 数量——十有八九它们就是断批的元凶。把动态纹理集中到固定图层位置(比如全部放最顶层),让普通元素在下面连续成段,一次改动省下的 batch 往往比重写半个界面还多。