Appearance
精模两次疑难攻坚:850 漂移与 132012 断链
精模(车道级地标)调试集中在两个硬骨头:850 版本的「漂移」其实是 5 个独立根因叠加出的 6 个症状,全部定位后一次性修掉;132012 断链则是一条从写下那天起就没通过的死链路,根因是 C# 回调早已废弃、数据被静默丢弃,最终只改 4 个 C++ 文件完成通道迁移。
850 漂移6 症状 → 5 根因
132012 迁移4 个 C++ 文件
回放闭环精度1e-3
背景
精模数据链路横跨三层:高德/百度底图 SDK 产出精模列表,guideengine(Kotlin/C++)做锚点转换和信号下沉,Unity Napa5 的 LDLayerLandmark / XRenderMeshManager 消费渲染。PC Editor 下没有 Android native 链路,ISYS_HIGH_QUALITY_MODE_CHANGED 不广播,精模调试依赖一套 Editor 工具手工喂数据。两次攻坚都发生在 850 版本联调期。
案例一:850 漂移——6 个症状,5 个根因
问题
Editor 里调高德精模时出现一串看似关联的怪现象:没点 Show 精模就自己加载了(S1);Hide 之后再 Show 不出来(S2);Mercator 模式点 Show 弹框拒绝(S3);大坐标 12622281.085420489311218 输入后 UI 只保留 7 位小数(S4);运行时 GameObjectPool 报 Init Fail(S5);精模全部贴在同一水平面、Y 维度退化(S6)。
根因反推
逐个症状追到根,发现是 5 个互相独立的根因叠加,不是一处 bug 的连锁反应:
| 根因 | 机制 | 对应症状 |
|---|---|---|
| CSV 启动预加载 | BuiltInLandmark.OnAttach 里 LoadFromCsv + CreateLandmarks 绕开 OnReceiveList 直接塞 _landmarkDictShow,id 与 Window 填的不同 | S1 + S2 |
| proto 缺字段 | XPrecisionMoldInfo 的 LonLat 只有 lon/lat 两维,没有 z/CoordType/Scale,Mercator forward 公式的 z 和 Scale 送不进 Core | S3 + S6 |
| DoubleField 精度截断 | EditorGUILayout.DoubleField 内部 ToString 默认约 7 位小数,Mercator 千万级大坐标直接丢精度 | S4 |
| GameObjectPool 只认 Addressables | LoadAsset → Addressables.LoadAssetAsync,未注册的 prefab 加载失败 | S5 |
| resPath 前缀硬约束 | resPath.StartsWith("ArtAssets") 不满足直接 return null,Editor 拖入的 Assets/ 完整路径被挡 | S5 前置 |
Y 退化的机制值得单独说:forward 公式是 Unity.Y = (RPos.z - anchor.z) * scale.z,RPos.z 拿不到时恒为 0,Unity.Y 退化成 -anchor.z 的常量,所有精模自然贴在同一水平面。
方案与关键实现
借位透传协议(跨端)。 proto 不改,复用现有字段借位传参:C++ 把 anchorZ 塞进 pointsLeftBottom.lon,lat 置 0 占位,pointsRightTop 清空;Unity 侧 ApplyAmapAnchorZ 在 adapter 是高德时把 info.pointsLeftBottom?.x 写回 RPos.z。改动集中在 OnReceiveList 循环末尾的统一 gate,覆盖三条 Show 路径。
Editor-only 调试通道。 调试参数走 userData 前缀协议:ldmark-debug:mode=GaodeRelativeMercator;scaleX=...,Core 侧 ApplyDebugCoordinateModeFromUserData 解析前缀后写 CoordinateType + Meter2P20Scale;native 真实 userData 没有此前缀,函数早退,线上零影响。
三层代码隔离。 Core(napa5-hmi)只加无条件的辅助方法(adapter 类型 + userData 前缀双 gating 早退);调试窗口放在独立 Editor asmdef(includePlatforms=Editor + defineConstraints=LD_DEBUG),ProjectSettings 追加 LD_DEBUG symbol。配套 3 个测试文件、11 个 test case 覆盖 Broadcast / Path / Prefs。
其余修复。 删 LandmarkConfig.csv + Reader、精简 BuiltInLandmark(漂移源头之一,改由 EditorPrefs 承担单条输入持久化);DoubleField 换成 DrawDoubleFieldHP(DelayedTextField + G17 + double.TryParse),加 6 个 string buffer 镜像保精度;GameObjectPool.LoadTemplate 加 Editor 下 AssetDatabase 兜底;放宽 resPath 前缀硬约束。
案例二:132012 断链——一条从未通过的链路
问题
高德精模链路日志显示 C++ 侧 SendPrecisionMold ✓ 成功向Unity派发精模数据 signal=132012,Unity 侧 SystemServiceHandler.cs L198 的 case 132012 也早就写好了,但精模就是不显示——case 从未被命中。
根因
C++ 到 Unity 有两条平级通道。通道 A(x3d 插件通路):data_service_plugin 白名单 → SystemSignalModel 手写 parser 把 proto 转成 XStruct → x3DEngineEventDataCallback 进 C#,105010(百度链)走的就是这条,活着。通道 B(独立地图通路 UnityTransport):signalCallback 零解析透传 proto 字节,靠 mXDataHandle 回调进 C#——而 C# 侧的注册入口 setX3DPluginCallback 早已被废弃删除,全仓 git grep 只剩一行注释。mXDataHandle 恒为 nullptr,signalCallback 在空指针判断后直接丢弃数据,无日志、无报错。132012 此前配置在通道 B,等于从接入那天起数据就从没真正进过 C#。
方案:迁移通道 A,只改 4 个 C++ 文件
mermaid
flowchart LR
K["Kotlin GblMapPrecisionHelper<br>发 132012 proto"] --> BUS["native dispatcher 总线"]
BUS -.->|"改造前"| UT["unity_transport<br>signalCallback"]
UT -.->|"mXDataHandle 恒 nullptr"| DROP["数据静默丢弃<br>断链"]
BUS -->|"改造后"| DSP["data_service_plugin<br>白名单准入"]
DSP --> SSM["SystemSignalModel<br>复用 PrecisionMoldListParser"]
SSM --> CB["x3DEngineEventDataCallback"]
CB --> H["SystemServiceHandler<br>case 132012 首次命中"]
H --> R["ObjEvent 广播 → 精模渲染"]4 处改动:data_service_plugin.cpp 白名单加 132012;xp_xpu_module.cpp registerProtocolParser 注册解析路由;system_signal_model.cpp case fallthrough 复用 105010 的 s_precisionMoldListParser;unity_transport.cpp 注释掉通道 B 白名单行。C# 侧 L198 的 case 本来就是按通道 A 的数据形状写的,迁移后第一次被真正喂到数据——Kotlin 和 C# 零改动。
输入输出必须拆号。 105012 是转换器输入(原始 GCJ02 经纬度),132012 是转换器输出(高德锚点坐标)。不能同号回发,原因有三:GblLaneService 观察者会再次命中造成死循环;白名单按 SignalCode 路由、不看载荷,一个号只能代表一种坐标语义;回放会把两种坐标系的数据混在一起。
附:Landmark 回放工具的三层根因
Editor 回放工具「反算出的相对位置跨帧漂移」也不是一处 bug,而是三层叠加:
| 层 | 根因 | 证据 |
|---|---|---|
| L1 坐标系 | captureTarget 挂在 LDLayerTileRenderMesh 子节点下(-90°X 旋转 + 非均匀缩放),与 landmark root(identity)不在同一空间 | 诊断 CSV 的 parent_lossy / parent_rot 列 |
| L2 公式不对称 | GetUnityPos 只对 z 轴乘 scale.x,x/y 不乘,与 tile 渲染「全轴乘 scale」约定不一致 | pos.x 跨 7 帧漂 14.5(= 0.874 × Δanchor.x),pos.y 恰好锁死 |
| L3 数据污染 | 「开始采集」不清队列,多次录制累加,timestamp 跨 5 亿 ms、pos 跨 8000+ 单位 | 主 CSV 队列分析出 3 段不同基线 |
修复同样分三层:反算改用 landmarkRoot.InverseTransformPoint(worldPos) 绕开父链污染;正算(GetUnityPosWithScale)与反算改成对称公式——正算 X/Y/Z 全乘 scale、反算对应除 scale,且隔离在 GaodeRelativeMercator 分支不污染线上 BuiltOut 逻辑;采集入口加「清空 / 取消 / 追加(不推荐)」弹窗防污染。对称公式做完数学闭环验证后,实测 pos.x / pos.y 跨 144 帧稳定到 1e-3。回放放弃 timestamp 驱动(污染数据下不可靠),改手动按钮 + 每帧推 cursor 的状态机:
mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Playing: ▶ 回放
Playing --> Playing: 每 LateUpdate 推一帧
Playing --> Idle: ⏹ 停止
Playing --> Done: 播完隐藏
Done --> Idle: 复位指标
| 指标 | 数值 |
|---|---|
| 850 漂移 | 6 症状 → 5 根因,一次性修复 |
| 调试通道隔离 | 独立 asmdef + LD_DEBUG,3 个测试文件 11 个 test case |
| 132012 迁移 | 4 个 C++ 文件,Kotlin/C# 零改动 |
| 回放工具闭环 | 对称公式,跨 144 帧稳定到 1e-3 |
复盘
- 一串症状别急着找一个「总根因」。850 漂移的 6 个症状来自 5 个互相独立的缺陷,逐个症状钉到代码行再动手,才敢说「一次性修复」。
- 静默丢弃是链路排查的最大陷阱。132012 断链没有任何报错,C++ 日志还打着「成功派发」——空指针判断后直接 return 的写法,让一条死链路看起来活着。废弃入口要连回调判空一起清,或者至少留一条丢弃日志。
- 跨端协议字段不够时,借位透传 + 前缀调试通道是低成本方案,但前提是双 gating 早退保证线上零影响,再用 asmdef + 编译宏把调试代码物理隔离。
- 数值问题先证数学闭环再谈调参:正算反算对称之后 1e-3 的实测稳定性是结果,不是目标。