7c6d84859d15c0323e6a0a099a6ee0f60cbbf3fd
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fa2703968e | added ruff test | ||
|
|
bcab2790bf | modify 3d view to sync / identical to 2d view | ||
|
|
ccedb9ddd9 | migrate pyqt5 to pyside6 | ||
|
|
ef487098c9 |
feat(studio-3d): 限速、真实耗时统计、无位移复用资产几何,以及保护包密钥注入(阶段 9 上半)
- What:
1. **显示限速真的生效**(R5):`ThreeDCoordinator.tick()` 用注入的单调时钟与
`display_period_s() = 1/max_fps` 限制**合成启动**频率(窗口定时器仍是 16 ms,但"显示帧率"是给操作者的
承诺,不是定时器的描述);声明边界(activate/begin_session/show_rest/隐藏)把预算清零,所以按下开始后的
第一帧立刻合成。生产者侧改为**按需**跳过:`Scene3DView.submit_packet()` 在
`coordinator.has_pending_frame()`(mailbox 里已有一帧等待)时不再构造快照——那是 O(H×W) 的拷贝,
而 mailbox 只保留最新一帧,两次拷贝里必然丢一个;被跳过的数量计入新的 `skipped_producer`,
与"显示跳帧"分开。**不丢任何本该显示的帧**(等待中的那一帧仍在)。
2. **真实统计**:`RenderFrame.compose_ms`(composer 在构造帧之前测量并写入,不再读会被下一任务改写的
`last_stats`);`GLRenderer` 的绘制计数改名为 `paint_calls`,`RenderStats.displayed_frames` 由
**widget 按帧身份**统计(相机旋转/色标改动只是重画,不再是"多显示了一帧");`render_totals()` 增
`compose_ms`/`latency_ms`(合成开始→被接受的单调耗时)/`paint_calls`/`skipped_producer`,并且
`shown`/`compose_ms` 改从 widget 读(无 GL 的机器也能报告,之前只有画过才有数)。
3. **无位移时复用资产几何**(R5 建议):`SceneComposer` 缓存 `asset_hash → (positions, normals)`;
变形时照旧从形变后的位置重算法线,不变形时直接用资产自己的(只读、零拷贝)数组。20k 顶点实测:
compose p50 **6.15 ms → 1.03 ms**(法线段 4.9 ms → 0.0001 ms,剩余是采样 1.01 ms);
真实发布包(1032 顶点)compose p50 **0.50 ms → 0.17 ms**。
4. **保护包密钥注入**(R4):新增 `scene3d/keys.py`——从 preference 读取
`scene3d_verify_key`(验签公钥 PEM)/`scene3d_content_key_file` + `scene3d_content_key_id`
(内容密钥,经 `protect.load_key_file`)/`scene3d_require_signature`(发布模式,拒绝无签名包),
组装成 `SceneKeys` 交给 `SceneAssetLoader` → `load_package` → `PackagedAssetLoader.load(keys=…,
verify_key_pem=…)`;`AppWindow._sync_3d_source()` 解析它并 `Mesh3dPanel.set_keys()`。
缺失/不可读的密钥是**状态而不是崩溃**(`describe()` 里带原因),密钥材料永不进日志;
`Mesh3dPanel.describe()` 的 scene 块记录 `keys`(布尔 + 路径)。
- Why: 审查报告 R5(`max_fps` 只被存储与显示、`draw()` 硬编码 `compose_ms=0.0`、`_displayed_frames` 把
重画也算新帧、`SceneComposer.last_stats` 被显示链路完全未读)与 R4(底层支持签名/加密包,Studio 加载
入口却从不传验签公钥与 KeyProvider,合法保护包打不开,只能发明文包或关掉校验)。R5 的另一半是
"当前只有曲面着色、没有 displacement 时,composer 仍每帧重算法线并重新打包位置"——缓存后 20k 顶点
降低到六分之一。
- Verification:
· `test_scene3d_coordinator.py` +6:15 fps 的假时钟下 1.32 s 内只启动约 20 次合成(60 fps 对照 ≥55 次)、
`display_period_s()` 跟随设置、结果延迟是真实单调差值、`has_pending_frame()` 随 mailbox 变化;
`test_scene3d_live.py` +3:无位移时发布帧直接**就是**资产自己的(只读)数组、force_deform 但仍无位移时
给出 `field_unavailable` 诊断、有标定位移时仍重算、`compose_ms` 属于该帧且不被下一次合成改写;
`test_scene3d_view.py` +2:等待中的帧让生产者跳过第二个快照且取出后立即恢复、`render_totals()` 的
compose 成本与延迟是真实值;新增 `test_scene3d_protected_package.py` 7 项:Studio 入口打开签名包、
用 preference 里的内容密钥打开加密包、缺验签公钥 → `asset_auth_failed`、密钥文件缺失/无 key id →
状态而非崩溃、发布模式拒绝无签名包而开发模式接受、`SceneAssetLoader` 把密钥交给 worker、面板携带密钥。
· 全量:shared+apps 同一次运行 **588 项 0 失败 15 跳过**;真 GL 冒烟(`QT_QPA_PLATFORM=windows`)
**14 项 0 失败**;ui_check 587 项 0 失败;locale_check 0 缺失。
· 顺带修掉两个**既有的顺序依赖**真 GL 用例(不是本次引入:在上一提交上单跑同样失败):
`test_the_producer_side_never_waits_for_the_gpu` 的收尾没有排空 mailbox(留下的一帧会被下一个测试
上传),`test_an_automatic_colour_scale_follows_the_frame` 在渲染之前就断言"范围来自本帧",
且用"不同颜色数"当自动量程的代理(对可见面积敏感)。现在前者排空并断言排空完成,后者先渲染再断言,
判据改为**平均亮度**(固定 0..1000 把 3..7 压进 LUT 最暗的一小段,自动量程铺满整条色带)。
· 性能报告(`.repro/perf-cpu-*-after-9.json`,`bench_scene.py --cpu-only`):合成 p50
合成 poc 0.016 ms / 20k 顶点 1.03 ms / 真实包 0.17 ms;对比已提交的 `perf-report-windows-dev.json`
(20k 6.15 ms、真实包 0.50 ms)。
- Not in this change: **画布布局第二批**(2D/3D 页签容器、参数抽屉、窄窗口回退、诊断抽屉)与曲线量程开关
仍未做;阶段 9 的门禁部分(`soak_scene.py --minutes 120` GUI 模式、Jetson Orin Nano 与 QCS6490 上的
`probe_gl/bench_scene/soak_scene` 报告)需要长时间运行与板卡,本轮未跑,结论不写"通过";
签名私钥仍只在构建机(本提交只引入"读"的一半)。
|
||
|
|
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)、限速与真实统计、二维后端统一均未动。
|
||
|
|
5cd92254e1 |
feat(studio-3d): 阶段 5 —— 发布保护(签名/可选加密/平台锁定)与性能长时报告
- What: 新增 `scene3d/protect.py`(封套 v2:Ed25519 签名 + 可选 AES-256-GCM,nonce/salt 每包唯一、 元数据作 AAD、明文摘要进签名、KeyProvider 只问 get_content_key)与 `record/visualization3d.py` (session.json 的 visualization3d 块:写入版本化、读取时缺块=不支持);`package.py` 支持 v2 (payload=plain 仅签名 / aes-gcm 签名+加密)并继续读 v1,新增解压比上限与"写入后自读回"校验; `contracts.py` 新增 PlatformLock/PlatformInfo/check_platform_lock、Sealing、platform_mismatch 诊断码; `Scene3DView` 在装配前查 OS/架构、拿到 capabilities 后查图形半区,违反即撤下场景并在面板显示原因; 工具 `compile_scene.py` 新增 --sign-key/--write-verify-key/--encrypt-key-id/--key-file/--lock-from-probe, `validate_scene.py` 新增 --key-file/--key-id/--verify-key/--require-signature(发布门禁), 新增 `bench_scene.py`(p50/p95 分阶段 + --cpu-only + --vertices N + --package)与 `soak_scene.py`(RSS 四分位速率 + 队列上界 + 抽稀采样);两份报告落到 docs/architecture/。 - Why: 规格 §5.2 的保护目标是"提高复制成本"而不是"防提取",所以要的是签名(来源)+ 可选认证加密(机密性) + 密钥隔离,而不是自制加密或把 ZIP 当加密;§12 的"包保护/性能/长时"三行需要可复现的数字与判定标准, 而不是一次性的观感。平台锁定还必须只降级三维:锁定不通过时二维照常。 - Verification: shared 全量通过(新增 test_scene3d_protect.py 35 项:封套往返/篡改/密钥/锁定/记录块/ compile→validate 端到端发布流程;test_scene3d_package.py 20 项含 v1↔v2 兼容);apps/studio 全量通过 (test_scene3d_view.py 30 项含锁定拒绝与 session.json 块);ui_check 566 项 0 失败;locale_check 0 缺失; 真实资产端到端:1032 顶点编译出 signed+sealed+locked 包(56 927 B 密文),validate --require-signature 退出 0, 改一个字节退出 2;性能报告(perf-report-windows-dev.json):1032 顶点 compose p50 0.50 ms(30 fps 上限内), 20022 顶点/39480 三角形 compose p50 6.15 ms(约 33 ms 帧的 18%),采集线程 submit p95 < 0.4 ms(合成器被人为放慢到 50 ms); 长时报告(soak-report-windows-dev.json):无窗口 10 min/60 fps 尾段 0.354 MB/min,GUI 4 min/30 fps 尾段 0.809 MB/min, 队列上界(mailbox=1、单飞行合成)两者均成立。过程中修掉:GUI 进程里的合成器计时被 GIL/vsync 污染(98.8 ms vs 真值 6.1 ms)、 跨线程读 last_stats 撕裂、key id 无 provider 时会静默写明文包、soak 工具自身记时间列表会无界增长、RSS 读取失败伪装成零增长、 平台锁定不是终态诊断。 - Not in this change: ARM 板卡的能力报告与 2 小时门禁运行(按用户要求暂缓;命令与判定标准已写入阶段 5 文档); GPU 显存随时间的测量(无可移植 API,报告说明渲染器缓冲估计恒定且更新路径不新建 GPU 对象); `cryptography` 在此环境无法联网安装,本机验收用的是本机已有的 cp310 构建(pyproject 里已声明为可选 extra `protected-assets`)。 |
||
|
|
ea1027e5a6 | 阶段 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)。 | ||
|
|
0b309e7edf | 三维显示骨架:可执行合同 + 模块签名 + 分阶段计划 - What: 按 docs/architecture/studio-3d-design.md 与 studio-3d-contracts.pyi 搭建骨架(阶段 0)。shared/src/orisys_app_shared/scene3d/:contracts.py 把 .pyi 的规格变成可执行代码——全部 DTO/枚举(FrameIdentity/FieldDescriptor/SurfaceFieldFrame/MeshPart/SurfaceBinding/CameraPreset/ExpectedGeometry/SceneAssets/ColorScale/DisplaySettings/VertexFields/RenderFrame/CameraMatrices/GLCapabilities/RenderStats/CompileOptions/CompileReport)、标准诊断码表(DIAGNOSTIC_SEVERITY,含 stale_frame=info、field_unavailable=warning 等)、ThreeDError、freeze()(拥有数据 + C 连续 + 只读)、五个校验器(validate_surface_frame/validate_binding/validate_mesh_part/validate_assets/validate_display_settings/validate_render_frame)、LatestFrameMailbox(容量 1、替换计数、锁内只换引用)、UNAVAILABLE_DESCRIPTOR/rest_descriptor() 与 summarize_parts(),以及全部 Protocol;该模块不 import Qt/OpenGL/SDK。assets.py/sampling.py/composer.py 为签名骨架(未实现处抛 NotImplementedError 并写明归属阶段)。apps/studio/src/orisys_studio/scene3d/:camera/adapters/coordinator/gl_backend/widget/panel 签名骨架,其中 configure_surface_format() 已实现(QApplication 之前调用,desktop33=GL3.3 Core / gles30=GLES3.0,深度 24、无 MSAA);模块级一律不 import GL 绑定,PyOpenGL 只在 GLDispatch.bind_current_context 里加载。tools/scene_assets/{probe_gl,validate_scene,compile_scene}.py 提供入口与参数,未实现时输出 not_implemented 并返回 2(避免被 CI 当成通过)。docs/architecture/studio-3d-stages.md:阶段 0-5 的产出、每阶段的验收矩阵、并行度与回归门。测试:shared/tests/test_scene3d_contracts.py 26 项(freeze 拥有数据、可写/非连续数组被拒、scalar 与 valid 形状一致、位移缺 quality 被拒、非有限 scalar 标为 valid 只报警不静默、绑定越界/-1 约定/权重和为 1、三角形索引越界、感知法线单位长度、恰好一个 sensing part、binding 必须属于 sensing part、设备几何/域/尺寸不符被拒、色标 lower<upper、邮箱替换计数与多线程不阻塞生产者、身份区分 source/generation、骨架可导入且不引入 PyOpenGL);apps/studio/tests/test_scene3d_skeleton.py 4 项(骨架不 import OpenGL、surface format 两个 profile、widget 构造不需要 context、各 stub 报出归属阶段)。 - Why: 用户要求"先从骨架搭起,然后计划功能实现顺序,最后逐阶段实现与验收"。骨架阶段的价值在于把接口变成可执行的(.pyi 本身无法被测试),并把"数组只读、单位与质量必须声明、近似位移不得当毫米、参考帧不得冒充测量、显示丢帧不得计入记录丢帧"这些容易在实现时被绕过的规则先固定下来;同时明确不 import Qt/OpenGL 的边界,使采集线程与 CI 能在无 GPU 环境导入它。 - Verification: shared 全量 187 passed(原 161 + 26);studio 95 passed(原 91 + 4);ui_check 554 项检查 0 失败;compileall 通过;probe_gl --profile desktop33 在未实现时返回 2 并说明将产出什么报告。 - Not in this change: 三维功能本身(阶段 1 起);app_window/main.py 未接线,骨架对现有应用完全惰性(无任何模块被应用导入)。 |