diff --git a/docs/报告/CM95_S52_OpenCPN渲染流程对照审计报告_v1.md b/docs/报告/CM95_S52_OpenCPN渲染流程对照审计报告_v1.md new file mode 100644 index 0000000..004a988 --- /dev/null +++ b/docs/报告/CM95_S52_OpenCPN渲染流程对照审计报告_v1.md @@ -0,0 +1,566 @@ +# CM95 S-52 OpenCPN 渲染流程对照审计报告 v1 + +日期:2026-09-20 + +## 1. 审计目的 + +本报告不从最终截图差异反推代码缺陷,而是以 OpenCPN 源码的真实渲染执行流程为审计基准,逐阶段对照 CM95 当前实现。 + +目标是回答: + +1. OpenCPN 从 CM93/S-57 对象到最终屏幕绘制,实际经过哪些步骤; +2. CM95 当前每一步是否等价; +3. Conditional Symbology Procedure(CSP)返回的完整 rule chain 是否被完整保存和执行; +4. 哪些当前缺陷可以在人工看图之前由源码/结构审计直接发现; +5. 后续应建立怎样的自动硬门槛,避免继续依赖人工发现遗漏。 + +## 2. 固定基线 + +OpenCPN 参考提交: + +`f7211b4e1874c453e37a7f190aa191c55a25c32e` + +主要参考源码: + +- `gui/src/cm93.cpp` +- `libs/s52plib/src/s52cnsy.cpp` +- `libs/s52plib/src/s52plib.cpp` +- 同版本 chartsymbols / lookup 资源 + +CM95 审计时 HEAD: + +`fe008ef6eb527d7980728de694a1c7bc81d38b3c` + +主要对照文件: + +- `src/cm95pbf/s52.py` +- `src/cm95pbf/csp.py` +- `src/cm95pbf/presentation.py` +- `tools/cm95_s52_style_compiler.py` +- `web/kyushu_compare.html` + +## 3. OpenCPN 实际渲染流程 + +OpenCPN 的流程不是“CSP 选择一个最终符号”。 + +真实执行链是: + +`CM93 object → CM93→S57 class normalization → S-52 Lookup → 初始 rule list → CS(...) → CSP 返回 instruction string → 再次解析为 rule list → 每条 primitive 逐条渲染` + +### 3.1 CM93 → S57/S52 对象视图 + +`cm93.cpp` 在创建 S57Obj 时包含 CM93 类别替换和属性适配。 + +例如已确认: + +- `ITDARE → DEPARE` +- `SPOGRD → DMPGRD` +- `FSHHAV → FSHFAC` +- `OFSPRD → CTNARE` + +这一步决定后续采用哪一个 S-52 Lookup/CSP。 + +### 3.2 Lookup + +Presentation Library Lookup 返回普通绘制指令和/或: + +`CS(XXXXXXXX)` + +OpenCPN `_LUP2rules()` 将 INST 解析成 Rule 链。 + +### 3.3 CSP + +当遇到 `RUL_CND_SY` 时: + +`GetAndAddCSRules() → RenderCS() → s52cnsy.cpp 对应 CSP` + +CSP 返回的不是单一图标,而是完整 instruction string,例如可能同时包含: + +`SY + TX + SY + SOUNDG03 + QUAPNT01` + +或: + +`AC + AP + LS + SY` + +### 3.4 CSP 返回串再次解析 + +OpenCPN 将 CSP 返回串再次通过 Lookup/rule parser 转换成新的 Rule 链。 + +因此: + +`SY(...);TX(...);SY(...);...` + +不是提示信息,而是必须按顺序执行的正式渲染程序。 + +### 3.5 最终 primitive 执行 + +`DoRenderObject()` 对 Rule 链逐个 switch 执行。 + +OpenCPN 当前实际执行的主要 primitive 包括: + +- `TX` → RenderTX +- `TE` → RenderTE +- `SY` → RenderSY +- `LS` → RenderLS / RenderGLLS +- `LC` → RenderLC +- `MP/MPS` → RenderMPS +- `CA` → RenderCARC +- `AC` → Area color renderer +- `AP` → Area pattern renderer +- `CS` → 再次进入 conditional symbology + +结论: + +**OpenCPN 的语义单元是“完整有序 Rule Chain”,不是一个最终 symbol code。** + +## 4. CM95 当前流程 + +当前 CM95 有两条不同路径。 + +### 4.1 非 mariner-dependent CSP + +`resolve_csp_hints()` 调用 `csp.py` resolver,得到 instruction string。 + +之后通过: + +- `static_rule_hints()` +- `materialize_symbol_hints()` +- `materialize_text_hints()` + +转换成 PBF scalar/JSON hints,再由 style compiler 消费。 + +### 4.2 mariner-dependent CSP + +当前集合: + +- DEPARE01 / DEPARE02 +- DEPCNT02 +- SOUNDG02 +- OBSTRN04 +- WRECKS02 + +在 `mariner_mode=runtime` 时,`s52.py` **不会执行完整 resolve_csp_hints()**。 + +而是只调用: + +`materialize_mariner_runtime_facts()` + +将中性业务事实写入 PBF,然后把 CSP 名保留给 runtime。 + +因此完整的 OpenCPN CSP rule chain **没有进入 PBF,也没有形成一个统一 Runtime Rule IR**。 + +Web 端随后在 JavaScript 中重新手写: + +- DEPARE 颜色 +- DEPCNT safety contour +- SOUNDG 显示 +- OBSTRN/WRECKS symbol +- LIGHT CA screen-space 绘制 + +这形成了第二套决策逻辑。 + +## 5. 核心结构性缺陷 + +### 5.1 同一个 CSP 被实现两次 + +例如 OBSTRN04: + +第一份逻辑: + +`src/cm95pbf/csp.py::resolve_obstrn04_instruction()` + +第二份逻辑: + +`web/kyushu_compare.html::runtimeHazardSymbol()` + +OpenCPN 只有一份 CSP 决策树,返回完整 Rule Chain,再由通用 renderer 执行。 + +当前架构则是: + +`OpenCPN CSP → Python 人工翻译一次 → runtime facts → JavaScript 再人工翻译一次` + +任何一层少一个分支都会产生视觉缺失。 + +本次“有虚线圈但没有 10.5”就是这种重复翻译导致的。 + +## 6. 已确认的源码级不等价 + +### 6.1 OBSTRN04 POINT:QUAPNT01 丢失 + +OpenCPN POINT 分支最终会追加: + +`CSQUAPNT01(obj)` + +无论是 isolated danger 还是普通 obstruction branch,都会执行 positional-quality presentation。 + +当前 `resolve_obstrn04_instruction()`: + +- isolated danger:直接 `return udw` +- 普通 POINT:返回 symbol / SOUNDG03 + +没有追加 QUAPNT01。 + +因此即使 Python 固定 profile 下直接使用 resolver,也不是完整 OpenCPN rule chain。 + +### 6.2 OBSTRN04:OBJNAM TX 丢失 + +OpenCPN OBSTRN04 在统一 end 阶段: + +如果存在 `OBJNAM`,追加: + +`TX(OBJNAM,1,2,3,'15118',-1,-1,CHBLK,26)` + +当前 Python resolver 没有该 TX。 + +当前 Web runtime 同样没有实现。 + +### 6.3 OBSTRN04 LINE:sounding 丢失 + +OpenCPN: + +当 LINE 有 VALSOU 且 `VALSOU <= 20`,在 LS 后还会追加 `SNDFRM02/SOUNDG03` sounding presentation。 + +当前 Python LINE branch 只返回: + +- LC +- 或 LS + +没有 sounding。 + +### 6.4 OBSTRN04 AREA:sounding 与 QUAPNT01 丢失 + +OpenCPN AREA 有 VALSOU 时: + +- 先 LS +- 再追加 sounding +- 最后追加 QUAPNT01 + +当前 Python AREA 有 VALSOU 时只返回 LS。 + +### 6.5 OBSTRN04 isolated AREA:QUAPNT01 丢失 + +OpenCPN isolated area: + +`AC + AP + LS + ISODGR51 + QUAPNT01` + +当前: + +`AC + AP + LS + ISODGR51` + +少 QUAPNT01。 + +### 6.6 WRECKS02 POINT:QUAPNT01 丢失 + +OpenCPN POINT: + +- isolated:`ISODGR51 + QUAPNT01` +- 有 VALSOU:`DANGERxx + TX('Wk') + [WRECKS07] + SOUNDG03 + QUAPNT01` +- 无 VALSOU:WRECKS symbol + QUAPNT01 + +当前 Python resolver 的这些分支均未完整追加 QUAPNT01。 + +### 6.7 WRECKS02 AREA:QUAPNT01 丢失 + +OpenCPN AREA: + +line/fill/isolated danger/sounding 后仍追加 QUAPNT01。 + +当前 Python area path 不完整。 + +### 6.8 Web runtime 的 SOUNDG 不是 OpenCPN SOUNDG03 + +本次为危险物补的: + +`runtime_depth_text="10.5"` + +可以恢复肉眼数字,但它不是 OpenCPN SOUNDG03 的真正实现。 + +OpenCPN SOUNDG03 会根据: + +- SAFETY_DEPTH +- TECSOU +- QUASOU +- STATUS +- QUAPOS +- 深度整数/小数位 +- 正负深度 +- sounding symbol family + +生成多枚: + +`SOUNDSxx / SOUNDGxx` + +symbol sequence。 + +当前 Web runtime 是普通 MapLibre text。 + +因此: + +**“数字出现”不等于 1:1 S-52 sounding 已完成。** + +### 6.9 WRECKS 的 TX('Wk') / WRECKS07 当前 Web runtime 未执行 + +Python resolver 已能生成部分完整 instruction。 + +但 runtime mode 下这些 instruction 不会被发送给 Web;Web 只根据 facts 自行选择主要 hazard symbol 和 depth text。 + +所以以下 OpenCPN primitives 当前可能不出现: + +- `TX('Wk',...)` +- `SY(WRECKS07)` +- QUAPNT01 symbol +- SOUNDG03 glyph chain + +### 6.10 LIGHTS05 实际应审 LIGHTS06 + +OpenCPN 参考源码: + +`LIGHTS05() → return LIGHTS06(param)` + +因此审计目标必须是 LIGHTS06 的真实流程,而不是函数名 LIGHTS05 后面的旧不可达代码。 + +本次已核对: + +- 非 sector / sector CA 半径逻辑; +- sector angle +180° 转换; +- LITVIS obscured sector; +- all-round CA; + +当前核心 CA 逻辑与 OpenCPN LIGHTS06 基本一致。 + +但 light description 的同位置去重/偏移规则仍应单独建立 branch test,不能仅靠已有 screenshot 判 PASS。 + +## 7. Rule Hint 压缩本身也存在风险 + +`presentation.py::static_rule_hints()` 明确是: + +> Materialize compact scalar hints; full rule IR remains lookup-side. + +它对于同类 primitive 通常只存第一个 scalar: + +- first SY +- first AC +- first AP +- first LC +- first LS + +虽然 `materialize_symbol_hints()` 在多 SY 时可以生成 `s52_symbol_rule_sequence`,但当前 style compiler 并不是一个通用的“有序 instruction executor”。 + +所以即使非 runtime CSP,也必须审计: + +**原始 rule chain 是否在 scalar 化之后仍然保持所有可见 primitive。** + +## 8. Style Compiler 与 OpenCPN renderer 的架构差异 + +OpenCPN 是通用 rule executor。 + +当前 style compiler 是“按已知 materialized fields 拼 MapLibre layers”。 + +当前 static coverage gate 能检查: + +- AC +- AP +- LS +- LC +- SY +- LIGHT_SY +- TX/TE +- LIGHT_TX + +但它检查的是“某 render kind 有 layer 消费”,不是: + +`OpenCPN rule #1 → #2 → #3 ... 是否全部、按顺序、按条件等价执行` + +Runtime coverage 更粗: + +- RUNTIME_DEPARE +- RUNTIME_DEPCNT +- RUNTIME_SOUNDG +- RUNTIME_OBSTRN +- RUNTIME_WRECKS +- RUNTIME_CA + +当前 capability 只能回答“某类 renderer 是否存在/preview”,不能回答: + +**OBSTRN04 的 7 个 primitive 是否一个都没漏。** + +## 9. 为什么现有审计没有提前抓到 10.5 + +现有审计能证明: + +- Source data 存在; +- PBF facts 存在; +- CSP 名存在; +- runtime capability 有一个 renderer; +- 页面确实画了 hazard symbol。 + +但它没有建立: + +`OpenCPN OBSTRN04 branch → expected ordered primitive sequence` + +因此: + +`SY(DANGER51)` + +和: + +`SY(DANGER51) + SOUNDG03(10.5) + QUAPNT01 + OBJNAM TX` + +在旧 capability audit 中都可能被归为同一个: + +`RUNTIME_OBSTRN` + +粒度过粗。 + +## 10. 应建立的新审计:OpenCPN Source-First Rendering Equivalence + +建议新增独立审计层,不依赖最终 PBF screenshot。 + +### Gate A — OPENCPN_CSP_BRANCH_COVERAGE + +从 OpenCPN `s52cnsy.cpp` 固定 commit 提取每个目标 CSP 的 branch matrix。 + +至少记录: + +- procedure +- geometry type +- object class +- attribute conditions +- mariner parameters +- called sub-procedures +- ordered output primitives + +当前第一批: + +- LIGHTS06 +- SOUNDG02 / SOUNDG03 +- UDWHAZ03 +- OBSTRN04 +- WRECKS02 +- DEPARE01 +- DEPCNT02 +- QUAPOS01 / QUAPNT01 / QUALIN01 +- SLCONS03 +- RESTRN01 +- RESARE02 + +### Gate B — CSP_RULE_SEQUENCE_EQUIVALENCE + +对每个 branch,比较: + +`OpenCPN expected ordered rule sequence` + +与: + +`CM95 resolver expected ordered rule sequence` + +不是只比较主要 symbol。 + +例如: + +Expected: + +`SY(DANGER51), SOUNDG03, QUAPNT01, TX(OBJNAM)` + +Actual: + +`SY(DANGER51), SOUNDG03` + +必须 FAIL。 + +### Gate C — RUNTIME_PRIMITIVE_COVERAGE + +对于 mariner-dependent CSP,检查 expected primitive 是否存在对应 runtime executor: + +- SY +- TX +- TE +- LS +- LC +- AC +- AP +- CA +- SOUNDG/MPS +- OP / DisplayBase side effects +- nested QUAPOS + +不能再只检查 `RUNTIME_OBSTRN` 一个粗粒度 flag。 + +### Gate D — REAL_DATA_BRANCH_COVERAGE + +扫描实际 Dataset,把真实出现过的 attribute combination 映射到 OpenCPN branch signature。 + +如果实际数据中存在某个 branch,而该 branch 没有 equivalence fixture: + +FAIL。 + +这能避免只测试“理论上典型的几条”。 + +### Gate E — RENDER_CHAIN_CONSERVATION + +建立: + +`Source → normalized S57 view → Lookup → CSP branch → ordered Rule IR → PBF/runtime facts → renderer primitive execution` + +逐段 signature。 + +任何阶段删掉 primitive 都失败。 + +## 11. 推荐架构修正 + +审计能发现问题,但要从根本上减少问题,建议取消“Python 一套条件 + JS 再写一套条件”的结构。 + +推荐: + +`OpenCPN-compatible CSP Decision IR / Runtime Rule IR` + +流程改为: + +`Source facts → 唯一 CSP resolver → ordered Rule IR → 平台 renderer` + +例如 OBSTRN04 resolver 输出: + +```json +[ + {"op":"SY","code":"DANGER51"}, + {"op":"SOUNDG03","depth":10.5,"...":"..."}, + {"op":"QUAPNT01","quapos":4}, + {"op":"TX","field":"OBJNAM","...":"..."} +] +``` + +Web / NavSea renderer 只负责执行 IR,不再自己判断 WATLEV/VALSOU/CATWRK。 + +这样 OpenCPN 决策树只翻译一次。 + +## 12. 当前审计结论 + +当前实现不能称为“OpenCPN 渲染流程 1:1 等价”。 + +可以确认: + +- CM93 → S52 lookup 主链已经较完整; +- static resource contract 已较强; +- LIGHTS multi-presentation 守恒已有独立审计; +- CA screen-space renderer 已较完整; + +但 mariner-dependent CSP 存在明确的 Rule Chain 丢失: + +- OBSTRN04:FAIL +- WRECKS02:FAIL +- SOUNDG runtime:FAIL(当前为文字近似,不是 SOUNDG03 glyph executor) +- DEPARE/DEPCNT:仍为 PREVIEW_ONLY,需按 branch 审计 +- LIGHTS06:核心 CA 已接近,但完整 branch/label sequence 尚未建立 source-first equivalence gate + +因此下一阶段不建议继续按截图逐项补。 + +应先建立: + +1. OpenCPN CSP branch manifest; +2. ordered Rule IR; +3. CSP rule-sequence equivalence audit; +4. runtime primitive coverage audit; + +然后再修 renderer。 + +人工截图应降级为最终视觉验收,而不再承担发现业务分支遗漏的职责。