Blender 的 UI 抽成库后:跑同一份 gpui 代码,首帧快 5 倍

同一份 gpui 源码,编译三次:BUI 首帧 40 ms、gpui 231 ms、gpui-fast 226 ms。

分享
Blender 的 UI 抽成库后:跑同一份 gpui 代码,首帧快 5 倍

上周我们发了一篇对 GPUI 的批评,说它的状态模型按树付费。这周有个更好玩的东西出现:有人把 Blender 的界面从源码树里拆出来,做成了一个库,然后拿同一份 gpui 语法的源码编译了三遍。

结果:首帧 40 毫秒对 gpui 的 231 毫秒、gpui-fast 的 226 毫秒;启动消耗 CPU 27 毫秒对 226 / 217;闲置时 gpui 每分钟烧 743 毫秒 CPU,BUI 是 0。

项目叫 BUI(Blender's UI),GPL-3.0,ppmpreetham/BUI,两天内 2.4k star。

一、它是什么

一句话:Blender 的用户界面,作为一个库,可以从 Python、Rust、JavaScript(React、Svelte)、C、C++ 和 Dear ImGui 里用。

README 的说法很直接:人们花了二十年构建并优化 Blender 的 GUI,它省电、省内存、非常快——"我们把它解耦出来,让每个人都有机会用它做自己的产品。"

它同时发布了几个包:pip install blenderui、cargo add blenderui、npm i @blender-ui/react,Windows / Linux / macOS 三平台。渲染器有两套:默认的 CPU 软件渲染,和可选的 GPU 渲染(BUI_GPU=1)。

它给不同语言准备了不同的"语法皮":Python 侧可以写 tkinter 风格、Qt 风格、Blender 组件风格;Rust 侧可以写 rsx!、view!,也可以写 gpui 风格的 builder 链(div().flex().gap_2().child(...)、Render、cx.listener)。

二、为什么这件事值得看

因为它绕开了"重写一个 GUI 框架"这件事。

大多数 Rust GUI 框架是在重新发明一套布局、绘制、字体、输入法、可访问性。Blender 的界面是已经在几十万行 C 代码里跑了二十年的东西:它有自己的一套 widget、自己的一套交互(拖数字、框选、右键菜单)、自己的一套主题系统。

BUI 的做法是把这一层"抠"出来——保留 widget 与交互,把渲染与宿主窗口重新接。于是你可以得到"Blender 的手感",但写的是 Python 或 Rust。

对做工具的人来说,这个诱惑很具体:你要的不是另一个框架,你要的是"看起来不像 Electron"。

三、基准一:启动与闲置

同一份小设置窗口(标签、输入框、复选框、滑块、按钮),五种写法,加上同一个 gpui 语法源文件 settings.rs 分别编译到 BUI、gpui、gpui-fast。10 次启动的中位数,从进程启动到第一帧上屏,Python 3.14 / Node 22:

写法 渲染器 启动 启动耗 CPU 闲置内存 闲置每分钟 CPU
Rust(HTML + CSS) CPU 38 ms 27 ms 13 MB 0 ms
Rust(HTML + CSS) GPU 42 ms 61 ms 31 MB 1.8 ms
Blender 组件(Python) CPU 112 ms 86 ms 23 MB 0 ms
tkinter 语法(Python) CPU 139 ms 112 ms 25 MB 0.2 ms
同一份 gpui 代码:BUI CPU 40 ms 27 ms 15 MB 0 ms
同一份 gpui 代码:BUI GPU 43 ms 60 ms 31 MB 2.1 ms
同一份 gpui 代码:gpui GPU 231 ms 226 ms 29 MB 743 ms
同一份 gpui 代码:gpui-fast GPU 226 ms 217 ms 31 MB 2.5 ms

原文的总结是:同一份代码,BUI 的首帧比 gpui 与 gpui-fast 早约 5 倍,到达那里的 CPU 少约 8 倍,在 CPU 渲染器上内存只有一半;闲置时 gpui 持续占用约 1.2% 的一个核,BUI 是 0。

笔记本测不了"每个 app 的能耗",所以它用"app 引发的 CPU 时间"来代理:启动时消耗的 CPU + 窗口闲置 20 秒内消耗的 CPU。BUI 只在有变化时重绘,其余时间睡——所以两套渲染器的闲置占用都接近零。

四、基准二:帧时间

Rust 侧,场景是 3,000 个动画格子,所有 app 在同一会话内跑(GPU 行用 Direct3D 11):

BUI(GPU) BUI(CPU) gpui-fast gpui
FPS 59.9 57.6 31.5 19.9
每帧 CPU 11.6 ms 22.0 ms 31.9 ms + GPU 50.5 ms + GPU
内存 40 MB 30 MB 65 MB 77 MB
首帧 ~5 ms ~5 ms ~200 ms ~220 ms
闲置 CPU 0 % 0–0.9 % 0–0.4 % 0.9–1.4 %

更大的场景里差距更明显:1,000 个动画格子时,BUI 两套渲染器都跑满 60 FPS,gpui-fast 49.2、gpui 36.4;不封顶的话,BUI 在 100 格能到 1,491 FPS、1,000 格 309、3,000 格 94。

Python 与 C 的完整组件 demo(520×900):整帧重绘约 0.7 ms,悬停或拖数字这类"只画损坏区"的帧约 0.05 ms,原生窗口从输入事件到上屏约 0.06 ms。对照:Dear ImGui 的 demo 窗口全部展开(27k 三角形,1280×800)一次软件全重绘约 5–8 ms。

五、它快在哪

作者给了四条实现上的原因,都是常规手段,但组合得干净:

  • GPU 渲染器里,每个图元都是同一个 shader 的一个实例,所以一帧只是几次实例化绘制(Windows 上是 Direct3D 11,Linux 上是 OpenGL 3.3)。
  • 文字与图片缓存在图集(atlas)里。
  • 帧渲染到一个持续存在的目标上,只有损坏区域重绘——这就是上面那个 0.05 ms 的悬停帧。
  • Windows 上用 flip-model 交换链 + dirty rect 呈现。

它还做了一个很硬的正确性检查:GPU 着色器"求值软件光栅器的公式",结果在 172,800 个像素里有 10 个差异超过 3/255,两套后端一致。

六、三条必须写出来的限制

  1. 数字是"某个时间点"的。 原文写得很清楚:这些数据在 Ryzen 7 5800U 笔记本(Windows 11、插电)上测得,而且早于 Dear ImGui、Linux GPU 渲染器、亮色主题、Blender 搜索与表达式这些特性落地。"它们展示的是事情的形状,不是对你机器的承诺。"
  2. gpui 那边不算完全对等。 gpui 没有表单控件,所以基准里它的控件是带样式的 div;而 BUI 用的是真的 widget。"同一份代码"指的是源码形态一致,控件语义并不相同。
  3. gpui-fast 的数字在同一台机器上会波动(重场景 30–60 FPS),所以只能在同一个会话内比。

还有一条不在基准里、但更要紧的:GPL-3.0-or-later。 你想把它嵌进闭源商业产品,这就是个真问题——Blender 的 UI 便宜,是因为它一直是 GPL 的。

七、我们的整理

上周那篇批评说的是状态模型:GPUI 的 notify 只精确到实体,成本跟着树走。BUI 证明的是引擎层:把布局、绘制、字体、损坏区重绘做到位,同一份代码的首帧和帧时间能差 5 倍、闲置 CPU 能差到 0。

这两件事不矛盾,也不能互相替代。

  • BUI 的快,主要来自"只画变了的东西"和"绘制不经过整棵树的重建"——那是引擎与呈现的功劳。
  • 上周那篇说的是"一次状态变更会触发多少应用代码重跑"——那是状态模型的问题,BUI 没有回答它(它给的是一套 widget 与事件接口,不是 signal 图)。
  • 所以正确的读法是:同一个状态模型,换一个好引擎,常数能降一个数量级;但渐近行为还是由状态模型决定。 3,000 行时差 5 倍,30,000 行时谁先撞墙,得另测。

三条实用判断:

  1. 你要的是"不像 Electron 的桌面工具",而且能接受 GPL —— BUI 现在是这个需求下最省事的一条路,Python 就能写,启动 112 ms。
  2. 你要的是"大量自定义绘制 + 高频刷新"(示波器、波形、3D 视口叠加 2D 面板)——BUI 的 GPU 渲染器 + 损坏区重绘正好对症,而且它自带 Blender 的控件手感。
  3. 你已经在 GPUI 上、且痛点不在绘制而在重渲染 —— 换成 BUI 不会自动解决,得先按上周那篇的判据量一遍:你的列表有多长、一次改动会触发多少 view 重建。

边界:基准是作者自己跑的,测试代码与原始数据在 bindings/rust/bench/ 里公开可复跑;但设备单一(一台 5800U 笔记本)、时间点偏早(部分特性未落地)、gpui 侧控件不等价。方向可信,绝对值别直接抄进你的选型 PPT。


来源