AutoUI 高性能 Image Viewer 与后端媒体管线设计
状态:Implemented(PLAN-547 execution complete; independent review pending) 日期:2026-09-04 范围:
examples/ui/031-image-viewer、Auto 后端图片服务、AutoUI 媒体资产传输、 Vue/VM/Rust 三运行形态 参考实现:qimgv 的加载优先级、邻图预取、latest-wins 缩放合并和工作集缓存思想; 不复制其 Qt/C++ 代码、资源或 UI。
1. 背景与决策
现有 examples/ui/029-photo-gallery 是图库产品样板:相册导航、过滤、排序、收藏、 缩略图网格和轻量大图预览。它的数据源是固定网络 URL,查看器只是网格应用的一个 视图分支,不承担本地目录扫描、异步解码、资源预算、快速连续翻图和高倍率缩放。
本设计决定新建独立的 examples/ui/031-image-viewer,不把性能管线塞入 029-photo-gallery。两者未来可以复用同一个 ImageSurface 显示契约,但产品状态、 后端职责和验收指标保持独立。
Image Viewer 必须遵守 AutoUI 的基本交付模型:
- 前端和后端均使用 Auto 编写;
- Vue 模式生成 Vue 前端与 Rust HTTP 后端,前后端分离;
- VM 模式解释执行前后端 Auto,用于开发期快速迭代;
- Rust 模式把前后端转译为 Rust,
--server rust --merged下进程内调用,形成最终 独立原生进程; - 图片加载、目录索引、解码/缩略、预取、缓存和过期请求仲裁只在后端 Auto 服务中 定义一次;Vue 前端不另写 File API、解码器或缓存算法。
2. 目标与非目标
2.1 目标
- 同一份 front/back Auto 源码可生成 Vue、由 VM 解释、并转译为 Rust 原生应用。
- 原生 UI 线程不执行文件读取、目录扫描、图片解码、缩略或高质量重采样。
- 首次请求立即展示窗口和占位态,当前图片优先于所有预取任务。
- 快速连续翻图和缩放采用 latest-wins;过期结果不得覆盖较新的界面状态。
- 默认只保留当前图和相邻图工作集,并由字节预算约束缓存。
- Vue 通过后端 HTTP 媒体路由读取编码图片;VM/Rust merged 通过相同媒体 URI 命中进程内资产,不经过 HTTP、JSON 或 base64。
- 支持 JPEG、PNG、WebP 静态图,正确应用 EXIF orientation。
- 支持上一张/下一张、fit-window、fit-width、1:1、自由缩放、锚点缩放、拖拽平移、 旋转、全屏、缩略图条、文件信息、快捷键和后端原生打开文件/目录。
- 建立可重复的 release 性能和资源占用门禁。
2.2 非目标
- 不在首期实现视频、RAW、HEIF/HEIC、AVIF、JPEG XL 或动画逐帧播放。
- 不实现裁剪、调整尺寸、覆盖保存、删除、复制、移动、打印、脚本和壁纸设置。
- 不实现远程上传协议或浏览器 File API;Vue 模式面向本机 Rust 后端或已配置的 后端可访问目录。
- 不实现任意大图的磁盘级瓦片金字塔;首期保证单幅大图不产生无界重复副本。
- 不追求 Vue 与原生端内部实现一致,只要求可见行为和后端策略一致。
- 不把 Image Viewer 写成 Rust 专用应用,也不在生成产物中手改代码。
3. 运行形态
同一套 Auto 源码
┌────────────────┴────────────────┐
│ │
src/front/*.at src/back/*.at
UI/viewport/输入/状态展示 目录/加载/处理/队列/缓存策略
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ │ │ │
Vue generator Rust UI generator AutoVM a2r Rust
│ │ │ │
│ View::ImageSurface │ 进程内 API
│ └──────────┬──────────┘ │
│ │ │
└─ HTTP JSON 控制面 ───┴─ /api/__auto/media ─┘
+ HTTP 媒体数据面 或进程内媒体解析3.1 Vue + Rust split
- Vue 只调用
api.at生成的控制 API,并把ImageAsset.src绑定给ImageSurface。 - Vite 已代理
/api到 Rust 后端;媒体 URI 继续使用相对/api/...。 - 浏览器对媒体 URI 发普通 GET/HEAD;后端返回正确 MIME、长度、ETag 和缓存策略。
- 浏览器负责最终像素呈现,但没有应用级加载、缩略、预取或缓存实现。
3.2 VM merged
- front/back Auto 均由 VM 执行。
- 控制 API 走既有 merged 宿主桥;图片任务进入公共媒体运行时。
ImageSurface遇到/api/__auto/media/...时先查同进程媒体注册表,不开启 HTTP。
3.3 Rust + Rust merged
- front/back Auto 均转译为 Rust,控制 API 直接调用后端函数。
- 媒体注册表与 Iced renderer 位于同一进程,
ImageSurface直接取得共享资产。 - release 可执行文件不启动 HTTP 服务,也不把像素序列化为 JSON/base64。
4. 总体分层
4.1 Front Auto:展示与交互
src/front/app.at 保存轻量界面状态:
session_id、generation、当前索引和文件元信息;asset_src与asset_revision,不保存编码字节或 RGBA;fit_mode、zoom、offset_x/y、rotation;- 工具栏、缩略图条、信息面板、加载/错误/空状态;
- 80ms settle timer 与最新 viewport revision;
- 键盘、滚轮、拖拽和按钮事件。
Front Auto 不负责目录读取、文件格式判断、解码、缩略、缓存和任务线程。
4.2 Back Auto:策略唯一真源
src/back/image_service.at 定义:
- viewer session 生命周期;
- 目录图片过滤和确定性排序;
- 当前索引与 generation;
- current/neighbor/settled-rendition 三类请求的优先级;
- 当前图 + 前后邻图的 keep set;
- 快速翻图和缩放的过期结果判定;
- 缓存 retain/release 决策;
- 错误归一化和可观测统计。
底层线程、解码器、编码器、共享字节和 GPU handle 是运行时机制;何时请求、请求谁、 保留谁、接受哪个结果由 Back Auto 决定。
4.3 公共媒体运行时:机制层
新增公共 ui::image_pipeline,同时被 VM、Rust 生成后端、Rust HTTP server 和 Iced renderer 使用。它提供:
- 有界优先队列和两个解码 worker;
- 内容指纹与 rendition key 去重;
- encoded、decoded、render-handle 分层缓存;
- 进程内
MediaAssetRegistry; - GET/HEAD 媒体响应;
- 订阅/唤醒和指标快照;
- 丢弃过期结果和安全释放资源。
公共运行时不保存 viewer 的当前索引或业务导航规则。
5. 控制面与数据面
5.1 为什么不直接扩展普通 JSON API
现有 TypeScript API 客户端默认 response.json(),Rust Axum 目标默认 Json<T>。 若把图片放进 []int 或 base64 JSON,会产生体积膨胀、大字符串、重复复制和 native merged 模式无意义的序列化。
本设计保留 api.at JSON 控制面,另由生成的后端自动挂载框架媒体数据面。像素不作为 普通 Auto 值跨界。
5.2 媒体 URI
Back Auto 返回稳定的相对 URI:
/api/__auto/media/{asset_id}/{revision}asset_id是进程内生成的不可猜测标识,不包含文件路径;revision参与缓存键,内容改变后生成新 URI;- Vue 直接 GET;
- VM/Rust renderer 识别此前缀并先走进程内注册表;
- split native 模式若未来启用,也可回退 HTTP。
5.3 媒体 HTTP 语义
- 支持 GET、HEAD、
If-None-Match;首期不要求 Range。 - 成功返回
Content-Type、Content-Length、ETag、Cache-Control: private, max-age=31536000, immutable。 - 未就绪的请求最多等待配置的首帧期限;超时返回 503 +
Retry-After。 - 不存在返回 404;已过期 ticket 返回 410;解码失败返回 422。
- 响应体直接引用
Arc<[u8]>或文件映射,不构造 base64/JSON。
6. Auto 数据契约
后端 API 使用普通可序列化记录:
type ImageEntry {
id str
name str
path_display str
width int
height int
file_bytes i64
modified_ms i64
}
type ImageAsset {
id str
revision int
src str
width int
height int
mime str
encoded_bytes i64
state str // queued | ready | error | expired
error str
}
type ViewerSnapshot {
session_id str
generation int
entries []ImageEntry
current_index int
current ImageEntry
asset ImageAsset
prev_index int
next_index int
}
type ViewRequest {
session_id str
generation int
viewport_w int
viewport_h int
device_scale float
fit_mode str
zoom float
rotation int
quality str // interactive | settled
}path_display 仅用于 UI,不能反向用作媒体读取路径;真实 canonical path 保留在后端 session 中。
7. 后端 Auto 状态机
7.1 打开
open_file/open_directory创建新 session,并递增 generation。- 后端 canonicalize 用户选择,确定根目录和当前文件。
- 扫描同目录的 JPEG/PNG/WebP,按自然文件名排序并生成
ImageEntry。 - 向运行时提交 current 请求,优先级 100。
- 提交 prev/next 请求,优先级 10。
- keep set 更新为 current + prev + next;旧 session 资源解除 pin。
- API 立即返回 queued asset;
ImageSurface自行等待媒体就绪。
7.2 翻图
- generation 加一,计算循环 prev/current/next。
- 清除尚未开始的旧 current 请求;正在解码的任务允许结束但不得 publish 为当前代。
- 若目标已在缓存,立即发布;否则提交优先级 100。
- 更新 keep set,邻图优先级 10。
- 任一结果提交前比较
(session_id, generation, source_fingerprint)。
7.3 缩放与平移
- pointer/wheel 事件先在 Front Auto 更新 viewport transform,原生端只变换现有纹理, Vue 只变换已有
<img>。 - 每次 zoom 改变递增
view_revision并打开 80ms settle timer。 - settle 时 Front Auto 发送
quality = settled的ViewRequest。 - Back Auto 只保留同 session 最新 settled revision;中间请求被覆盖。
- 新 rendition 就绪后发布新 asset revision;前端保留视觉中心并无闪烁替换。
7.4 关闭
close_session取消未开始任务、标记在途结果不可发布、解除所有 pin。- 媒体条目在无引用且超过 TTL 后回收。
- 窗口关闭和后端进程退出均触发 worker shutdown,不遗留线程。
8. 调度与缓存
8.1 队列
- decode worker 默认 2 个;高质量 resize lane 默认 1 个。
- 优先级:current 100、settled current 90、neighbor 10、thumbnail 5。
- 去重键:
source_fingerprint + orientation + rendition_spec。 - 队列最多保留 8 项;同 session 的 neighbor 最多 2 项。
- latest-wins 通过 generation/revision 在入队、解码完成和 publish 三处校验。
8.2 缓存
- Metadata:路径指纹和尺寸,默认最多 4096 项。
- Encoded rendition:默认 64 MiB。
- Decoded host pixels:默认 256 MiB。
- Render handles:按 asset key 弱引用;不再使用进程生命周期无界
HashMap。 - 当前图可突破 decoded budget 一次;发布完成后必须驱逐其他非 pinned 条目,使常态回到
budget + current_asset_bytes以内。 - 邻图是软 pin:内存压力下先于当前图淘汰。
所有预算可由环境变量覆盖,但测试和示例使用默认值。
9. 图片处理策略
- 首期格式:JPEG、PNG、WebP 静态首帧。
- 先读 header 和 EXIF orientation,再决定 rendition。
- fit 模式优先生成接近
viewport × device_scale的显示版本,避免小窗口长期保留 多份全尺寸 RGBA。 - 1:1 或放大到原始像素需求时才请求原尺寸版本。
- interactive 阶段使用现有纹理线性过滤;settled 阶段用高质量缩小算法。
- 旋转 90/270 度交换逻辑尺寸,缓存键包含 orientation/rotation。
- Alpha 图片保持透明;无 Alpha 输出优先 JPEG/WebP,避免无意义 RGBA 网络体积。
- 后端永不覆盖源文件。
10. ImageSurface 跨端契约
新增 Display widget ImageSurface:
image-surface (
src: .asset_src,
fit: .fit_mode,
zoom: .zoom,
offset_x: .offset_x,
offset_y: .offset_y,
rotation: .rotation,
filter: if .settling { "linear" } else { "high" },
alt: .title,
onload: .ImageLoaded,
onerror: .ImageFailed,
onwheel: .ZoomAt,
onpan: .PanBy,
ondblclick: .ToggleOneToOne
) {}事件语义:
onwheel(delta_y, x, y):坐标为 surface 本地逻辑像素;正值缩小、负值放大。onpan(dx, dy, phase):phase 为 start/move/end,增量为逻辑像素。onload(width, height, revision):当前 src 成功可见后仅发一次。onerror(code, message, revision):失败可重试,旧 revision 事件必须丢弃。ondblclick(x, y):由 Auto 决定 fit 与 1:1 切换。
Vue 映射为裁剪容器 + <img>,只负责展示、输入归一化和 transform;图片内容仍来自后端 媒体 URI。VM builder 与 Rust generator 都构造 View::ImageSurface,Iced 共享实现负责 原生绘制。
GPUI 暂不做性能实现,但新增 View variant 时必须提供明确的普通 Image 降级,确保 feature 组合可编译。
11. 安全与健壮性
- 只有后端文件选择器返回或配置根目录内的 canonical path 可进入 session。
- HTTP 媒体 URI 不接受文件路径、
..、盘符或 UNC 路径。 - asset id 至少 128 bit 随机性,并绑定当前进程/session。
- 解码前检查文件大小、声明尺寸和像素总量;默认拒绝超过 1 GiB 文件或 400 MP 图像, 返回可见错误而不是分配失控。
- 所有尺寸乘法使用 checked arithmetic。
- 损坏图、截断图、不支持格式和权限错误均归一为稳定错误码。
- 不在日志中输出完整用户路径;调试日志使用 basename + asset id 前缀。
12. 可观测性与性能预算
媒体运行时暴露只读统计:
- queued/running/completed/dropped 任务数;
- encoded/decoded cache bytes、entries、hit/miss/eviction;
- current request 的 queue/decode/publish 耗时;
- 活跃 session、asset、worker 数;
- UI thread 最大阻塞采样。
Rust merged release 在仓库参考机器上的首期门禁:
- 空窗口首次可见 P95 ≤ 250ms;
- 24MP JPEG 冷打开到 fit 首帧 P95 ≤ 500ms;
- 已预取邻图切换到可见 P95 ≤ 100ms;
- 连续 100 次导航后只允许最后 generation 成为当前图,队列峰值 ≤ 8;
- 静置 10 秒后进程 CPU 平均 ≤ 1%,无固定高频轮询;
- 非当前缓存回落到配置预算内,decoded 常态 ≤
256 MiB + 当前图 decoded bytes; - 关闭 viewer 后 2 秒内 session pin、worker in-flight 和媒体引用归零。
Vue 和 VM 不套用上述绝对启动时间,但必须通过同一状态机、工作集和过期结果断言。
13. 测试矩阵
13.1 纯逻辑
- Back Auto:自然排序、循环索引、generation、keep set、latest-wins、错误归一化。
- Runtime:优先级、去重、软/硬 pin、字节预算、TTL、shutdown。
- Media route:GET/HEAD/ETag/304/404/410/422/503、MIME、无路径泄漏。
13.2 生成与语义 parity
- Aura schema/registry 识别
ImageSurface。 - VM builder 和 Rust generator 对所有 props/events 生成同一 View 语义。
- Vue generator 只生成媒体 URI 绑定与展示/输入代码,不生成目录扫描、解码或缓存逻辑。
- front/back API 在 Vue split、VM merged、Rust merged 返回同形记录。
13.3 应用 E2E
- Vue + Rust backend:打开目录、首图、翻图、缩放、平移、fit、旋转、错误恢复。
- VM:同场景 AutoUI MCP 驱动与截图。
- Rust merged:同场景 MCP/原生输入驱动与截图。
- 三端比较状态快照和关键几何,不要求逐像素完全相同。
14. 实施切片
PLAN-547 作为一个 L2 实施计划依次完成:
- 公共媒体注册表、调度器、缓存和统计;
- Back Auto
auto.image机制桥与媒体 HTTP 路由; ImageSurface的 schema、View、Vue、VM、Rust 和 Iced 链路;031-image-viewer全栈 Auto 示例;- 三端功能验收与 Rust merged release 性能门禁。
若实施中发现“非阻塞 Back API 调用”无法通过现有 queue/ticket 模式表达,必须回写计划的 待澄清事项并停止扩大范围;不得临时在 Rust 生成产物或 Vue 源中手写旁路。
15. Plan 547 实现映射
当前实现位于 examples/ui/031-image-viewer,并覆盖 ImageSurface 的 View、Vue、VM、 Rust/Iced 与 GPUI fallback 链路;公共 image_pipeline 提供媒体格式识别、注册表、 HTTP 语义和调度/缓存测试。应用级 fixtures、Vue/VM runner、控制 API 与 release 性能 harness 的证据保存在该示例的 tests/ 目录和 Plan 547 复跑记录中。
16. 后续演进
029-photo-gallery可在后续计划中复用ImageSurface,但不迁入本计划。- 动画、RAW/HEIF/AVIF/JXL、色彩管理、瓦片金字塔、编辑和文件操作单独立项。
- 通用
BinaryResponse/Range API 可从媒体路由经验中抽象,首期不为此扩大公共 API 类型系统。