Ghostty 源码拆解

字体与 Renderer:从 Cell 到 GPU Frame

字体发现、shaping、atlas、cell rebuild 和 Metal/OpenGL 抽象

字体与 Renderer:从 Cell 到 GPU Frame

终端渲染的难点不只是画矩形。每个 cell 可能包含 Unicode grapheme、宽度变化、Nerd Font constraint、emoji/color glyph、粗体/斜体、超链接、Kitty image 和自定义 shader。Ghostty 把它们拆成 font、renderer、shader 和 terminal view。

字体子系统

src/font/ 的关键层次:

font.Library / discovery
  → face / Collection / DeferredFace
  → Metrics / SharedGrid
  → Shaper (CoreText / HarfBuzz / noop / web canvas)
  → Glyph / CodepointResolver
  → Atlas / sprite

discovery.zig 处理 CoreText、Fontconfig、Windows 等字体发现;opentype/ 解析 SFNT、head、hhea、OS/2、post、glyf、SVG;shaper/ 负责把 Unicode 文本 shaping 成 glyph run;Atlas.zig 把 rasterized glyph 放入可复用的纹理区域。

SharedGrid 为什么挂在 App 上

App 持有 SharedGridSet,surface 按字体配置共享 SharedGrid。相同字体、字号、特性和装饰设置的多个 surface 不需要重复建立 glyph atlas。Surface 仍保留自己的 metrics/renderer 关联,但共享 cache 的生命周期由 App 控制。

这是一个典型的“跨 surface 共享、跨线程只读”设计:共享的是昂贵的字体解析和 glyph 资源,不共享会被某个 surface 独立修改的 terminal state。

Renderer 的分层

src/renderer/generic.zig 的注释给出完整抽象链:

GraphicsAPI
  → Target
  → Frame
  → RenderPass
  → Step
  → Pipeline
  → Buffer / Texture / Sampler

Metal.zigOpenGL.zigWebGL.zig 提供 Graphics API wrapper;renderer/metal/renderer/opengl/ 实现资源类型、pipeline、frame、render pass 和 shader;通用 renderer 负责从 terminal/font/image 状态生成可绘制内容。

一帧的工作

renderer.Thread wakeup/timer
  → lock renderer state
  → inspect terminal dirty + size + focus + search
  → rebuildCells (必要时)
      → read viewport/page
      → resolve glyph + shape runs
      → build cell/image/link buffers
  → update uniforms / atlas / background image
  → begin Frame
  → render passes (background, cell, image, overlay, shader)
  → end Frame / health

renderer.Thread 有三个相关节奏:普通 render timer、较便宜的 draw timer 和 cursor blink timer。窗口不可见或未聚焦时,动画 draw 可以暂停;真正需要更新 terminal state 时,通过 wakeup 唤醒。macOS 若要求 app thread 绘制,则 renderer 发 redraw_surface 给 app mailbox,而不是直接跨线程调用平台 view。

cell rebuild 与 GPU draw 的区别

renderer 注释区分“rebuild cells”和“draw”。cell rebuild 会读取 Terminal、构造 vertex/cell 内容,成本高;draw 只提交已有 buffer,适合 cursor blink、shader 动画等不改变 terminal 内容的帧。

这也是 dirty flags 有价值的原因:把“逻辑内容变化”和“仅视觉变化”拆开,避免每次 cursor 闪烁都重新扫描 scrollback 或重新 shaping。

shader 和图片

shader 资源来自 src/renderer/shaders/:GLSL、Metal 和 Shadertoy custom shader 分别有自己的编译/注入路径。Kitty graphics 和 background image 通过 image state、texture 和 render pass 进入 GPU;terminal cell 中出现的 image 不是普通文本 glyph,renderer 必须在布局时保留它的覆盖关系。

renderer/cell.zigrow.zig 等模块处理背景扩展、最小对比度、宽度约束和覆盖判断,保证一行 cell 的视觉效果不是简单“一格一纹理”。

失败与 health

每个 frame 结束时会报告 health;swap chain、target、shader reinitialize、GPU context lost 等状态会影响下一帧策略。renderer 不把所有错误都传播到 parser:终端状态可以继续增长,GPU 暂时不可用时由 renderer/runtime 决定是否重建目标。

设计取舍

  • 终端数据模型不依赖 Graphics API;
  • 字体昂贵资源跨 surface 共享;
  • rebuild 与 draw 分离,兼顾吞吐和动画;
  • 后端只承诺抽象接口,Metal/OpenGL 保留平台能力;
  • app thread 限制通过 must_draw_from_app_thread 在编译/运行时处理,而不是让通用 renderer 假设所有平台规则相同。

On this page