zjchina.net

Drogon · ROS 2 · 独立开发笔记

ENGINEERING NOTES/工程笔记

把渲染握回自己手里:zjchina 的三个身份层

2026 年 9 月 29 日。这几天 zjchina 从「一个用 Dioxus/Drogon 的站」长成了「一个有三个身份层的框架」。最下面是把契约钉死的 zjchina-core,中间是双端共用的 zjchina-ui,最上面——是我们决定自己造的那一层:zjchina-render,一个领域无关的自绘渲染内核。这篇文章交代这三层怎么长出来,以及为什么最后一步是「自己画」。

一、三层,各管一件事

框架的地基不是「代码写得多」,而是「同一份知识只有一处定义」。这个原则落到仓库里,就是三个 crate,依赖方向单向向内:

zjchina-render   —— L0 自绘内核(wgpu + winit + fontdue,领域无关)
        ↑ 未来真机设备端消费
zjchina-ui       —— L2 组件层(双端共用的页面/组件,SSR 与 CSR 同一棵树)
        ↑
zjchina-core     —— L2 中立契约层(serde DTO = Schema,禁 web-sys/axum)
  • zjchina-core 是所有「同一语义」的家:markdown 渲染、文章 HTML 增强、公开与管理两套共享 DTO、事件台账、决策日志、路由表与三轨道(Content/App/Shell)。它不碰 web-sys、不碰 axum,所以能同时编译到 wasm32(CSR)和 native(SSR)。你往这里放一行,双端就少一次「各写一份然后祈祷一致」。
  • zjchina-ui 是组件层:导航、首页的分栏、文章页、决策页、404、标签,SSR 侧车和 CSR 薄壳用的都是这几个文件。加上今天刚补的信号生命周期回归测试,它开始长出自己的「行为地基」。
  • zjchina-render 是这次的新成员,也是身份最重的一层。它不渲染 DOM,也不渲染 HTML——它直接在 GPU 上画。下面专门讲它。

二、为什么是「自己画」

故事得从 ADR-0005 说起。zjchina 的渲染场景天生分裂:

  • 对外是 zjchina.net,靠 SEO、零安装访问、零 JS 首屏吃饭,只能走浏览器 DOM + SSR;
  • 对内是真机设备端:工厂大屏、车间看板、KUKA 工业平板、机器人 HMI。这些设备没有浏览器,只能自绘。

二者不是「谁取代谁」,而是需要一条明确的渲染路由:自绘为尊,web 走浏览器渲染兜底。于是我们把自绘引擎定为 L0 身份层——一旦是第一公民,就禁不起轻换,得像地基一样稳。

最初我们以为答案现成:Dioxus 的自绘后端 blitz/wgpu 号称对应 Flutter Impeller。一个 spike 下去,结论反转了两次:

  1. blitz 出局。dioxus-blitz 在 crates.io 未发布、官方 pre-alpha;更致命的是,desktop/mobile 这两个 feature 实际映射到 wry/WKWebView——根本不是自绘真机,是套了个 WebView 壳。所谓「缺的只是优先级固化」是错觉,恰恰需要再造。
  2. 独立 wgpu 内核可行。我们在 tidewatch/crates/tw-render 里做了最小 spike:5 万根蜡烛(10 万 instanced 图元)+ 中文坐标标签,在 macOS/Metal 上稳定 120 FPS,字型图集渲染无误。

于是方向定死:独立 wgpu 内核,不复现 Dioxus 组件渲染(那等于自己重写 blitz)。spike 转正为 zjchina-render。

三、zjchina-render:一个领域无关的最小内核

它刻意做得「反专用」。内核里没有任何「蜡烛」概念——蜡烛是消费者(bin/candle.rs)用公开 API 拼出来的,恰好用来证明它领域无关。

模块就这么几块:

  • primitive 三类原语:Lines(网格/刻度线段批)、Rects(轴对齐矩形批)、Mesh(instanced 单位 quad,蜡烛 body+wick 的主路径),外加 Text(已栅格化字形)。
  • scene 保留式场景:Vec<DrawItem>,每个带世界坐标 Aabb2,按视口裁剪,脏标记区分「几何变更」与「相机变更」。
  • camera 一个 Camera2D,pan/zoom 折叠成 16 字节的投影 uniform。
  • renderer 拥有 wgpu 的 surface/device/queue/双管线(solid instanced + text),不垄断事件循环——宿主在自己的 ApplicationHandler 里调它,方便 tidewatch 和未来的 desktop 嵌入。
  • text 用 fontdue 把中文字形栅格化进 512×512 的 R8 atlas,(char, px) 键缓存,alpha blend 采样。

依赖全部走 MIT/Apache-2.0 红线,其中 fontdue 替代 cosmic-text 是为了避开 MPL——许可证不干净,再快的引擎也不能进身份层。

它的性能回答只有一个,就是我们 spike 里验证过的那条:

静态几何 instanced 一次性上传,每帧只写 16 字节投影 uniform。pan/zoom 不重建顶点,CPU→GPU 带宽压到最小。

四、为什么把它隔离出根 workspace

一个容易漏掉的工程决策:zjchina-render 在 crate 内开了一个独立 [workspace],不加入根 Cargo.toml 的 members。

理由是实打实的:根 workspace 是 dioxus/wasm/web 一脉,把 wgpu/winit 这类重型原生依赖并入 members,会让 cargo build --workspace 和 cargo test --workspace 全量拉入 GPU 编译链——这正是我们在 3.5G 内存小机上「全量编译饿死整机」换来的教训。位置仍在 zjchina 仓库(= 框架 L0 身份),只是构建图隔离。身份层与构建图解耦,是它能安静长大的前提。

五、这一层意味着什么

三层 crate 摆在一起,zjchina 的身份才算完整:

  • core 钉住「知识只有一份」——契约、Schema、漂移锁;
  • ui 钉住「体验只有一棵树」——双端不再各写一份;
  • render 钉住「真机设备端的最后一块」——不再等一个别人维护、且根本不存在的自绘后端。

自绘是 zjchina 未来的刚需,不是可选。工厂大屏、机器人 HMI 这些没浏览器的设备,才是一个多端框架真正的深水区。我们把这条路的第一步,踩实了。

用别人的渲染器,是把身份交出去;自己画,才是把身份握回来。

← 返回首页全部文章