阶段 3:实时数据 —— 采样、合成、最新帧调度与适配器 - What: sampling.py 实现 SurfaceSampler(向量化四点双线性:无效 tap 不计入也不致无效、任一**正权** tap 无效即整顶点无效且值为 NaN、域/形状不符先拒;采样表只在加载时构建)与 remap_to_domain(按 unroll 映射 gather,空洞保持 NaN 且 valid=False,保留尾随轴以支持矢量场)。composer.py 实现 SceneComposer.compose(域校验 → 采样 → 位移 → **从形变后位置重算法线**;scalar 绝不成以 display_gain;approximate 位移产生 display_illustrative(info) 诊断、calibrated 不产生、无位移则着色并报 field_unavailable)与 build_rest_frame(参考帧:scalar 全 NaN、valid 全假、descriptor=unavailable/display_only),gl_backend.rest_surface 改为委托它,避免两处定义。coordinator.py 实现完整调度:容量 1 双 mailbox、generation/来源/会话三重过期拒绝并计数、**最多一次合成在飞行**、可见性挂起/恢复、设置变更(倍率变化重合成**屏幕上那一帧**)、close 幂等;submit 只做身份与形状的廉价校验,绝不等显示。adapters.py 实现 Finger/Box 适配器:|flow| 或 depth 响应、可选经 unroll 映射 remap、设备 valid_mask 优先、域不符时**发布空**并报出所见的两个形状;一律不造位移(未标定的毫米不如没有),并新增 display_illustrative 到诊断码表(info)。assets.py 从"桩类堆"改成门面:re-export 真正实现 + SceneCompiler.compile(外壳 GLB 与预算检查;加密请求明确拒绝并指向阶段 5)。测试:shared/tests/test_scene3d_live.py 25 项(采样/无效语义/倍率只影响位置/示意形变标注/法线重算/remap 空洞/参考帧/只读数组)、apps/studio/tests/test_scene3d_coordinator.py 13 项(状态机、过期、替换计数、单飞行、submit p99<1ms、切换场景丢弃在飞结果、隐藏挂起、倍率重合成、close 幂等)、test_scene3d_adapters.py 13 项;并把三处"桩仍未实现"的过时断言改成对已实现行为的断言。docs/architecture/studio-3d-stages.md 阶段 3 记为完成。 - Why: 阶段 3 决定"屏幕上的颜色和力值到底对应什么"。三条最容易错且肉眼看不出来的规则必须由代码而不是约定来保证:无效点不是 0(NaN 与 valid 分离,否则缺口会被画成一个测量值)、显示倍率只动位置不动测量(否则色标悄悄变了含义)、以及显示永远不能给采集施加背压(容量 1 mailbox + 单飞行合成 + 廉价 submit)。 - Verification: shared 全量通过(scene3d 相关 103 项:contracts 26、assets 33、package 19、live 25);studio 全量通过(scene3d 相关 54 项:stage1 24、skeleton 4、coordinator 13、adapters 13;GL 冒烟 10 项在真实 context 下另行通过);ui_check 554 项 0 失败。过程中修掉 3 个真实缺陷:倍率变化时用 incoming.peek() 取不到"屏幕上那一帧"(已被消费)导致改了倍率画面不动——改为保留 _last_field_frame;display_illustrative 不在码表里被默认成 error;assets.py 里与 package/legacy 同名的重复桩类(会误导集成者,已改为门面)。另修正两处我自己的错误断言(对称凸起顶点法线本就竖直;pytest.approx 不能直接 .all())。 - Not in this change: 手势与面板(阶段 4);示意位移的真实来源(需要 SDK lift + 已确认尺度,现阶段 adapter 一��� displacement=None);app_window 接线与记录隔离(阶段 4)。
This commit is contained in:
@@ -121,22 +121,40 @@ sha256 + 原子写入 + 读取时先验签再相信)、两个工具(`validat
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:实时数据(字段 → 渲染帧)
|
||||
## 阶段 3:实时数据(字段 → 渲染帧)—— 已完成
|
||||
|
||||
**做**:`FingerFrameAdapter`/`BoxFrameAdapter`、`SurfaceSampler`、`SceneComposer`
|
||||
(`compose`/`rest_frame`)、`ThreeDCoordinator` 的双 mailbox 与 generation/过期拒绝、`submit` 的
|
||||
`accepted/replaced/stale/disabled/invalid` 语义。
|
||||
**做**:`sampling.SurfaceSampler`(向量化双线性采样、无效传播、`remap_to_domain` 走 unroll 映射)、
|
||||
`composer.SceneComposer`(采样 → 位移 → 重算法线;`rest_frame`/`build_rest_frame`)、
|
||||
`ThreeDCoordinator`(双容量 1 mailbox、generation/会话/来源过期拒绝、单飞行合成、可见性与设置)、
|
||||
`adapters`(Finger/Box:`|flow|`、depth 响应、域检查与诊断,**不造位移**)。
|
||||
|
||||
**验收**:
|
||||
**验收(已通过)**:`shared/tests/test_scene3d_live.py` 25 项、`apps/studio/tests/test_scene3d_coordinator.py` 13 项、
|
||||
`test_scene3d_adapters.py` 13 项;全部无需 GPU。
|
||||
|
||||
| 用例 | 通过标准 |
|
||||
| 用例 | 结果 |
|
||||
|---|---|
|
||||
| 采样正确性 | 常量场/斜坡/单点脉冲/NaN/遮罩场与 CPU 参考实现逐位一致 |
|
||||
| 无效语义 | 任一正权重点无效 ⇒ 该顶点无效;非有限 scalar 不显示为 0;参考帧 `scalar` 全不可用 |
|
||||
| 色标语义 | `display_gain` 只改显示位置:增大倍率前后 `scalar` 与力值逐位不变 |
|
||||
| 过期帧 | 旧 `generation`/`request_id` 永远不能覆盖新场景;`stale_frame` 安静丢弃并计数 |
|
||||
| 不阻塞采集 | 合成人为放慢 10 倍,`submit` 调用耗时 p99 < 1 ms,记录行数不变 |
|
||||
| 缺数据 | 无字段 → `no_data`,清掉旧测量颜色,允许显示参考模型 |
|
||||
| 采样正确性 | 常量/斜坡/单点脉冲与参考逐位一致;四点双线性与手算一致;2 万顶点采样 < 150 ms |
|
||||
| 无效语义 | 任一**正权** tap 无效 ⇒ 顶点无效且值为 NaN(不是 0);未用 tap 不参与也不致无效;非有限 scalar 同样无效;**0.0 是有效值** |
|
||||
| 色标语义 | `display_gain` 1→50 后 `scalar`/`valid` 逐位不变,只有位置变(示例 1 mm → 50 mm) |
|
||||
| 示意形变标注 | `approximate` 位移产生 `display_illustrative`(info,含「×N」「illustration」);`calibrated` 不产生;无位移时着色并报 `field_unavailable` |
|
||||
| 法线 | 从**形变后**位置重算;凸起处斜坡倾斜、顶点保持竖直、反向位移使斜坡反向倾斜;两条对角线顶点(与凸起不相邻)保持不变 |
|
||||
| 过期帧 | 旧 generation/来源/会话一律 `stale` 并计数;场景切换时正在跑的那次合成结果被丢弃 |
|
||||
| 不阻塞采集 | 合成放慢到 50 ms 时 `submit` p99 **< 1 ms**;200 帧只发生 ≤60 次合成;`max_in_flight == 1`(队列不会增长) |
|
||||
| 缺数据 | 无有效数据 → `no_data`,且呈现参考模型(scalar 全 NaN、`valid` 全假) |
|
||||
| 适配器 | 域不匹配时**发布空**并报出所见的两个形状;带 map 时按映射 gather,空洞为 NaN 且无效;结果只读、不与 packet 共享内存 |
|
||||
|
||||
**这一阶段抓到的**:
|
||||
|
||||
1. **换倍率需要"当前那一帧"**:原来用 `incoming.peek()` 重合成,而那一帧已被消费 → 倍率改了画面不变。
|
||||
现在协调器保留 `_last_field_frame`,倍率变化就重画**屏幕上那一帧**(而不是跳到一个更新的瞬间)。
|
||||
2. `display_illustrative` 不在诊断码表里 → 被默认成 `error`;已作为 `info` 加入码表。
|
||||
3. **对称凸起的顶点法线是竖直的**(我第一版测试断言它倾斜,等于断言一个 bug);现在断言斜坡倾斜、顶点竖直,
|
||||
并且"与凸起不相邻的对角顶点不变"。
|
||||
4. `assets.py` 里还留着与 `package.py`/`legacy.py` 同名的**桩类**——阶段 2 实现后它们就成了会误导集成者的重复定义;
|
||||
现在 `assets.py` 是门面(re-export + `SceneCompiler`,加密请求明确拒绝并指向阶段 5)。
|
||||
|
||||
**明确未做**:手势/面板(阶段 4);**示意位移的适配器接入**留到阶段 4——需要 SDK 的 lift 与一个已确认的尺度,
|
||||
现在适配器一律 `displacement=None`(宁可只着色,也不给未标定的毫米)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user