Hotdry.

Article

Ratty 终端模拟器的 GPU 渲染管线:Ratatui 与 Bevy 的双引擎架构

深入剖析 Ratty 如何通过分离终端仿真层与呈现层,将 Ratatui 的 2D 渲染桥接到 Bevy 的 3D 场景,并设计 RGP 协议实现内联 3D 对象锚定。

2026-05-11systems

在传统终端模拟器的世界里,所有渲染几乎都在 CPU 侧完成 —— 字符映射、光栅化、抗锯齿等操作无一不经过主内存的拷贝与回读。Ratty 的出现打破了这个范式,它将一个完整的游戏引擎(Bevy)嵌入终端,用 GPU 加速的管线同时承载文本渲染与 3D 场景合成。本文从架构设计、渲染管线、坐标映射和协议设计四个维度,拆解这个「终端 × 3D 渲染引擎」混合系统的工程细节,并给出可落地的配置参数与性能观测点。

架构概览:终端仿真与场景呈现的分离

Ratty 的核心设计哲学是将终端的行为逻辑与其视觉呈现彻底解耦。这一决策源于一个朴素的需求:既然要让终端内容在 3D 空间中自由变形,那么终端状态的解析就不能依赖任何与空间坐标强绑定的假设。

从模块边界来看,Ratty 包含两条平行的工作流:

终端仿真侧(左侧)负责与外部进程交互。它通过 portable-pty 创建伪终端(PTY),读取 shell 的输出字节流,再用 vt100 解析 VT100 转义序列,维护一套与物理屏幕无关的逻辑终端状态 —— 包括光标位置、单元格属性、滚动区域和字符缓冲区。这部分完全在 CPU 侧运行,不涉及任何 GPU 调用。

场景呈现侧(右侧)则接管所有像素级渲染。它接收来自 Ratatui 的终端缓冲区(Buffer),将 2D 纹理上传到 GPU,由 Bevy 构建 3D 场景并输出到窗口。这条链路上的所有计算(坐标变换、光照、透视投影)均发生在 GPU 上。

两条工作流之间存在一个桥接层,其核心职责是:把左侧的逻辑单元格坐标映射为右侧的场景空间坐标,并将 3D 对象的变换参数(旋转、缩放、深度偏移)回传给场景图。这个桥接层是整个系统的技术难点,也是性能敏感区域。

渲染管线:四次跨介质传递

Ratty 的渲染管线并非一条平坦的 GPU 流水线,而是在 CPU 与 GPU 之间反复横跳的五步链路:

┌─────────────────────────────────────────────────────────────┐
│ Step 1: 终端仿真(CPU)                                       │
│ portable-pty → 字节流 → vt100 解析 → 逻辑屏幕状态            │
└────────────────────────┬────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 2: Ratatui 缓冲区构建(CPU)                            │
│ 逻辑屏幕状态 → Buffer::empty() → 字符/样式填充               │
└────────────────────────┬────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 3: GPU 文本渲染(GPU)                                  │
│ parley(字形整形)→ Vello(矢量光栅化)→ RGBA 纹理           │
└────────────────────────┬────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 4: 纹理回读(GPU → CPU → GPU)                         │
│ RGBA 像素数据经 PCI Express 回读至主存,再拷贝到 Bevy Image  │
└────────────────────────┬────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 5: Bevy 3D 合成与输出(GPU)                            │
│ 终端纹理映射到场景几何体 + 3D 对象 + 光照 → 帧缓冲区         │
└─────────────────────────────────────────────────────────────┘

这个管线中,第四步的 GPU→CPU→GPU 回读是当前架构最大的性能瓶颈。每次渲染循环,完整的终端纹理(典型分辨率如 1920×1080 的 RGBA 数据约 8MB)都需要经 PCI Express 总线回传至主存,再重新上传到 GPU 作为 Bevy 的输入纹理。这不是传统意义上的 zero-copy 路径,而是「GPU 加速的桥接」而非「全 GPU 常驻」。

GitHub 文档中明确指出,如果未来要走向真正的 zero-copy,需要实现 Bevy 的自定义渲染插件,直接在 Bevy 的渲染线程设备上分配和管理终端纹理,跳过 CPU 回读这一步。实现这一点需要深入理解 Bevy 的渲染图(Render Graph)扩展机制,当前社区已有初步讨论,但尚未落地。

光标映射:从单元格坐标到场景空间

光标是 Ratty 中最直观展示了「终端状态 → 3D 对象」映射机制的组件。Ratatui 将光标视为一个特殊的单元格(写入空格并携带光标样式属性),而 Bevy 则将光标实现为一个独立的场景实体(Scene Entity),挂载了网格(Mesh3d)、材质(MeshMaterial3d)和变换(Transform)组件。

单元格到像素的换算遵循线性映射:

cell_width  = viewport_size.x / cols as f32
cell_height = viewport_size.y / rows as f32

x = -viewport_size.x * 0.5 + (cursor_col as f32 + 0.5) * cell_width
y = viewport_size.y * 0.5 - (cursor_row as f32 + 0.5) * cell_height

注意 y 轴的符号反转 —— 终端的坐标系以左上角为原点,而 3D 场景中的正交投影通常以中心为原点且 y 轴向上。

光标的 3D 动画完全由 Bevy 的 ECS(Entity Component System)驱动,与终端状态解耦:

let spin = elapsed_secs * spin_speed;           // 自转速度 (默认 1.4)
let bob  = (elapsed_secs * bob_speed).sin()    // 上下浮动
         * cell_height * bob_amplitude;         // 浮动幅度 (默认 0.08)

transform.translation = Vec3::new(x, y + bob, cursor_plane_offset);
transform.rotation    = Quat::from_rotation_y(spin);
transform.scale        = Vec3::splat(cell_width.min(cell_height));

spin_speedbob_speedbob_amplitudeplane_offset 均可在配置文件中独立调优,互不干扰。光标模型支持 .obj.glb 两种格式,scale_factor 参数控制模型相对于单元格尺寸的缩放比例。

Ratty Graphics Protocol(RGP):在终端协议中嵌入 3D 场景描述

RGP 是 Ratty 的核心创新,它将 3D 对象的注册、锚定和属性配置编码为终端控制序列,使得任何向 stdout 写入数据的应用都能触发 3D 渲染,而无需了解 Bevy 的任何 API。

协议载体采用 APC(Application Program Command)控制序列,格式为:

ESC _ ratty ; g ; <verb> [ ; <key=value> ... ] ESC \

使用 APC 而非其他序列的理由是:APC 序列在大多数终端中不会被透传到应用层,适合承载「终端自身解释」的数据。而 ratty;g 的命名空间前缀则避免了与其他使用 APC 的应用冲突。

四种核心操作覆盖了完整的资源管理生命周期:

s(support query)用于能力探测。客户端发送 ESC _ ratty;g;s ESC \ 后,Ratty 以同样格式响应能力标记列表,如 v=1;fmt=obj|glb;path=1;anim=1;depth=1;color=1fmt 字段告知客户端支持哪些 3D 格式,path 表示是否支持路径引用的资源注册方式,animdepth 分别表示动画和深度控制能力。

r(register)将 3D 资产注册到 Ratty 的资产库中:

ESC _ ratty;g;r;id=7;fmt=obj;path=CairoSpinyMouse.obj ESC \

注册后的资产通过 id 引用,后续所有操作都基于这个整数句柄而非路径字符串。

p(place)将已注册的资产锚定到终端单元格坐标:

ESC _ ratty;g;p;id=7;row=5;col=10;w=3;h=2;animate=1;scale=1.0;depth=1.5;color=7fd0ff;brightness=1.0 ESC \

rowcol 定义锚点单元格,wh 定义该对象在单元格空间中的尺寸范围,animate 控制是否播放骨骼或顶点动画,depth 调整对象在 z 轴上的偏移(正值推远,负值拉近),color 以十六进制 RGB 覆盖模型材质颜色,brightness 调整光照强度。

d(delete)从场景中移除对象并释放资产引用计数。

应用层集成通过 ratatui-ratty widget 实现。对于已有 Ratatui TUI 的项目,只需添加依赖并将 RattyGraphic widget 渲染到目标区域即可:

use ratatui_ratty::{RattyGraphic, RattyGraphicSettings};

let graphic = RattyGraphic::new(
    RattyGraphicSettings::new("CairoSpinyMouse.obj")
        .id(7)
        .animate(true)
        .depth(1.5)
        .color([0x7f, 0xd0, 0xff])
);

graphic.register()?;
(&graphic).render(target_rect, &mut buf);

widget 内部并不直接修改 Buffer,而是将 RGP 序列写入 stdout,由 Ratty 解释执行。这种 stdout 注入方式保持了与标准 Ratatui 渲染流程的兼容性。

配置参数速查

以下参数可在 $HOME/.config/ratty/ratty.toml 中调整,按影响维度分组:

光标模型参数

[cursor.model]
path         = "CairoSpinyMouse.obj"  # 或 .glb 格式
scale_factor = 6.0                    # 相对单元格尺寸的缩放倍数
brightness   = 0.5                     # 材质亮度 0.0-1.0
x_offset     = 0.5                     # 单元格内的水平偏移
plane_offset = 18.0                    # z 轴推远距离
visible      = true                    # 是否显示 3D 光标模型

光标动画参数

[cursor.animation]
spin_speed     = 1.4   # 旋转速度(弧度/秒)
bob_speed      = 2.2   # 浮动频率(弧度/秒)
bob_amplitude  = 0.08  # 浮动幅度(单元格高度倍数)

3D 场景参数(按住 Ctrl+Alt 配合方向键实时调整)

[scene]
warp_factor = 0.0  # 终端表面弯曲程度,0 为平面
mode        = "3d" # "2d" 或 "3d",3D 模式下可透视「终端背后」

性能特征与当前局限

Ratty 不是一个面向轻量日常使用的终端模拟器。作者在 FAQ 中坦承,Bevy 游戏引擎的依赖链带来了约 300MB 的额外内存开销,首次编译需要处理约 600 个 Rust crate。这是追求 GPU 渲染能力和 3D 表现力所做的工程权衡。

已知的性能瓶颈包括:每帧的 GPU→CPU→GPU 纹理回读(约 8MB/pixel 数据量级)、Bevy 的默认渲染设置未针对终端场景调优、以及 Ratatui Buffer 每次重建时的全量重绘。这些问题在交互式输入场景下(打字速度~100 char/s)可能不构成明显延迟,但在需要高帧率动画或处理大尺寸终端(4K 显示器上的 200×60 终端)时会显现。

技术传承:从 TempleOS DolDoc 到 RGP

Ratty 的设计灵感直接追溯到 Terry Davis 的 TempleOS 和其 DolDoc 文档格式。DolDoc 的独特之处在于:文档文本与二进制资产共存于同一文件,文本区域中的 $$SP,"<1>",BI=1$$ 标记是对文件尾部 CDocBin 二进制区的引用索引。这意味着精灵数据天然就是文档的一部分,而非独立的外置资源。

Ratty 的 RGP register + place 机制在语义上与 DolDoc 的引用模型一致,只是用 APC 序列替代了 DolDoc 的内联标记语法,将「文件内引用」扩展为「stdout 流内引用」。从工程角度看,这种设计避免了为每帧渲染单独传输完整的 3D 模型数据 —— 注册操作只需执行一次,place 操作则只携带句柄和变换参数,流量极小。


资料来源:本文技术细节基于 Ratty 官方博客文章(blog.orhun.dev/introducing-ratty)及 GitHub 仓库(github.com/orhun/ratty)的架构文档与源码注释。

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com