viewer

rust2genshin_viewer 是一个 GPUI 写的节点图浏览器。

作用

可视化地看节点图:打开一个编译产物,把里面的节点图画出来;看每一步优化把它改成了什么样;看函数的调用约定。它是调试工具,用来回答「编译器把这段 Rust 翻成了什么」—— 尤其是优化器那一步到底动了什么。它读的是未 lower 的中间态 IR,为什么这么设计见viewer 的设计。

用法

用 viewer 打开编译的最终产物 .rlib 或中间产物 .ogia,在左栏双击一个节点图打开它,右栏就是那张图的调用约定,以及各种优化操作。本仓库的 .ogia 产出端就是 build-demo:
cargo run -p build-demo --bin beyond-server
产物 target/genshin-unknown-beyond-server/release/librust2genshin_demo.rlib 里带一个 rust2genshin_demo.ogia entry(ar t 可以列出来)。把这个 rlib 拖进 viewer 即可。正常构建不会为它生成独立文件 —— .ogia 只以 entry 形式存在于 rlib 里。

命令行

cargo run -p rust2genshin-viewer -- <路径>
必须走 cargo run,不能直接双击 target/debug/rust2genshin_viewer.exe —— 它依赖 rustc_driver-*.dll,那个 DLL 在 $(rustc --print sysroot)/lib/rustlib/<host>/lib/ 下,cargo 会把这个目录加进 DLL 搜索路径,直接跑不会。命令行有四种用法:排查时先 --check。 它能区分「文件里没有图」和「图里没有节点」这两种看起来一样的现象。后者是正常的 —— librust2genshin_lib-*.rlib 里就有一张节点数为 0 的图(rust_begin_unwind),viewer 不该把它当错误。拖放之外还要有命令行入口,也因为拖放能不能用是平台的问题 —— 先 --check 确认文件本身对不对,再看窗口。

优化

右栏下半的按钮,各管一件事。拆开是为了能看清是哪一步把图改坏的:
按钮做什么
自动整理按 core 的布局算法重排坐标。拖动结果会丢,可以 Ctrl+Z 撤回。
一键优化只跑消除规则到不动点,不含展开 Fn 节点。
链接全部把所有 IrNodeId::Fn 节点展开成内部图(右键「link」的批量版)。
循环改写跑 compile::loops::lower_loops —— 把控制流里的环换成 native 的「有限循环」节点。
lowercore 的完整降级流程(Optimizer::lower_ir):展开 Fn → 优化 → 改写环 → 展开列表 → 类型降到 wire 层。
此外每条消除规则各有一个按钮(标签省掉了 eliminate_ 前缀),点一次 = 这条规则在整张图上过一遍。想逐步观察是哪一步改坏的,就一条条点;想一步到位点「一键优化」。一次点击只留一个撤销存档 —— 一次 Ctrl+Z 回到点击前。「lower」之后前三个按钮和它自己一起变灰 —— 图已经是产物形态,对它们没有意义了。这个状态是显式记下来的,不从图上推:lower 之后节点确实全成了 IrNodeId::Native,但反过来不成立 —— 点「一键优化」也可能把一张小图消到只剩一两个原生节点(真实产物里 add_n、borrow_read、pair_sum 就是这样),那种图并没有 lower 过,按钮必须还能点。标记跟着存档走,所以 Ctrl+Z 退回去之后按钮会自动恢复。

调用约定

打开一张复合节点图(一个函数)时,右栏上半会多出一个视图,回答这个问题:「调用方看到的那个节点,和这个函数现在长什么样,差在哪?」主图没有这套东西(没人调用它),不显示。视图是内外两层:外层的每个数据流出引脚按当前状态连成三种形态之一:第三种有个坑:lower 里的 compact_value_exports(core/src/compile/optimize.rs:617)会把没人读的值出口整根摘掉 —— 一个「直接返回常数」的函数正是这样,出口没有节点读,值只留在 decl.proxies_out 里。于是内层连那个出引脚都不剩,常数一度跟着整个消失。现在 FnInterface::orphan_rets(viewer/src/doc.rs)把那张表读出来,这种常数照样画,只是 conv::Constant::inner 为 None(只影响有没有连线,不影响位置)。控制流引脚同理:当前函数体有控制流就连到内层,没有就直接从外层控制入横穿到控制出。内层的位置每张图各记一份,切走再切回来还在原处。

交互

几处不看代码就猜不出来的行为:左栏双击才打开一张图,单击只是选中那一行 —— 和 IDE 的文件树一致。已开在某个页签里的图,双击会切过去而不是再开一页。撤销 / 重做 / 关页签只在标签页区域有焦点时生效(画布或右栏)。焦点在左栏搜索框里时,Ctrl+Z 该是文本编辑的撤销,不该去撤节点图。实现上是一套 gpui key context —— 画布和右栏各挂一次,任一持有焦点即满足;打开页签时会把焦点交给画布,所以打开后立刻能按快捷键。每个页签有自己一整套画布状态 —— 选中了哪些节点、右键菜单开着没有、正在拖谁、视野在哪、「调用约定」内层节点拖到了哪儿。切走再切回来什么都没变,不是重置成同一个画布。关掉最后一个页签不会关窗口,也不会关掉文件。撤销 / 重做 / 关页签都只作用于当前那张图:每张图各有一份撤销栈和重做栈,切图不清栈,切回来还能接着撤。

快捷键

快捷键作用生效范围
Ctrl+Z / Ctrl+Y撤销 / 重做只在标签页区域(画布或右栏)有焦点时
Ctrl+W关闭当前页签同上
Alt+← / Alt+→后退 / 前进全局
Ctrl+V / Esc粘贴到搜索框 / 清空搜索框左栏搜索框有焦点时
Shift+左键加选、取消选画布
Ctrl+左键跳转到定义画布
撤销 / 重做没有按钮,只有快捷键。做了新操作之后重做栈清空(标准语义)。

前进 / 后退

切页签、在左栏切图、跳转到定义,都会记一条位置历史。前进/后退回到的是图 + 视野(平移与缩放也一起还原),不只是哪张图。两个入口:和浏览器的语义一致:做了新导航之后前进栈清空。前进/后退不会改写当前页签的内容:目标图已经开在某个页签里就切过去,原来那一页如果已关闭才新开一页 —— 和左栏双击是同一条规则。「跳转到定义」也走这条规则。

布局

节点的初始坐标来自 core 的 node::layout —— 和 .gia 编码前跑的是同一套,所以 viewer 看到的排列就是产物里的排列,两边不会各排各的。节点宽度分两处:布局按标签估算宽度决定列间距(宁可估宽 —— 估窄了节点会压到下一列上),渲染时用排版引擎实测文字宽度让盒子贴着文字撑开。实测值会写回几何,所以引脚、连线、Export 桩都和盒子对齐。但布局是加载里最贵的一步(实测 57 张图 3.1 秒,占整个加载的 97%;展平只要 7.9 毫秒)。所以它推迟到第一次打开那张图时才算。切图不重算 —— 拖动过、整理过、优化过的位置,切走再切回来都还在。坐标只存在一处(Doc 里每个节点的 position)。拖动直接改它,渲染和命中测试都从它读 —— 没有“渲染用这份、连线用那份”的平行副本,也就没有两边对不上的可能。

健壮性

输入是用户随手拖进来的任意 .ogia / .rlib,所以渲染和交互全程不 panic:所有下标访问都走 get,取不到就按原点 / 空值兜底。优化规则里有大量 unwrap(撞上畸形节点就会炸),整段包在 catch_unwind 里 —— 失败时用点击前的字节把图整体回滚,状态栏说明原因,这一跳不进撤销栈。一处已知残留:link / lower 会改动 Linker 的持久状态(已展开函数的缓存 / 资产),panic 时图能回滚,但那部分收不回来。这条路径就是 core 自己在跑的展开逻辑,panic 意味着 core 出错。

构建

它不在工作区 default-members 里(原因见设计):
cargo build -p rust2genshin-viewer
cargo test -p rust2genshin-viewer