第 5 章:从 Element 到 GPU——一帧是怎样画出来的
GPUI 使用保留状态的 View + 每帧声明式 Element 树 + 显式三阶段绘制。真正提交给 GPU 的不是 DOM,而是按 draw order 排序和分批的 Scene primitives。
1. 三层对象不要混淆
| 层 | 典型类型 | 生命周期 |
|---|---|---|
| View state | Entity<T: Render> | 跨帧存在,保存交互状态 |
| Element tree | impl Element | 每次 render 重新构造 |
| Scene primitives | Quad/Glyph/Path/Surface | 当前帧提交 GPU |
Render trait 只有一个核心方法:把可变 view state 投影成 Element。
pub trait Render {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>)
-> impl IntoElement;
}源码:crates/gpui/src/element.rs:161-166。
2. Element 的三阶段协议
Element 定义关联状态并经历三个阶段:
request_layout
→ 返回 LayoutId + RequestLayoutState
→ Taffy 计算几何尺寸
prepaint
→ 已知 bounds
→ 建 hitbox / focus / event listener / text layout
→ 返回 PrepaintState
paint
→ 向 Window/Scene 写入 primitive源码:crates/gpui/src/element.rs:51-137。
为什么分开
布局阶段还不知道最终 bounds;命中测试需要 bounds,但不必立刻写 GPU;paint 应当只消费已经 准备好的几何和排版结果,尽量线性执行。
3. Drawable 用类型状态防止乱序
Drawable<E> 保存 ElementDrawPhase:Start → RequestLayout → LayoutComputed → Prepaint → Painted。每一阶段带着下一阶段需要的关联状态。
源码:crates/gpui/src/element.rs:253-286。
这避免每个 Element 把临时布局状态塞进长期对象,也让框架能在阶段错误时尽早 panic。
4. Window 一帧的主循环
Window::draw 的核心顺序可以简化成:
处理 dirty views / effects
→ 调 Render 生成 roots
→ request layout
→ layout engine compute
→ prepaint roots
→ prepaint deferred / prompt / drag / tooltip
→ paint roots
→ paint overlays
→ Scene::finish 排序
→ platform renderer present源码定位:crates/gpui/src/window.rs:2824-3087。
5. Prepaint 不只是“绘制前准备”
Prepaint 建立这一帧的交互空间:
- 插入 hitbox;
- 注册 mouse/key listener;
- 记录 focusable/tab stop;
- 计算 text line layout;
- 确定 clipping/content mask;
- 安排 tooltip、popover 等 deferred draw;
- 为 accessibility tree 写节点。
因此 input dispatch 与绘制共享同一份几何事实,避免视觉位置和点击区域各算一遍。
6. Scene 是扁平 GPU 指令集
Scene 收集:
- shadow;
- quad;
- path;
- underline;
- monochrome/subpixel/polychrome sprite;
- platform surface。
源码:crates/gpui/src/scene.rs:39-53。
插入 primitive 时先与 content mask 相交,完全不可见就跳过;再分配 draw order,写入对应类型 数组和 operation 序列。finish() 按 order 排序各数组,renderer 随后按连续类型批处理。
源码:crates/gpui/src/scene.rs:87-163。
7. Layer 与 z-order
Scene::push_layer(bounds) 为一组 primitive 分配共同 draw order。它不是 CSS stacking context, 而是 GPU 批处理和遮挡顺序的显式边界。
Window 的 paint_layer 在 paint 阶段使用它。对于大量滚动列表,合理 layer 能避免逐 primitive 构造复杂全局顺序。
8. 为什么文字不是普通 quad
文字经历:
字符串 + Font + Feature
→ shaping(glyph id / advance / cluster)
→ line layout / wrap
→ glyph raster / atlas tile
→ subpixel 或 monochrome sprite
→ Scene batchTextSystem 和平台文本后端承担 shaping/font fallback;Scene 只关心已经定位的 sprite。 Editor 的文本布局可以缓存 shaped line,滚动时复用,而不用每帧重做语义层计算。
9. 元素缓存为什么能工作
Window 记录 prepaint range 与 paint range。稳定 element id 可在下一帧复用之前的阶段输出和 Scene operation。Scene::replay 可以把旧帧某段 paint operations 重新插入当前 Scene。
源码:crates/gpui/src/scene.rs:141-149、crates/gpui/src/window.rs:3302-3380。
缓存的关键不是“整个 View 没变”,而是稳定 id 对应的子树可以证明其绘制输入未变。
10. 大列表的特殊处理
文件树、设置页、会话列表和编辑器都不能为所有行构造完整 element。GPUI 提供 list、 uniform_list 和 deferred draw:
- 先根据 viewport 推导可见范围;
- 只为可见行 request layout/prepaint/paint;
- sticky header、popover 等延迟到主内容后绘制;
- 滚动时复用未变化区域。
11. EditorElement 为什么巨大
editor/src/element.rs 同时承担:
- 可见行和 gutter 几何;
- selection/cursor/diagnostic/block 绘制;
- text shaping 与 wrap 结果消费;
- mouse position → DisplayPoint;
- IME、drag、hover、scrollbar;
- inline completion、blame、diff、code lens。
它是“编辑器领域投影到 GPUI 三阶段协议”的适配器,而不是 Editor 的全部业务状态。核心长期状态 仍在 Editor / DisplayMap / MultiBuffer 实体中。
12. 性能来自哪些具体选择
- 语义状态变化先缩成 patch/invalid range;
- View 只通知受影响实体;
- list 只实例化 viewport;
- text shaping 和 element paint range 可缓存;
- Scene 按 primitive 类型和 order 分批;
- content mask 在提交前剔除不可见 primitive;
- GPU 后端只消费扁平数组,不遍历产品对象图。
“GPU 编辑器”只是最后一步。前面每一层都必须避免全量工作,GPU 才不会只是快速画出一次昂贵 CPU 重算的结果。