一个面向 LLM 对话的高性能渲染引擎:把大模型流式吐出的对话事件,渲染成一块流畅、 带丰富动效、可无限缩放平移的画布。用 Rust 写、编译成 WebAssembly、打包成 npm 包, React / Vue 可直接引入。
🎬 在线官网(官网本身就是最大 demo):ohbonsai.github.io/infinite-chat —— 首页是全屏单 canvas 幕式播放器(引擎放映自己的宣传片:8 幕含字阵绽放高潮——整段对话飞起聚成 一朵花、花瓣飘落、指针可拨可点绽放,逐字皆 SDF 图元的编队;自动播 + 可接管 ←/→/进度点/滚轮/触摸; 无 GPU 走截图幻灯降级) · 完整会话重放 · Markdown 全类型 + playground · 图标画廊。部署与本地预演见 DEPLOY-pages;降级幻灯资产用
node scripts/gen-pages-assets.mjs本地生成入库。
LLM 改变了一切。而人与 LLM 之间的主入口,就是 chat。 不是 IDE 插件、不是一堆表单和按钮——是一条对话。对话就是新的命令行、新的操作系统入口。
可现在的 chat 界面,坦白说基本都是垃圾:逼你一个任务开一个新对话、长会话越用越卡、富内容(代码 / 表格 / 公式 / 图)渲染得又丑又糙、流式吐字像打嗝、动效廉价或干脆没有。它们配不上背后那个正在改变世界的模型。
所以我们重做一个——一条永不结束、无比流畅、效果上限拉满的对话,配得上 LLM 时代的主入口。这不是又一个聊天框,是把 chat 当成第一公民界面来做。
核心信念:做好一个实时流式 + 高动效 + 弱网可丢消息的 AI 对话界面,本质上是在做一个 游戏引擎——所以我们就用游戏引擎的手法做。详见 spec/decision/0000-overview。
痛点:现在的 AI 对话工具逼你"一个任务开一个新对话"——做完一件事就新建会话,历史散落各处;可真实使用里一个会话经常 100+ 轮还停不下来,而"为每个任务不停建新对话"的体验很糟。
我要的:像微信聊天那样,和某个"对象"(某个 agent / 项目)的所有历史永远在一条会话里,一直往下滚——超长会话 / 无限会话(infinite session)。这是本项目最强的应用场景。
为什么这逼出一个游戏引擎:一条 100+ 轮、上万行、还在流式增长的会话,用普通 DOM / react 渲染会越用越卡(DOM 节点爆炸、reflow、内存只涨不降)。所以才用游戏引擎手法——GPU 实例化 + 块冻结(settled 不重排)+ 视口裁剪(只画可见)+ 无边画布——让会话无限长也始终丝滑,fps/内存只与"可见的一屏"成正比,与历史总量无关。
一句话:infinite-chat 的存在就是为了承载 infinite session——其余的流式丝滑、SDF 文字、无边画布、极致规模(见 TODO2 C),本质都是为"一条永不结束的会话依然流畅好看"服务。
- 是:一个对话渲染引擎(画布 + 文字 + 嵌入块 + 动效 + 流式/容错状态机),以 可嵌入组件形态交付。前端框架只管外围控件(输入框、按钮、弹窗),对话画布交给本库。
- 不是:一个 markdown→HTML 组件(那是 react-markdown / Streamdown 的活)。普通体量、 纯 DOM 的聊天,用 DOM 方案更省;本引擎的价值在无边画布 + GPU 动效 + 规模下不卡 (见 0011 的适用边界)。
Rust 核心(流式/markdown 大脑) ──编译──▶ WebAssembly ──打包──▶ npm 包 ──▶ React / Vue 直接 import
- 浏览器 / wasm 是固定主战场。原生(Tauri 等)不作为当前目标约束。
- 图形层只有 wgpu 一个抽象:instance 开
BROWSER_WEBGPU | GL→ WebGPU 优先、WebGL2 自动兜底(同一份代码,已启用,待专测);Canvas2D 不做,极端无 GPU 交 a11y 的 DOM 镜像兜底。注意 WebGL2 无 compute,逐字 compute 特效为 WebGPU 专属(0003 §5、0011 §3.4)。
- 可嵌入组件:wasm 库,React / Vue 直接引入;前端框架无关,只暴露画布 + 少量配置。 → 因此包体要轻、依赖要省(这是否决重型文字栈/框架的根本原因)。
- 2D SDF 世界 + 无边画布:文字是 SDF 图元(非 DOM、非位图),与矩形/图片 quad 共用相机、视口裁剪、实例化;任意缩放清晰,支持 GPU shader 特效。
- 场景 = LLM 对话,FSM 驱动事件:对话事件流驱动一切;状态机贯穿回合收尾、标签区域、 嵌入块生命周期。
- content→layout→render 三层契约,且 layout/render 可替换(0001 §2.2)。
语义角色(
StyledSpan)进、像素/坐标出;换解析器/排版/渲染后端只动各自内部,契约不动。 这条是本项目所有"能换方案而不伤筋动骨"的根本。 - 效果是数据,不是分支(0002 §5.1); 插件 = 注册表项,不是代码分支,可热加载(0006 §7)。
- 流式正确性是一等公民:平滑器(蓄水池匀速吐字,做法同网游平滑远端玩家)、
块冻结(settled 块不重算,只动尾块)、remend(尾部主动补全防半截语法闪烁)、
GPU
spawn_time淡入(逐字动画零 CPU 参与)。这套"流式大脑"是 Rust 核心最不可替代的价值。 - FSM 驱动的不止"事件":回合收尾看门狗(soft/hard 超时,"忘了 idle"兜底,0005)、 标签区域(0006 §5)、嵌入块 Placeholder→Loading→Ready→Failed(0004 §7.3)。
- 容错 / 对账 / 可恢复:catch-up vs live 双模、
resync_from_snapshot、EventSource 自动重连、 幂等快照、确定性重放(0003)。刷新不丢历史、弱网不丢不错。 - 降级与无障碍:WebGPU→WebGL2→Canvas2D 兜底;canvas 对屏幕阅读器是黑盒, 作为可嵌入组件必须配一层 DOM 镜像(否则部分接入方不可用,常比性能更早成否决项)。
- 安全:模型输出、插件注入的标签一律当数据,绝不执行、不当真 HTML 解析; 未知标签默认原样显示绝不静默吞掉(0006 §4/§6)。
- 让浏览器干重活:浏览器解码图片、Canvas2D 做文字整形(→TinySDF 生成 SDF)、 SVG/mermaid 交浏览器光栅化;wasm 只持元数据。
- 自有引擎,不被框架锁定:借算法不借框架——用 TinySDF/ESDT(一个算法), 否决 AntV G / egui / cosmic-text / glyphon(0011 §2)。
- GPU-driven:实例化 + 视口裁剪 + 块冻结 → 规模下每帧成本平(对比 DOM 节点爆炸的根本动机)。
- 工程严谨可测 + 运行时可观测:测试期——确定性重放、proptest、naga shader 构建期校验、native 测试为铁律
(testing-and-benchmark);运行时——
?debug节流帧统计(fps/帧耗时/发射 glyph vs 总量/atlas 占用·淘汰), 因为性能退化多是运行时、数据相关、GPU/主线程的,测试期抓不到(见 TODO 可观测性)。 - 字体自带、放宽 BR5:打包自选字体(
@font-face供 Canvas2D 光栅),接受字形非系统字体 (即"追平 GitHub 结构/配色可,但字形一致与自带字体互斥",0009→0011)。
| 关切 | 选择 | 出处 |
|---|---|---|
| markdown 解析 | pulldown-cmark(经 vendored jcode-render-core),不手写 nom、不上 comrak | 0010 |
自定义语法(<thinking>/:::/@/角标) |
走标签层 segmenter + 注册表,不动解析器 | 0006 / 0010 |
| 文字渲染 | SDF 图元(移植 TinySDF/ESDT),逐字 compute/vertex/fragment 三层 | 0011 |
| 数据结构 | 命令日志→派生缓存、CPU 树/GPU 扁平网格双索引、定长瓦片 page-pool 图集、GPU-SDF/CPU-盒双表示 | 0011 §3.3 |
| 嵌入块(图片/mermaid) | 降格为异步纹理块,浏览器光栅化,wasm 只持元数据 | 0004 §7 / 0007 |
| 多实例同步 | 见 ADR | 0008 |
crates/
├── core/ # 流式/markdown 大脑:content(解析→StyledSpan)、store、fsm、app、frame —— 引擎无关、可测
├── render/ # WebGPU 渲染:atlas / scene / shaders(将从位图升级到 SDF,0011 退役清单)
└── wasm/ # wasm 绑定:ChatCanvas、transport、layout_bridge、glyph_bridge
web/ # 浏览器侧 harness + JS 桥(pretext-bridge / glyph-raster,0011 起让位给 SDF)
vendor/ # jcode-render-core(后端中立 markdown 文档模型)
spec/ # 设计文档:decision(ADR 0000–0011)、plan、research、architecture
想快速看这个引擎能做什么?生成一份单文件功能体检报告 —— plan28–38 这一大轮的全部 人可感知功能,逐项 e2e 捕获截图/视频 + 人话解释(九大项 / 44 小项),装订成一个 自包含 HTML,双击即看。
node test/feature-report.mjs # 全量:逐项捕获截图/视频 → 生成报告(约 8 分钟)
node test/feature-report.mjs --only cards # 增量:只重捕某大项或某小项(如 cards / card-diff)
node test/feature-report.mjs --report-only # 只用现有资产重出报告(几秒,不重新捕获)产物:test/results/feature-report.html(约 13 MB 单文件,所有图片/视频 base64 内嵌)。
报告本体与捕获资产都 gitignored(重跑即得,不入库);manifest / 生成器 / 捕获 spec 入库。
file:// 下部分浏览器禁内嵌视频,起个本地静态服务最省事:
cd test/results && python3 -m http.server 8899
# 浏览器打开 http://localhost:8899/feature-report.html(推荐 Chrome / Edge,webm 兼容最好)报告左侧是粘性目录(九大项 + 小项锚点),顶部总览标注 commit 与五层门状态,每个小项一张 卡片(标题 + 人话「你会看到什么」+ 出处 plan/ADR + 截图或可播放视频)。
- 报告即测试:每项捕获都带可见性断言,任一项失败即红、不出残缺报告;清单结构由
web/src/feature-manifest.test.ts锁进五层门,保清单不腐。 - 每个大 plan 收口后重跑一次,功能不腐、介绍常新。
- 单一真值源 =
test/feature-manifest.json(增删功能点、改解释都在这里)。
- Plan 2(F–J)已完成:快照/过滤、滚动/视口裁剪/块冻结、markdown 角色化、回合收尾、 弱网容错;46 个 native 测试。详见并经审核:spec/plan/plan2_progress。
- Plan 3 方向:画布化(SDF 文字图元 + 相机 + 空间索引)、input/选区/hit-test、 嵌入块、富 shader 特效、DOM 镜像无障碍。
设计决策按编号读:0000 总览 · 0001 架构契约 · 0002 事件管线/FSM · 0003 容错降级 · 0004 markdown 管线 · 0005 回合收尾 · 0006 标签层/扩展 · 0007 富媒体嵌入 · 0008 多实例同步 · 0009 文字渲染引擎 · 0010 markdown 解析策略 · 0011 SDF 文字图元 · 0012 调试器 GUI:HTML vs egui · 0013 数学(LaTeX)渲染策略 · 0014 表格两趟布局 · 0015 字形源解析与回退(Bitmap/TinySDF/MSDF) · 0016 streaming 形变渲染机制(past→current 双关键帧) · 0017 markdown 流式落地(提交前沿 + 保守预测/和解) · 0018 SDF 装饰/面板图元(参数化 shader 框 + 共享 storage buffer)