在传统终端模拟器的世界里,所有渲染几乎都在 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_speed、bob_speed、bob_amplitude 和 plane_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=1。fmt 字段告知客户端支持哪些 3D 格式,path 表示是否支持路径引用的资源注册方式,anim 和 depth 分别表示动画和深度控制能力。
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 \
row 和 col 定义锚点单元格,w 和 h 定义该对象在单元格空间中的尺寸范围,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)的架构文档与源码注释。
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。