Skip to content

第 5 章:从 Element 到 GPU——一帧是怎样画出来的

GPUI 使用保留状态的 View + 每帧声明式 Element 树 + 显式三阶段绘制。真正提交给 GPU 的不是 DOM,而是按 draw order 排序和分批的 Scene primitives。

1. 三层对象不要混淆

典型类型生命周期
View stateEntity<T: Render>跨帧存在,保存交互状态
Element treeimpl Element每次 render 重新构造
Scene primitivesQuad/Glyph/Path/Surface当前帧提交 GPU

Render trait 只有一个核心方法:把可变 view state 投影成 Element。

rust
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 定义关联状态并经历三个阶段:

text
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 的核心顺序可以简化成:

text
处理 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

文字经历:

text
字符串 + Font + Feature
  → shaping(glyph id / advance / cluster)
  → line layout / wrap
  → glyph raster / atlas tile
  → subpixel 或 monochrome sprite
  → Scene batch

TextSystem 和平台文本后端承担 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-149crates/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. 性能来自哪些具体选择

  1. 语义状态变化先缩成 patch/invalid range;
  2. View 只通知受影响实体;
  3. list 只实例化 viewport;
  4. text shaping 和 element paint range 可缓存;
  5. Scene 按 primitive 类型和 order 分批;
  6. content mask 在提交前剔除不可见 primitive;
  7. GPU 后端只消费扁平数组,不遍历产品对象图。

“GPU 编辑器”只是最后一步。前面每一层都必须避免全量工作,GPU 才不会只是快速画出一次昂贵 CPU 重算的结果。

独立源码学习笔记 · 文档采用 CC BY-SA 4.0