Files
orisys-app/tools/scene_assets
ykcheungab 3c0424b90c feat(studio-3d): 设备几何身份接入 —— 同尺寸不同映射/不同表面被拒绝,资产加载只采用最新请求
- What:
  1. **设备侧自己说几何**(新增 `shared/.../acquisition/geometry.py`):`describe_sensor_geometry()`
     从运行时真正加载的文件推导——`manifest.json` 的 `assets_dir`、`sensor.json` 的
     `camera.resolution_stitch`/`source_scale`、以及据此命名的 unroll map(`unroll_map_uv_s0.4.npz`)。
     产出 `DeviceGeometry(expected, verified, detail)`:`field_domain_id` + `shape_hw`(映射身份,含
     flip/scale/映射内容摘要)与 **无单位** 的 `source_geometry_id`(地图里 `world_xyz` 的内容摘要)。
     米制 `geometry_id` 需要"1 源单位 = N 米"这句只有人能做的声明,设备侧**故意留空**。两类失败区分对待:
     定位不到 bundle / 没写 `assets_dir` → `verified=False`(可显示,但不声明已校验);bundle 变了
     (map 缺失、与 `resolution_stitch` 不符)→ 直接拒绝并指名两个形状/文件。
  2. **契约**:`ExpectedGeometry += source_geometry_id`;`SceneAssets += source_geometry_id,
     compatible_sensor_model, compatible_hardware_revisions`;诊断码 `geometry_unverified`(info);
     `digest.source_geometry_digest(world_xyz)`;`legacy.convert()` 写入 `source_geometry_id`;
     `package.py` 的 manifest 增 `source_geometry` 块(数组名与单位写明),读取时把 `compatible` 与
     `source_geometry` 一并还原(旧包读到空串)。`validate_assets()` 的 expected 段重写:**只比较已声明的
     字段**,`source_geometry_id` 不符即拒绝,包内未记录该身份 → 返回 `geometry_unverified` 警告而不是
     静默通过;顺带修掉 `expected.sensor_model != assets.parts[0].role` 这行毫无意义的比较,改成真正的
     型号/硬件版本比对(`*` = 任意版本)。
  3. **接线**:`app_window._sync_3d_source()` 传 `expected_provider=`(一个闭包,指向当前传感器配置);
     `SceneAssetLoader` 在**工作线程**上解析它(`_resolve_and_load`);`view.use_scene(assets, source_id,
     expected)` 改为 `validate_assets(assets, expected)` 并把警告送到面板,同时记录 `device_verified`;
     `_make_snapshot()` 用**设备**的身份构造 adapter(设备没声明的字段才回退资产),使
     "帧与场景匹配" 第一次成为独立检查而不是自证;`describe()` 记录 `scene.device_verified` 与设备身份。
  4. **资产加载请求身份(R7-a)**:`SceneAssetLoader` 增请求号与 pending 槽——忙时不再丢弃新请求,而是
     保留最新一个并在前一个结束后启动;完成信号带请求号(`loaded(assets, 描述, id, device)` /
     `failed(diag, id)`),`Mesh3dPanel` 丢弃过期完成(否则旧文件的场景会落回新选择上)。失败后可重试。
  5. **工具与包**:`compile_scene.py`/`validate_scene.py` 报告 `geometry_id`/`field_domain_id`/
     `source_geometry_id`/兼容信息;README 写明无单位身份是什么、为什么设备侧用它;**重编发布包**
     `apps/studio/config/sensors/finger/scene/finger-display-1.0.0.oryscene`(geometry_id 仍为
     `1a2e0e92…`、1032 顶点 / 1974 三角形,新增 `source_geometry_id=46a03a5f…`,因此 asset_hash 变为 `ea4beab6…`)。
- Why: 审查报告 P1/R3:主窗口只传 sensor_type 与 package_override,`Mesh3dPanel._expected` 永远是 None,
  `validate_assets(assets, None)` 跳过全部设备比对,而 adapter 的 geometry_id/field_domain_id/shape_hw
  全部来自**已选中的资产**——于是收到的数据被贴上"资产自己期望的身份"。结果:shape 相同但 UV 映射、
  flip_v、source_scale 或相机配置不同时无法区分。P1/R7:`SceneAssetLoader.load()` 在忙时直接 return,
  既没保留最新请求也没有 generation,`_on_loaded` 又用当前 `_source` 标记已加载路径,快速切换配置时旧
  文件的任务会回到新选择上。
- Verification:
  · 证据探针 `.repro/probe_stage6_device.py` → `probe_stage6_device.json`(真实 bundle + 真实包):
    设备描述 `verified=true`、domain `feb7f71e…`、source geometry `46a03a5f…`、shape 300x280、
    米制 id 未声明、map = `unroll_map_uv_s0.4.npz`;发布包通过该描述;**同尺寸不同映射**(只改 map 一个
    像素的坐标)→ domain 变化、shape 不变、包被 `field_domain_mismatch` 拒绝;设备什么都不声明 →
    接受 + `geometry_unverified`。
  · 新增 `shared/tests/test_scene3d_device_geometry.py` 9 项:描述来源、发布包通过、同尺寸不同映射拒绝、
    同 map 不同表面拒绝、定位不到 bundle → unverified、`resolution_stitch` 与 map 不符拒绝、map 缺失
    指名文件、无 `assets_dir` → unverified、map 命名规则。
  · `test_scene3d_contracts.py` +4(未声明身份 → 警告而非匹配、source geometry 不符拒绝/缺记录 → 警告、
    型号与硬件版本、`source_geometry_digest` 的稳定性与形状校验);`test_scene3d_package.py` +2
    (`source_geometry` 往返、旧包读成"未声明");`test_scene3d_view.py` +6(adapter 身份来自设备、
    设备 domain 不符 → 拒绝且不上屏、无描述 → `geometry_unverified` 且 `device_verified=false`、
    **最新请求才运行**、面板丢弃过期完成、失败后可重试)。
  · 全量:apps/studio 0 失败(`--junitxml`:231→236 项);shared 0 失败(含 device_geometry 9 项);
    `validate_scene.py` 对重编包全项 ok。
- Not in this change: 生产侧仍不声明米制 `geometry_id`(那句单位声明只有人能写;要设备端复算它需要把
  `meters_per_source_unit` 与刚体变换记进配置,属可选加强项,已在本提交的 `DeviceGeometry.detail` 里留位);
  `ExpectedGeometry.mapping_digest` 未加——`field_domain_id` 本身就是由映射内容摘要派生的,再加一个字段
  只是同一个事实的第二个名字;`docs/architecture/studio-3d-contracts.pyi` 与本阶段新契约的同步放在阶段 6
  的文档提交里;保护包密钥注入(R4)、限速与真实统计、二维后端统一均未动。
2026-09-14 21:10:53 +08:00
..

Scene asset tools

Offline tools for the three-dimensional view. They never run in the application: the app loads a compiled package (.oryscene) and nothing else.

tool what it is for
compile_scene.py a Blender export (+ optional housing GLB) → one .oryscene package
validate_scene.py the checks the loader and the compiler must agree on; the release gate
probe_gl.py what this machine's OpenGL actually is (exit 0 GPU, 3 software, 4 no context)
bench_scene.py per-stage p50/p95 of the running view (--cpu-only for the pure CPU cost)
soak_scene.py long run: RSS trend and the queue bound

Run them with the repository's own interpreter, from orisys-app:

$py = '.venv-build\Scripts\python.exe'

Compiling the shipped finger sensor

The runtime asset is sensors/finger/assets/30fps-0703: it is what sensors/finger/manifest.json names (assets_dir), and its unroll map (unroll_map_uv_s0.4.npz, out size 300×280, source_scale 0.4) matches camera.resolution_stitch in sensor.json, which is the grid the live optical flow is computed on.

Do not compile assets/visualization/flatten_remap.npz: it is a 1024×1024 domain, so every live frame would be refused with field_domain_mismatch (300x280 vs 1024x1024). That map belongs to offline visualisation.

$sdk = 'D:\orisys_ws\orisys-sdk\sensors\finger\assets\30fps-0703'   # or the copy in apps/studio/config

# 1) state the unit (the importer never guesses it) and read what it measured
& $py tools\scene_assets\validate_scene.py --legacy-dir $sdk --map "$sdk\unroll_map_uv_s0.4.npz"

# 2) compile
& $py tools\scene_assets\compile_scene.py `
  --legacy-dir $sdk `
  --map "$sdk\unroll_map_uv_s0.4.npz" `
  --meters-per-source-unit 0.001 `
  --output apps\studio\config\sensors\finger\scene\finger-display-1.0.0.oryscene

# 3) verify
& $py tools\scene_assets\validate_scene.py apps\studio\config\sensors\finger\scene\finger-display-1.0.0.oryscene

--meters-per-source-unit is a physical statement about the sensor, not a formatting choice. The measured mesh extent is 15.81 × 15.0 × 7.5 source units (1032 vertices, 1974 triangles); the shipped finger surface is about 15.8 mm across, so one source unit is one millimetre and the value is 0.001. If the mechanical drawing says otherwise, state that number instead — nothing downstream can tell the difference, and a wrong factor looks plausible.

Expected result: 1032 vertices, 1974 triangles, geometry_id starting 1a2e0e92…, a 56 KB package, and validate_scene.py reporting every check ok.

The package also records a unit-free source_geometry_id (46a03a5f… for the shipped bundle). It is the only geometry identity the acquisition side can reproduce without the unit statement above, so it is what lets the running application verify that the scene it is drawing was built from the same surface — and the same mapping, via field_domain_id — that the device is actually running. A package that predates this record is still accepted, and reported as unverified rather than as a match.

Where the application looks for a package

  1. preference.json → "scene3d_package": "<path>" (an absolute path; also the way to point a developer build at a freshly compiled package);
  2. <config>/sensors/<sensor>/scene/*.oryscene — for the studio app that is apps/studio/config/sensors/finger/scene/;
  3. nothing: the pane reports the directory it searched and the compile command above, and the two-dimensional views keep working.

The SDK's own sensors/ tree is not searched (the app prefers its local config/sensors bundles). Compile into the app's scene/ directory, or set scene3d_package.

Release packages (signature, optional encryption, platform lock)

All three are optional and independent of the plain package above:

& $py tools\scene_assets\compile_scene.py --legacy-dir $sdk --map "$sdk\unroll_map_uv_s0.4.npz" `
  --meters-per-source-unit 0.001 `
  --sign-key keys\build-ed25519.pem --sign-key-id build-2024 `
  --write-verify-key keys\build-ed25519.pub.pem `
  --encrypt-key-id finger-display --key-file keys\finger-display.key `
  --lock-from-probe docs\architecture\gl-report-windows-dev.json `
  --output apps\studio\config\sensors\finger\scene\finger-display-1.0.0.oryscene
  • --sign-key signs the manifest (and therefore every entry hash, the compatibility information and the platform lock). The private key stays on the build machine; the client ships only --write-verify-key's public half.
  • --encrypt-key-id + --key-file additionally seal the content with AES-256-GCM. Encryption never replaces signing. Reading a sealed package needs the protected-assets extra (cryptography); a plain package does not.
  • --lock-from-probe takes the platform lock (OS, architecture, GL profile, vendor, renderer, minimum texture size, "no software rasteriser") from a probe_gl.py report. A machine the lock excludes loses the 3D pane, with the reason, and keeps everything else.
  • --encrypt-key-id without --key-file, or with no crypto backend installed, is refused rather than silently writing a plain package that looks protected.

Release gate:

& $py tools\scene_assets\validate_scene.py <package> `
  --key-file keys\finger-display.key --key-id finger-display `
  --verify-key keys\build-ed25519.pub.pem --require-signature

Performance and long runs

# per-stage p50/p95 of the running view (needs a real window system)
$env:QT_QPA_PLATFORM = 'windows'
& $py tools\scene_assets\bench_scene.py --frames 600 --output perf.json
& $py tools\scene_assets\bench_scene.py --cpu-only --frames 500 --vertices 20000 --output perf-cpu.json
& $py tools\scene_assets\bench_scene.py --package <package> --key-file keys\finger-display.key `
    --key-id finger-display --verify-key keys\build-ed25519.pub.pem --output perf-real.json

# long run: the release gate is two hours in the GUI mode (it is the stricter one)
& $py tools\scene_assets\soak_scene.py --minutes 120 --fps 30 --output soak.json
& $py tools\scene_assets\soak_scene.py --no-window --minutes 10 --fps 60 --output soak-cpu.json

The committed reports for the developer machine are docs/architecture/perf-report-windows-dev.json and docs/architecture/soak-report-windows-dev.json; the stage-by-stage acceptance, the failure samples and the compatibility notes are in docs/architecture/studio-3d-stages.md.