24跨端统一架构

一套代码跑在车端、笔记本和服务器上,这件事该不该做、做到哪一层、什么时候不该做。 这一页的顺序是先质疑前提,再给分层。

先问前提 压到压不动 抽象的成本是永久的

一、先问一句:这是通用底座还是专用工具

同一个问题,两种答案

如果做的是通用可视化底座,要跑在车、笔记本、服务器上,还要允许插件定制, 那确实需要一套分层。

如果做的是专用标注工具,功能上根本用不着跨端抽象。 为一个不存在的需求设计抽象,是纯成本。

所以第一句话应该是问前提,而不是直接给架构。

二、分层:差异只在最下面两层

业务         数据模型 · 算法 · 传播 · 渲染逻辑        统一
──────────────────────────────────────────────
图形中间层    Skia · Dawn · WebGPU 实现               统一
──────────────────────────────────────────────
图形后端      Vulkan · Metal · D3D11 · GL              各端不同
──────────────────────────────────────────────
shell        窗口 · 事件 · 打包 · 离屏目标             各端不同

看一个真实的例子,浏览器内核。

层跨平台吗例子
引擎✅ 统一渲染引擎、JS 引擎
图形中间层✅ 统一二维绘图库、WebGPU 实现
图形后端❌ 必须不同Vulkan、Metal、D3D
平台 shell❌ 必须不同窗口、事件循环、打包、权限
"不一样"的只有最下面两层

上面全是统一的。所以"最终只剩下 shell"这句话要修一个字: 剩下来的不是一层,是两层。

一层是"语义相同但实现必须不同"的后端,一层是"语义本来就不同"的 shell。

三、图形栈不是插件,是基础库

这个区分很关键,而且很容易做错。

插件基础库
什么时候结合运行时装载编译期链接
目的可替换同一份实现
图形栈要哪个❌ 不需要替换✅ 要同一份
把图形栈做成插件,反而鼓励各端各写一份

插件化的默认假设是"每端都会有自己的实现"。而你要的恰恰是"只有一份"。

这条路的一个现成做法是:同一套底层封装,一份编成 wasm 给浏览器, 一份编成动态库给原生应用。底下还是 Vulkan、Metal、D3D 各不相同,但上面是同一份代码。

而且 CPU 那部分可以做到逐位一致

部分一致性
CPU 逻辑,解码、传播、插值、几何计算可以逐位一致
GPU 渲染,后端不同⚠️ 只能容差内一致

因为把同一份确定性代码编成 wasm 和编成原生,浮点语义是确定的, 不依赖编译器的优化选择。而 GPU 不行:缓存策略、时序、精度都可能不同。

所以"统一"的证明方式也变了

不是"代码一样",是"输出可复现地相同"。

CPU 那部分可以断言相等。GPU 那部分只能在容差内一致,而且要有回归测试守着。

四、判据:把差异往下压,压到压不动为止

"业务上提、环境下沉"这句话没有判据,不好用。换一个说法会好用得多。

压不动的判据

这一层的差异,会不会让结果不一样?

不会            →  还能往下压,继续统一
会,语义相同    →  是后端层
会,语义就不同  →  是 shell 层

"压不动"这个说法比"下沉上提"好用,因为它有方向、有终点,而且每一步都能问一句"结果会不会变"。

还要分清「统一的是代码,不是运行配置」

允许各端不同不允许不同
分辨率、采样率、帧率坐标系约定
细节层级、抗锯齿、阴影投影公式、单位
缓存大小数据模型、算法

判据是一句话:降配之后,人从画面上能得出的结论会不会变?

从"精确在这儿"变成"精度低一点",可以接受。从"在这儿"变成"在那儿",不行,那是 bug。

五、什么时候这套原则会失效

#失效情形具体长什么样
①被最弱的端拖累为了跑车端把管线压到极简,笔记本上白白浪费算力
②统一的代价大于分裂只有一个端需要的功能塞进公共层,另外两端背了用不上的依赖
③差异吃不掉服务器上根本没有"窗口"这个概念,适配器只能返回空
④公共层改动半径太大一个端的小需求,要动所有端的核心代码
⑤跨端不是业务需求只是一句技术口号,那整套抽象都是纯成本

第一条的解法不是分裂,是统一算法、分立预算。 算法必须一样,否则结果不同;预算可以各端不同,否则被最弱端拖死。

六、怎么发现它已经开始烂

最好的信号:用户开始绕

不看"抽象设计得好不好",看调用方有没有在写 workaround。

绕行是有因,公共层里冒出一堆平台判断才是果。等你在代码里看见那些判断,说明已经烂了一阵了。

信号说明什么
绕行次数多抽象没覆盖到
绕行活过了两个迭代它不是临时方案,是缺失的接口
三个端都绕了同一处那一处抽象一定错了
绕完之后结果不一致最致命。统一的目的一开始就是结果一致

更隐蔽的一种:绕行被洗白了

第一版   if (platform === 'car') { 特殊处理 }     明显,评审会拦住
第二版   renderDetailed: boolean                  藏起来,像个正常参数
第三版   RenderOptions { detailed, lowLatency }   已经没人记得它为什么存在

区分好配置和洗白的开关,有个简单判据:这个参数名能不能直接拿去跟产品讨论。

能问"你要多高分辨率",那它是好配置。只能问"你要不要走 carPath",那它已经烂了。

七、这一页带走什么

跨端统一是产品形态决定的,不是技术美德。专用工具不需要它。

需要的时候,差异压到最下面两层就够了:图形后端和 shell。上面全是统一的, 而且图形栈应该是基础库,不是插件。

而抽象的成本是永久的,收益是当时的。所以判断它有没有烂,别看设计, 看调用方有没有在绕。

← 上一页:ROS 与实时接入 ·  回导读 →
京ICP备19013239号