- Add debounced aria-live region that announces visible terminal text to screen readers (500ms throttle, non-empty trimmed rows only) - Add role=application, aria-label, aria-roledescription to terminal - Add prefers-reduced-motion listener: disables cursor blink animation and sets --terminal-reduce-motion CSS variable - Add prefers-contrast: more media query with Canvas/CanvasText system color fallback - Add visually hidden .terminal-screen-reader CSS utility - Mark P7 complete in roadmap
307 lines
23 KiB
Markdown
307 lines
23 KiB
Markdown
# Terminal Roadmap — 对照 xterm.js 的追平计划
|
||
|
||
本文档记录 `base-path-demo` 浏览器终端**当前已实现的能力**、**相比 xterm.js 仍缺失的功能**,以及每一项**计划的实现方式与优先级**,供后续开发接手。
|
||
|
||
> 定位提醒:本项目目标不是复刻 xterm.js 这个通用 JS 终端库,而是按 [TERMINAL_HANDOFF.md](TERMINAL_HANDOFF.md) 的方向,用 `Axum + portable-pty`(后端)+ `Leptos + Rust/WASM`(前端)沉淀一套**高性能、可复用的 Rust 终端组件**。因此追平的重点是「让真实终端会话好用且性能好」,而非铺平 xterm.js 的全部 API 面。
|
||
>
|
||
> **性能目标**:终端默认渲染器已回切到 Canvas 2D,保留 WebGL glyph atlas / per-cell quad 与 DOM renderer 作为可切换路径。最新基准显示 Canvas 2D 在 10k 行输出、200×80 / 240×100 viewport 下吞吐和 frame time 都明显优于当前 WebGL 实现;WebGL 继续作为优化回归目标保留。
|
||
|
||
---
|
||
|
||
## 1. 覆盖度自评(截至本文档)
|
||
|
||
按用途粗估:
|
||
|
||
- **「跑通真实终端会话」核心用途**:约 **85%**
|
||
- **xterm.js 完整 API / 特性面**:约 **40%**
|
||
|
||
差距集中在 **API/插件生态、accessibility、搜索/链接、Unicode 深度精度、运行时可配置性、工程化性能基准**,而非「能不能用」。
|
||
|
||
### 已实现(有代码 + 单元测试)
|
||
|
||
| 能力 | 位置 |
|
||
|---|---|
|
||
| PTY 会话 + WebSocket 双向流 | `server/src/terminal/{pty_session,ws}.rs` |
|
||
| ANSI 解析(vt100 crate):颜色 / 粗体 / 斜体 / 下划线 / 反色、CSI 光标控制、擦除、滚动区 | `app/src/terminal/core.rs` |
|
||
| 256 色 + RGB 真彩 | `core.rs::indexed_color` / `resolve_*` |
|
||
| alt-screen(vim / btm / htop),含历史隔离 | `core.rs::process` |
|
||
| scrollback + 虚拟滚动(万行不卡,绝对定位) | `core.rs::collect_window` / `component.rs::virtual_window` |
|
||
| 宽字符 / 组合字符网格对齐 | `core.rs` `TerminalSegment.cols` |
|
||
| resize / reflow | `core.rs::resize` |
|
||
| `clear`(含 `ESC[3J` 清 scrollback) | `core.rs::contains_erase_scrollback` |
|
||
| alt-screen 滚轮 → 方向键翻页(rAF 合并多次 wheel 事件,单次滑动最多一屏) | `component.rs::handle_wheel` |
|
||
| 基础按键:Ctrl-C/D/L、方向键、Enter/Tab/Esc/Backspace | `component.rs::map_key_to_terminal_input` |
|
||
| 完整键表(F1–F12/Home/End/PageUp/Down/Ctrl A–Z/Alt-key) | `component.rs::key_to_bytes` |
|
||
| application-cursor 模式(DECCKM,方向键切 SS3) | `component.rs::cursor_seq` + `TerminalCore::application_cursor` |
|
||
| 粘贴(Ctrl-V / Ctrl-Shift-V / 右键菜单,含 bracketed paste) | `component.rs::handle_paste` / `prepare_paste` |
|
||
| 文本选区 + 复制(自建模型,Ctrl-Shift-C / 选区存在时 Ctrl+C,跨滚动) | `component.rs` 鼠标处理 / `core.rs::selection_text` |
|
||
| 鼠标上报(vim/htop/tmux 鼠标,SGR/Default/UTF-8 编码,滚轮) | `component.rs` 鼠标处理 / `encode_mouse_report` |
|
||
| 中文 / IME 输入(拼音/日文/韩文,隐藏 textarea + composition 事件) | `component.rs::handle_input` / `handle_composition_*` |
|
||
| Canvas 2D 默认渲染器 + WebGL glyph atlas 可选路径(per-cell quad、DPR glyph atlas、背景/underline/cursor backdrop、DOM 回退) | `component.rs::Renderer` / `render_canvas` / `canvas_render_plan` / `render_webgl` / `GlyphAtlas` |
|
||
|
||
---
|
||
|
||
## 2. 追平项(按优先级)
|
||
|
||
### P0 — 复制 / 粘贴 + 文本选区 ⭐ ✅ 已完成
|
||
|
||
**现状**:粘贴、选区、复制**全部完成并验证通过**。
|
||
|
||
**已实现**:
|
||
1. **粘贴** ✅
|
||
- `on:paste`(`ClipboardEvent`)读 `clipboard_data().get_data("text")`,归一换行为 `\r` 后作为 `Input` 发送。
|
||
- bracketed paste:应用开启时(`core.bracketed_paste()`)包进 `ESC[200~ … ESC[201~`。
|
||
- 实现于 `component.rs::handle_paste` + 纯函数 `prepare_paste`。
|
||
- 右键走浏览器默认菜单(避免依赖剪贴板读权限);Ctrl-V 为主路径。
|
||
2. **选区 + 复制** ✅(自建选区模型,xterm.js 风格)
|
||
- 选区按**逻辑行列坐标**跟踪(`Selection { anchor, head }`),鼠标 `mousedown/move/up` 拖拽更新;`point_to_cell` 做像素→单元格换算(含 padding/scroll)。
|
||
- 高亮 overlay:`selection_rects` 算每行矩形,渲染在虚拟滚动层内(共享 translateY,跟随滚动),不拆分文本段。
|
||
- 复制取 `TerminalCore::selection_text(start, end)` —— 直接从 `history_rows + screen_cache` 数据模型按行列取文本,**跨滚动、输出流动时都正确**(不依赖 DOM)。
|
||
- **Ctrl-Shift-C** 复制(Ctrl-C 保持 SIGINT);**当选区存在时 Ctrl+C 优先复制**(与 VS Code / Windows Terminal 一致),无选区时才发送 SIGINT。
|
||
- 打字清除选区。
|
||
- 实现于 `component.rs`(`Selection`/`selection_rects`/`point_to_cell`/鼠标处理)+ `core.rs::selection_text` + `core.rs::TerminalRow::text_in_cols`,均有单元测试。
|
||
3. **快捷键**:Ctrl-Shift-C 复制 / Ctrl-V 粘贴。
|
||
|
||
**已知后续项(非阻塞)**:拖拽到视窗边缘时**没有自动滚动延伸选区**(需先滚到位再选);行内 wide+组合字符混排的列切片为近似。
|
||
|
||
---
|
||
|
||
### P1 — 完整功能键 + application-cursor 模式 ✅ 已完成
|
||
|
||
**现状**:完整键表已实现并验证通过(`key_to_bytes_table` 单测覆盖)。
|
||
|
||
**已实现**:
|
||
1. **完整键表**(`component.rs::key_to_bytes`)
|
||
- F1–F4 SS3(`ESC O P/Q/R/S`)、F5–F12 CSI `~`(含 xterm 真实的 16/22 跳号)。
|
||
- Home/End、Insert/Delete、PageUp/PageDown、Enter/Tab/Esc/Backspace。
|
||
- Ctrl A..Z → 0x01..0x1a;以及 `@[\\]^_` 变体(→ 0x00/0x1b/0x1c/0x1d/0x1e/0x1f)。
|
||
- Alt+char → `ESC char`(标准 meta-as-ESC 编码)。
|
||
2. **application-cursor 模式(DECCKM)**:`cursor_seq(byte, app_cursor)` 助手;为真时方向键 / Home / End 发 SS3(`ESC O X`),否则 CSI(`ESC [ X`)。`handle_keydown` 和 `handle_wheel` 都据此切换。
|
||
3. **浏览器快捷键仲裁**:`handle_keydown` 对 bare `Ctrl+字母`(无 Shift/Alt/Meta)调用 `preventDefault()`,阻止浏览器吃掉 `Ctrl-R` 刷新、`Ctrl-F` 查找、`Ctrl-A` 全选等。`Ctrl+Shift` 组合(如 `Ctrl-Shift-C` 复制)和 `Ctrl+Alt`/`Ctrl+Meta` 组合保留给浏览器/系统。
|
||
|
||
**浏览器预留键(硬限制,无法拦截)**:`Ctrl-W`(关标签)、`Ctrl-T`(新标签)、`Ctrl-N`(新窗口)从 Chrome 4 起被浏览器保留,`preventDefault()` 对它们无效,浏览器在 `keydown` 到达页面前就消费了。这是所有网页终端的共同限制(xterm.js 同样如此),只能在浏览器之外解决(系统全局快捷键 / 扩展 / Electron 包装)。readline 退格删词可用 `Alt+Backspace` 替代 `Ctrl-W`。
|
||
4. **测试**:`key_to_bytes_table` 单测覆盖整套键表,23 个 terminal 测试全过、clippy 干净。
|
||
|
||
**有意调整的行为**(出于浏览器终端的可用性,与原生 xterm 不同):
|
||
- `Ctrl-C` (0x03) → 无选区时发 SIGINT 中断前台进程;**有选区时优先复制**,不发送 0x03。
|
||
- `Ctrl-Z` (0x1a) 与 `Ctrl-\` (0x1c) → 被前端过滤为 `None`,不发送到 PTY。原因是这两个键在默认 `stty` 下会触发 SIGTSTP / SIGQUIT,在浏览器标签页里极易误触导致前台进程被挂起或杀掉;需要这两个信号的用户可用 shell 内建命令(如 `kill -STOP` / `kill -QUIT`)替代。
|
||
|
||
---
|
||
|
||
### P2 — 鼠标上报(mouse reporting) ✅ 已完成
|
||
|
||
**现状**:已实现并验证。`vim`/`htop`/`btm`/`tmux` 的鼠标交互、tmux 选区、vim 滚轮均可以工作。
|
||
|
||
**已实现**:
|
||
1. **vt100 mouse-mode accessors**(`core.rs::mouse_protocol_mode` / `mouse_protocol_encoding`)—— 暴露 `None/Press/PressRelease/ButtonMotion/AnyMotion` 和 `Default/Utf8/Sgr`。
|
||
2. **事件监听与坐标换算**(`component.rs`)
|
||
- `on:mousedown` / `on:mouseup` / `on:mousemove` 复用现有的 DOM 事件。
|
||
- `pixel_to_grid` 把 `(clientX, clientY)` 换算成 0-based 可见网格 `(row, col)`(注意:与选区用的 `point_to_cell` 不同,这里**不加 scroll_top**,因为鼠标报告坐标是相对于应用正在绘制的当前屏幕,而非 scrollback 缓冲区)。
|
||
3. **编码**(`encode_mouse_report` 纯函数)
|
||
- **SGR(1006)**:`ESC[<b;col;row;M`(按下/移动)/ `ESC[<b;col;row;m`(释放)。ASCII-only,任意屏幕尺寸都正确。
|
||
- **Default / UTF-8**:`ESC[M` + 三字节(各 +32)。坐标 ≤94 时完全正确并 ASCII-safe;更大坐标被钳位到 94(大多数终端 likewise;现代应用都走 SGR)。
|
||
- **滚轮**:垂直/水平滚轮映射为按钮 64/65/66/67 的 `Press` 事件。
|
||
4. **模式仲裁**
|
||
- 应用开启 mouse reporting(`mode != None`)时,鼠标事件优先发给应用;否则走本地选区。
|
||
- 按住 **Shift** 点击可绕过鼠标上报、强制本地选区(xterm 行为)。
|
||
- 鼠标移动按网格单元去重,避免每像素都发报告。
|
||
5. **测试**:`encode_mouse_report`、`pixel_to_grid`、`with_mouse_modifiers`、`mouse_button_base` 均有单测;23 个 terminal 测试全过,clippy 干净。
|
||
|
||
**已知限制**:
|
||
- 主屏幕(非 alt-screen)滚动后,鼠标报告坐标仍相对于可见网格顶部;因为开启鼠标上报的应用通常运行在 alt-screen,这覆盖主要使用场景。
|
||
- 与浏览器 `Ctrl+W/T/N` 类似,浏览器可能保留某些鼠标手势(如右键菜单、拖拽文本),这些在网页终端内无法 100% 拦截。应用内鼠标操作应正常。
|
||
|
||
---
|
||
|
||
### P3 — 中文 / IME 输入 ✅ 已完成
|
||
|
||
**现状**:已实现。拼音、日文、韩文等 IME 组合输入可以正常工作;普通英文/数字输入也迁到了 `textarea` 的 `input` 事件,确保和 IME 走同一条路径。
|
||
|
||
**已实现**:
|
||
1. **隐藏 `<textarea>` IME 载体**(`component.rs` view)
|
||
- 绝对定位、透明、无边框、大小跟随终端光标单元格。
|
||
- 通过 `tabindex="0"` 成为焦点目标;`terminal-screen` 改用 `:focus-within` 显示焦点环。
|
||
2. **事件模型迁移**
|
||
- `on:input`:普通字符/数字/空格输入,读取 textarea value 后清空并发送。
|
||
- `on:compositionstart` / `on:compositionend`:IME 组合期间不发字节,`compositionend` 时发送最终文本。
|
||
- `on:keydown`:仍处理方向键、Enter、Esc、Backspace、F 键、Ctrl 组合等**非打印键**。
|
||
- `on:paste`:从 `terminal-screen` 移到 textarea,确保 focus 在 textarea 时右键/菜单粘贴也能触发。
|
||
3. **焦点管理**
|
||
- 点击 terminal-screen、WebSocket 连接成功、窗口 resize 后都聚焦 IME textarea。
|
||
4. **测试**:新增 `is_regular_text_input_detects_printable_keys` 单测,确保打印字符被正确交给 textarea;24 个 terminal 测试全过,clippy 干净。
|
||
|
||
**已知限制**:
|
||
- 主屏幕 scrollback 滚动后,光标可能位于可见区域下方,IME 候选框位置会对应到光标逻辑位置(可能偏下)。IME 主要用于当前输入, scrolled-back 场景输入较少见。
|
||
- 目前不支持 textarea 自动滚动延伸选区(和 P0 同一限制)。
|
||
|
||
---
|
||
|
||
### P4 — Canvas / WebGL 渲染器(性能目标)✅ 已完成
|
||
|
||
**现状**:Canvas 2D 已作为默认渲染器;WebGL glyph atlas / per-cell quad 与 DOM renderer 保留为可切换路径。长输出、输入后 live/history 稳定性、IME textarea buffer 坐标、DPR backing store、虚拟滚动 sizer、selection overlay、cursor overlay 均已验证。2026-06-24 benchmark 显示 Canvas 2D 在当前实现下优于 WebGL,因此默认策略先以 Canvas 为准。
|
||
|
||
**已实现**:
|
||
1. **Canvas 2D 默认渲染器 + WebGL 可选路径** ✅
|
||
- `Renderer::Canvas` 默认,`Renderer::WebGl` / `Renderer::Dom` 作为可切换回归路径。
|
||
- WebGL2 初始化 shader/program/vertex buffer/atlas texture/fallback texture。
|
||
- `GlyphAtlas` 按 grapheme + bold/italic + cell width 缓存字形;ASCII、CJK wide 字符、组合字符按 `unicode-segmentation` / `unicode-width` 拆成 cell glyph。
|
||
- 文本通过 per-cell quad 批量绘制,颜色作为 vertex attribute;小字号不再走整屏纹理二次采样。
|
||
- 背景、underline、cursor 背景在 WebGL 正常路径中直接绘制为纯色 quad;cursor 前景文字仍进 atlas。
|
||
- atlas 溢出或遇到不可拆分复杂段时自动回退整屏 Canvas texture,避免真实终端内容白屏。
|
||
- WebGL2 不可用时自动落回 Canvas 2D。
|
||
2. **Canvas 2D 渲染器** ✅
|
||
- 保留现有 `TerminalCore` 数据模型,渲染层切换为 Canvas。
|
||
- 一屏/一窗口 `fillText` / `fillRect` 绘制,避免每个 segment 都生成 DOM 节点。
|
||
- DPR backing store 与 CSS 尺寸分离,避免高 DPI 模糊。
|
||
- `CanvasRenderPlan` 统一计算 backing store、CSS size、canvas top、content offset。
|
||
- `CanvasDomState` 缓存 DOM 尺寸状态,避免每帧无意义重设 backing store。
|
||
- 保留 `terminal-virtual-sizer` 维持完整 scroll geometry,长 `cat` 后 live 模式稳定贴底。
|
||
- selection overlay 保持 DOM 绝对定位,canvas 只负责文本/背景/光标。
|
||
- IME textarea 改用 buffer 坐标定位,长 scrollback 后输入不会触发 live/history 抖动。
|
||
3. **测试** ✅
|
||
- `canvas_render_plan`、`canvas_fill_style`、`terminal_font`、`canvas_viewport_top_px`、`ime_cursor_cell`、`webgl_quad_vertices`、`webgl_shader_sources`、`GlyphAtlas`、`split_segment_glyphs`、`push_quad` 均有单测。
|
||
- `cargo test -p app --lib terminal::` 当前 38 个 terminal 测试通过。
|
||
|
||
**后续深化(进入新一轮 xterm.js 对标计划)**:
|
||
1. WebGL context loss/recovery 与 atlas 重建。
|
||
2. 渲染性能基准:固定脚本覆盖 10k/50k 行输出、200×80、240×100、持续输出 FPS / frame time。
|
||
3. 浏览器截图 + canvas/WebGL pixel smoke test 固化到工程化脚本。
|
||
4. emoji / ZWJ / variation selector / ligature 的 advanced shaping 策略。
|
||
|
||
---
|
||
|
||
### P5 — xterm.js API / 配置面(新一轮)✅ 已完成
|
||
|
||
**目标**:把当前 `TerminalPanel` 从页面内组件推进成可复用终端组件。
|
||
|
||
**已实现**:
|
||
1. `TerminalOptions` ✅
|
||
- cols/rows、scrollback、renderer、font family/size/line-height、theme、cursor style/blink、title/subtitle、show_header。
|
||
- 支持 runtime options signal 更新;字体、行高、theme 同步到 DOM / Canvas / WebGL glyph atlas / 虚拟滚动计算。
|
||
- `TerminalCore::new_with_scrollback` 让 scrollback 容量进入 parser 层,而不只是 UI 参数。
|
||
2. `TerminalHandle` ✅
|
||
- `write`、`paste_text`、`clear`、`focus`、`resize`、`scroll_to_bottom`、`copy_selection`。
|
||
3. 事件回调 ✅
|
||
- `on_data`、`on_resize`、`on_selection_change`、`on_title_change`、`on_render`。
|
||
4. 测试 ✅
|
||
- 新增 options 解析 / CSS 变量 / 行高参数化 / cursor shape / cursor glyph color 相关单测。
|
||
- `cargo test -p app --lib terminal::` 当前 41 个 terminal 测试通过。
|
||
5. 第一轮模块拆分 ✅
|
||
- `keyboard.rs`:paste normalization、key mapping、printable key 判断。
|
||
- `mouse.rs`:wheel rows、mouse report 编码、visible grid mapping。
|
||
- `selection.rs`:selection model、selection rect 计算。
|
||
- `ime.rs`:IME cursor buffer 坐标和 hidden textarea helpers。
|
||
|
||
**后续进入 P6**:
|
||
1. `TerminalHandle` 增强:`fit()`、serialize/search/link addon 接口。
|
||
2. 继续把 WebGL / Canvas renderer 从 `component.rs` 拆成专门模块。
|
||
|
||
### P6 — xterm.js addon parity(搜索 / 链接 / 序列化 / fit)
|
||
|
||
**当前进度**:
|
||
1. **Fit addon 等价能力**
|
||
- ✅ `TerminalHandle::fit()`,根据容器内容区重新测量 rows/cols,更新 core viewport 并同步 PTY resize。
|
||
2. **Search**
|
||
- ✅ `TerminalHandle::search_next/search_previous/clear_search`。
|
||
- ✅ literal search 跨 `history + screen`,支持 wrap,搜索结果通过 DOM overlay 高亮,WebGL/Canvas/DOM renderer 共用。
|
||
3. **Serialize**
|
||
- ✅ `TerminalHandle::serialize_text()` 与 `TerminalCore::buffer_text()`,导出当前 retained buffer 文本。
|
||
- ✅ `TerminalHandle::serialize_ansi()` 与 `TerminalCore::buffer_ansi()`,按当前 row/style 模型导出 ANSI-styled snapshot。
|
||
4. **Web links / OSC 8**
|
||
- ✅ URL 自动识别、点击打开,WebGL/Canvas/DOM renderer 共用链接 hitbox overlay。
|
||
- ✅ OSC 8 hyperlink 解析:支持 BEL / ST 终止,使用 cell-grid sidecar 记录链接,覆盖写入、常见 CSI 光标移动、清行/清屏、滚屏进 history 后仍能正确映射到链接 hitbox overlay。
|
||
- 后续可继续扩展非常规控制序列覆盖面,但 P6 addon parity 的核心能力已完成。
|
||
|
||
### P7 — Accessibility / 可访问性 ✅ 已完成
|
||
|
||
**已实现**:
|
||
1. **Screen reader buffer / aria-live** ✅
|
||
- 隐藏 `div[aria-live="polite"][role="status"]` 实时推送可见终端文本。
|
||
- 提取可见行文本(去尾空、过滤空行),每 500ms 防抖更新,避免大量输出时淹没屏幕阅读器。
|
||
- `role="application"` + `aria-label="Terminal"` + `aria-roledescription="terminal emulator"` 标注终端区域。
|
||
2. **Keyboard-only selection / copy** ✅(已在 P0 完成)
|
||
- `Ctrl-Shift-C` 复制;选区存在时 `Ctrl-C` 优先复制。
|
||
- 所有键盘处理通过 IME textarea 的 `on:keydown` 统一入口。
|
||
3. **High contrast / minimum contrast ratio** ✅
|
||
- 监听 `prefers-contrast: more` 媒体查询,自动切换 `Canvas` / `CanvasText` 系统色。
|
||
- CSS `@media (prefers-contrast: more)` 覆盖终端前景/背景/边框。
|
||
4. **Reduced motion 与焦点可见性** ✅
|
||
- 监听 `prefers-reduced-motion: reduce` 媒体查询,自动禁用光标闪烁动画和过渡。
|
||
- JS 侧同步设置 `--terminal-reduce-motion:reduce` CSS 变量,Effect 内联动 `prefers_reduced_motion` 信号。
|
||
- `terminal-screen:focus-within` 已有可见焦点环(`box-shadow: 0 0 0 3px rgb(56 189 248 / 15%)`)。
|
||
|
||
### P8 — Unicode / 字形精度
|
||
|
||
**计划实现**:
|
||
1. emoji / ZWJ / variation selector 的 grapheme cluster 宽度策略。
|
||
2. CJK ambiguous width 配置。
|
||
3. ligature 可选支持。
|
||
4. fallback font metrics 校准。
|
||
|
||
### P9 — WebGL Renderer 工程化深化
|
||
|
||
**计划实现**:
|
||
1. **WebGL context loss/recovery 与 atlas 重建** ✅
|
||
- 监听 `webglcontextlost` / `webglcontextrestored`,lost 时阻止默认行为并清空 `WebGlRendererState`。
|
||
- context lost 或 WebGL 初始化失败时显示独立 Canvas 2D fallback surface,避免同一 canvas 上 WebGL lost 后无法获取 2D context 导致白屏。
|
||
- restore 后清空旧 state 并触发重绘,下一帧重建 shader/program/buffer/texture/glyph atlas。
|
||
- context lifecycle listener 由 `WebGlContextLifecycle` 持有,组件卸载或 renderer 切换时自动移除。
|
||
2. **渲染性能 benchmark 和自动截图 / pixel smoke test** ✅
|
||
- `scripts/terminal-bench.mjs` 连接已启动的 `/rustui/terminal`,不负责启动/关闭服务。
|
||
- 固定覆盖 `200x80`、`240x100` viewport,支持 `--lines`、`--viewport`、`--output` 参数。
|
||
- 记录行输出耗时、rows/sec、requestAnimationFrame frame time 摘要、`window.__terminalBench` render probe。
|
||
- 截图 smoke 默认走 Playwright screenshot PNG 非空/熵检查;Canvas 2D fallback 可读时额外做像素采样。
|
||
3. **Canvas 2D / WebGL runtime switch** ✅
|
||
- `/rustui/terminal` 页面提供 WebGL / Canvas / DOM runtime 切换。
|
||
- 支持 `?renderer=webgl|canvas|dom` 初始化,便于浏览器回归和 benchmark 固定模式。
|
||
- `scripts/terminal-bench.mjs --renderer webgl|canvas|dom` 复用同一入口跑对比,并将截图按 renderer 命名。
|
||
4. **与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer** ✅
|
||
- 2026-06-24 基线,10k 行输出:
|
||
- `200x80`:WebGL `155.27 rows/sec`,frame p95 `283.3ms`;Canvas `311.67 rows/sec`,frame p95 `116.7ms`。
|
||
- `240x100`:WebGL `92.48 rows/sec`,frame p95 `399.9ms`;Canvas `157.65 rows/sec`,frame p95 `116.8ms`。
|
||
- 两种 renderer 的 pixel smoke 均通过;Canvas 可直接读取 canvas 像素,WebGL 走 screenshot smoke。
|
||
- 当前策略:默认 Canvas;WebGL 继续通过页面切换、`?renderer=webgl` 和 `--renderer webgl` 保留优化回归入口。
|
||
5. **WebGL renderer profiling / 慢路径拆解** ✅
|
||
- WebGL render path 记录阶段耗时:DOM/canvas 同步、backdrop 绘制、glyph collect、backdrop texture pass、glyph atlas、atlas upload、vertex build/upload、draw call。
|
||
- 记录计数:cells、atlas entries、atlas insertions/resets、full-canvas fallback 次数、glyph draw failed 次数。
|
||
- `window.__terminalBench.webglFrames` 暴露每帧采样,`scripts/terminal-bench.mjs` 汇总为 `rendererStats`,用于判断慢点是整屏 backdrop texture、atlas、vertex buffer,还是 fallback。
|
||
6. **WebGL backdrop texture pass 拆除** ✅
|
||
- 正常 WebGL 路径不再每帧把 Canvas backdrop 上传为整屏 texture;背景、underline、cursor 背景改为 1×1 白色纹理 + 纯色 quad 批量绘制。
|
||
- `backdropTextureMs` 只在复杂 glyph / atlas 失败等 full-canvas fallback 路径产生,用于继续观察 fallback 是否被真实内容触发。
|
||
- 页面切换、`?renderer=webgl` 和 `--renderer webgl` 保留,下一轮基准可直接对比这次优化后 WebGL 是否追近 Canvas。
|
||
7. **WebGL glyph collect cache** ✅
|
||
- WebGL renderer state 增加可见行 glyph 拆分缓存,相同行内容 / 样式 / theme 在滚动和连续输出时复用 grapheme 拆分结果。
|
||
- `GlyphKey.text` 改为 `Rc<str>`,缓存命中后 materialize 行 glyph cell 时避免反复复制 glyph 字符串。
|
||
- benchmark `rendererStats` 新增 `rowCacheHits` / `rowCacheMisses`,用于判断 `collectMs` 是否被 cache 命中率拉低。
|
||
8. **WebGL row vertex template cache** ✅
|
||
- atlas 覆盖后按行缓存 glyph vertex template,后续帧只按当前 row y 偏移 materialize,减少每帧 atlas lookup / UV 计算 / quad 构建。
|
||
- benchmark `rendererStats` 新增 `vertexCacheHits` / `vertexCacheMisses`,用于判断 `vertexBuildMs` 是否主要来自 cache miss。
|
||
|
||
---
|
||
|
||
## 4. 建议推进顺序
|
||
|
||
```
|
||
P0 复制粘贴(先粘贴→可见区复制) ← 最先,体验提升最大
|
||
↓
|
||
P1 完整功能键 + application-cursor
|
||
↓
|
||
P2 鼠标上报
|
||
↓
|
||
P3 中文 IME(放在功能键之后,避免输入逻辑冲突)
|
||
↓
|
||
P4 Canvas / WebGL 渲染器(满足大终端 + 高刷新率性能目标)
|
||
↓
|
||
P5 API / 配置面
|
||
↓
|
||
P6 addon parity(fit/search/serialize/web-links)
|
||
↓
|
||
P7 accessibility
|
||
↓
|
||
P8 Unicode / 字形精度
|
||
↓
|
||
P9 WebGL renderer 工程化深化
|
||
```
|
||
|
||
每一项都应延续现有做法:**纯逻辑抽成可单测的函数**(如 `map_key_to_terminal_input`、`wheel_delta_to_rows`、`virtual_window`),用 `cargo test -p app --lib terminal::` 锁定,浏览器只做交互验证。
|