Skip to content

MGFX 性能模型

MGFX 的性能目标不是“消灭 immediate”,而是在 immediate 心智模型下尽量减少 Lua 侧无意义工作、材质参数上传、临时表和临时 Color。GMod UI 常见问题不是 draw call 数量单独过高,而是每帧为了调度、归类、转换和上传参数付出过多 Lua/Source API 成本。

当前方向

  • Shape 和 widget 保持 immediate shader/fallback path。
  • Public NameEx(..., style) 仍然接受表,但 renderer 内部必须在 API 边界展开为直接参数,不能一层层传 style 表再重复解析。
  • 只保留经过实测的专用 fused shader,不恢复通用 data-texture batch scheduler。
  • 常规 shape 参数优先通过 $viewprojmat / pixel shader c11 上传。
  • $invviewprojmat / pixel shader c15 作为辅助 16-float 参数页,$c0..$c3 只保留给兼容和诊断。
  • Pattern 在 shader 中数学化生成,不拆成大量 LineEx 或几何段。
  • Scoreboard、表格、聊天和普通 label 文本优先走原生 GMod text。

当前实测基线

当前代表性压力场景是复杂 shop UI:大量 rounded box、gradient、stroke、shadow、glow、image 和少量 text 混合绘制。关闭诊断后,实测结果为:

text
满商品列表      稳定 130+ FPS
商品较少分类    稳定 160+ FPS

这次提升来自减少 Lua 侧准备工作和无意义 pass,而不是恢复通用 batch scheduler。

Prepared 绘制层

外部 API 可以继续使用 style table,因为这是 GLua/Lux 调用方最容易读写的形式。但内部绘制层不应该继续传递这些表。

正确链路:

text
public API style table
  -> API 边界解析一次
  -> prepared fill/stroke/effect scalar 参数
  -> shader/fallback draw

应避免的链路:

text
style table
  -> helper table/spec
  -> 转发 style table
  -> 再次 normalization
  -> shader/fallback draw

RoundedBox、Chamfer、Progress/Segment、Image round/chamfer、line-backed rect、poly/chamfer fallback 现在都走 prepared 参数。这样 public API 仍然易用,但 inner loop 不再重复调用 shadowRawouterGlowRawinnerGlowRawbackdropStylepatternStylefillFromStylefillVisible

参数上传

本地 GMod benchmark 结论:

text
SetFloat x16             ~3.6-3.9 us/iter
SetUnpacked + SetMatrix  ~0.5-0.6 us/iter

因此 hot shape path 的 16 个常规 float 参数统一打包到 Matrix():SetUnpacked(...),再用:

lua
mat:SetMatrix("$viewprojmat", matrix)

HLSL 侧读取:

hlsl
const float4x4 MGFXExtraParams : register(c11);

注意 Source/GMod 的矩阵索引按列抵达 HLSL。Lua 端打包必须使用 MGFX 的统一 helper,不要在调用点自己猜行列顺序。

辅助参数走 $invviewprojmat

hlsl
const float4x4 MGFXAuxParams : register(c15);
lua
mat:SetMatrix("$invviewprojmat", matrix)

SetFloat("$cN_x", ...) 仍然存在,但它是备用/诊断手段。只有已经确认 matrix 参数页无法满足需求,并且有实测理由时,才考虑逐 float 上传。

Pattern 数学化

Pattern 不应该在调用层展开成许多 line、box 或 polygon。StripePatternSmokePatternWornPattern 是 paint slot,应该由对应 shape shader 根据当前 shape local space 直接采样。

正确方向:

lua
MGFX.RoundedBoxEx(x, y, w, h, {
    radius = 8,
    fill = Color(8, 18, 24, 220),
    pattern = MGFX.StripePattern({
        color = Color(255, 255, 255, 22),
        spacing = 12,
        width = 2,
        angle = 135,
    }),
})

错误方向:

lua
for i = 1, 40 do
    MGFX.LineEx(...)
end

如果某个界面需要大面积斜线、烟雾、扫描线或噪声底纹,优先补 shader pattern 能力,而不是在 UI 层堆 primitive。

融合 Shader 策略

专用 shader 是可行的,但必须复刻原始分层结果。允许的前提:

  • 输入参数布局清晰,主参数走 MGFXExtraParams
  • 需要额外参数时只用 $c0..$c3 辅助页。
  • 抗锯齿、stroke、透明渐变、pattern、glow、backdrop 和 blend order 与原路径一致。
  • 不能为了少一个 pass 改变 source-over 视觉结果。

当前保留的 fused 路径:

text
roundrect_fx   fill/stroke + innerGlow
chamfer        fill/stroke + optional innerGlow
ring_fx        fill/stroke + optional innerGlow
roundrect_shadow_outer  shadow + outerGlow
chamfer_shadow_outer    shadow + outerGlow
ring_shadow_outer       shadow + outerGlow
image_mask_shadow_outer shadow + outerGlow

shadowouterGlow 在 API 语义上仍然分离;合成 pass 只用于能保持 CSS-like 结果一致的 shape。backdrop、convex poly 的 shadow/glow 和部分 pattern 仍可能是独立 pass,因为它们的 draw bounds、framebuffer read、参数页压力或 blend order 是可见行为。

RoundedBox 还支持 shadow = { {...}, {...} } 多层阴影。它用于替代“为了多个阴影叠多次完整 RoundedBoxEx”的写法:style 只解析一次,每一层只走 shadow-only path,主体和其它效果仍然只画一次。

Backdrop 共享模糊

Backdrop blur 按“引擎渲染帧 + backdrop.level 整数层”共享,不再由每个 控件各自捕获 framebuffer。每层第一个非零 blur 捕获当时的 framebuffer, 并完成横向、纵向两个全屏 pass;同层同强度的 shape 随后只做一次带遮罩采样。

如果本帧有 N 个模糊 backdrop,且都没有显式 recapture,统计应为:

text
captures     1
reuses       N - 1
blur passes  2

同层切换 blur 强度只会从该层的原始捕获重新执行两个模糊 pass,不再次复制 framebuffer。提高 backdrop.level 会在该位置重新捕获,使高层能包含已经画出的 低层 UI。只有同一 level 内也必须强制获取更新源时才设置 backdrop.recapture = true;不要给每个卡片或列表行都设置 recapture。

分配规则

Hot paint path 中避免这些写法:

lua
MGFX.RoundedBoxEx(x, y, w, h, {
    fill = Color(20, 24, 32, 220),
})

local c = colorAlpha(baseColor, alpha)

第一种每帧创建 style table 和 Color。第二种如果返回新 Color,也会制造分配压力。MGFX 会尽量在内部复用 scratch record,但调用方仍然应该缓存稳定 style、pattern、gradient 和颜色对象。

更好的方向:

lua
local panelFill = Color(20, 24, 32, 220)
local panelStyle = {
    radius = 8,
    fill = panelFill,
}

function PANEL:Paint(w, h)
    MGFX.RoundedBoxEx(0, 0, w, h, panelStyle)
end

如果颜色每帧变化,优先复用已有 Color 对象并改字段,或者让上层 UI 状态缓存少量离散状态。不要在行级 scoreboard、列表或 HUD 表格中为每个 cell 每帧创建临时对象。

文本成本模型

Plain text 走原生 GMod text。MGFX text composer 只适合需要 shader 效果的文字:gradient face、soft outline、glow、surface polish、weight bias 等。

不需要特效的高频文本应该直接使用原生绘制,尤其是:

  • scoreboard rows
  • player names
  • dense tables
  • chat/log
  • rapidly changing counters

如果要用 FX text,尽量让字符串集合稳定,并使用 MGFX.PrewarmText 预热。

性能分析

先安装 mgfx/devtools.lua@lux/mgfx/devtools。开发时:

text
mgfx_profile 1
mgfx_draw_counts 1
mgfx_status

真实 FPS 检查时关闭诊断:

text
mgfx_profile 0
mgfx_draw_counts 0

诊断本身会产生额外计数和文本输出成本。最终判断要看关闭诊断后的体感和帧率。