8.6 KiB
V26 Metal 混合 Segment 视觉错误——保留 Retained 的修复调查报告 v2
日期:2026-10-13
状态:iPad F 组真机 FAIL(2026-10-13 18:51 JST);CommandBuffer 修正候选不能作为修复完成证据。Native 编译/安装/启动通过,但 Retained 视觉验收不通过。禁止发布 V27。
前一版:V26_Metal混合Segment视觉错误_安全降级调查报告_v1.md REJECTED,禁止发布其中关闭 Metal Retained 的方案。
1. 用户强制约束
- 不允许关闭整个 Retained、不允许把有效 Retained Plan 隐式改成全量 Live。
- Retained 必须保持 GPU 离屏绘制、缓存 Build/Reuse、Cache Texture 合成;Continuous 执行频率保持独立。
- Android/iOS 使用同一套 JS Plan 和用户步骤,不以
Platform.OS分配不同实验语义。 - 以 Style Z-order 为唯一图层顺序,不改本船、AIS 或天气业务数据。
- 不新增私有 RN/Native API,不改 Particle Contract。
- 实际执行:2026-10-13 经用户授权,ChatGPT 通过 RDC 在 Mac 远程执行 iOS Metal 构建、签名、覆盖安装与启动;Android 原生编译和安装未执行。iPad 视觉验收由用户人工确认。
2. 已取得的实际复现
来源:Pleiades iPad mini 5,已安装 V26;Windows NavSea Metro 192.168.200.100:8081。
- E:全 115 层 CONTINUOUS,Retained 总开关开启,0 Retained Segment,iPad 无闪烁,图标/本船航行线/AIS 航线/色场/粒子正确。快照
/tmp/navsea-v26-ipad-render-20261013/E-0746/start.json。 - F:R0 仅
background,C1 其余 114 层,Nativevalid=true,fallback=false,actualExecution=[composite,live],R0 build 96/reuse 2778。冷启动即图标过大、本船/AIS 航线消失;天气仍可见且不闪。快照/tmp/navsea-v26-ipad-render-20261013/F-0740/start.json。 - 原始 RC:R0 层 0..30,C1 层 31..114;天气 ON 后闪烁,OFF 后持续。旧 RCR 预检的末端 Layer ID 硬编码与实际 Style 不符,现场最终跑 RC。
- A:关闭 Retained 不闪;B:Native 天气隐藏不闪;C 和 D 分别只显示色场或粒子都闪。C/D 使用相同 Native Runtime 数据提交,不代表单独 shader 根因。
F 在仅最底层缓存时也失败,说明“缩小缓存至简单图层”不能作为修复。
2026-10-13 新构建真机复测(候选 FAIL)
- RDC Mac Bazel
bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic:943/943,exit 0。新候选 ZIP SHA-256:b89bc06bf03eba20a5790da21215fe7e167da20b3fdfa6a34782420bd503a354。 - 新 XCFramework 从本地 ZIP 替换至暂存 NavSea.app,codesign 重新签名和验证 PASS,Pleiades 覆盖安装及启动 PASS;没有发布/替换正式 SwiftPM V26。
- 用户报告新候选 E:地图、天气 ON/OFF 都正常(E 不执行 Retained Cache)。
- 用户报告新候选 F:FAIL,冷启动光弧等图形巨大、不闪、航向线消失、天气开关无可见效果。不能将无闪烁当作正确。
- 新 F Native 快照:
/tmp/navsea-v27-retained-ipad-stage/F-1851/start.json和end.json,时间2026-10-13T09:51:04Z、generation 3、mapReactTag 802、118 Style Layers;R0[0,1)background、actualExecution=composite、build=13、reuse=1069、invalidation=13;C1[1,118)actualExecution=live;valid=true/fallback=false。证明缓存确实工作,但画面仍错。 - 结论:CommandBuffer 原实现存在执行序不明确的风险,然而此候选未修复视觉缺陷,无法将它认定为根因或 V27 修复。后续重点分析 Metal Composite 的绘制状态、RenderPass、Stencil/Depth 和 Live 绘制的相互作用。
- 新 F 完成后 Windows Metro 同一 Android/iOS Debug 开关已改回 E,iOS Bundle HTTP 200;用户于 2026-10-13 20:43 JST 确认 iPad 重新运行 E 后显示 OK。由此 E 稳定恢复;F 明确失败。候选仍留在 iPad,仅切回 JS E,不代表回装了旧 V26。
- 后续本轮只读源代码调查:
renderer_impl.cpp的 F/E 共同走标准 LivedrawableOpaquePass/drawableTranslucentPass,区别是 F 额外执行了RetainedUnderlayCache::beginBuild/finishBuild/composite;mtl::RenderPass和PaintParameters::beginRenderTarget()会重建 Pass 状态及清理 tile clipping mask ID。需重点验证 Cache Composite 的真实 Metal state、Stencil/Depth、顶点/纹理坐标以及 Live drawable 的状态恢复;仅从快照计数不能推断根因。
3. 已从实际代码确认的候选根因
V26 src/mln/mtl/offscreen_texture.cpp 的 OffscreenTextureResource::bind 以 context.getBackend().getCommandQueue()->commandBuffer() 单独取得离屏 CommandBuffer;swap() 对它独立 commit + waitUntilCompleted。
iOS platform/ios/src/MLNMapView+Metal.mm 的默认画面 MLNMapViewMetalRenderableResource 自建 commandQueue / commandBuffer,直到帧末 swap 才提交。
renderer_impl.cpp 先用默认画面 CommandBuffer 组织上传,再绘制 Retained 离屏缓存、调用其 present,之后在默认画面绘制缓存纹理及 Continuous。这两条独立命令队列并不建立“主帧上传 → 离屏构建 → 主帧合成”的 GPU 顺序保证。 这是确定存在的同步风险;是否是图标放大/航线丢失的唯一原因仍需真机验证。
4. 第二版实现(保留 Retained)
include/mln/mtl/context.hpp和src/mln/mtl/context.cpp:提供帧内借用的主 CommandBuffer 指针,在 beginFrame/endFrame 清理,不持有跨帧引用。src/mln/mtl/command_encoder.cpp:默认画面 UploadPass 创建时,将它的 CommandBuffer 登记为当前帧 CommandBuffer。include/mln/mtl/offscreen_texture.hpp、src/mln/mtl/offscreen_texture.cpp:增加仅供 Retained Cache 显式启用的帧内排序开关。启用后其离屏 RenderPass 复用当前帧主 CommandBuffer;离屏swap在共享模式只清理引用,不提前提交主帧,由默认画面帧末统一 commit。未启用此开关的离屏纹理(包括 Snapshot/其它 RenderTarget),即使同帧存在主 CommandBuffer,也维持原先的独立 commit/wait。src/mln/renderer/retained_underlay_cache.cpp:仅在 Metal Retained Cache 分配或使用 offscreenTexture 时显式启用上述开关;OpenGL 路径不变。test/renderer/mtl_retained_attachments.test.cpp:新增共享 CommandBuffer、离屏 swap 后仍允许添加同帧 RenderPass 的针对性 Metal 测试。
预期顺序:一个 Metal CommandBuffer 内 Upload → Retained offscreen render → main composite → live Continuous → final present。各 Pass 的独立 Color/Depth/Stencil attachment 继续保留。
没有修改 RetainedUnderlayCache 的 Build/Reuse/Composite 语义,也没有跳过任意合法 Retained Segment。
5. 验收和停止条件
- 检查改动的 C++ 格式与差异;本轮不自动运行 Native 编译或设备操作。
- 用户手工构建 Metal Candidate XCFramework(使用现有已验证命令
bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic),在独立测试版中接入。不能覆盖正式 V26、不能用旧 SwiftPM binary 冒充新补丁。 - 先运行 MetalRetainedAttachments 的针对性单测,确认共享 CommandBuffer 不提前 commit,独立离屏路径不退化。
- iPad 首先复测 F:保留
[R(background),C(rest)]的 Native Plan,不允许 Native 临时回退全部 Continuous;要求build>0、reuse>0、actualExecution=[composite,live],并且图标大小和所有航线正常。 - F 通过后再测 RC:天气 OFF/ON/OFF,色场、粒子、本船/AIS/海图视觉完整,无异常闪烁;对照 Android OpenGL 同一 JS Plan。
- 检查帧率、CPU/GPU 时间、缓存重建/复用、内存;保证 Retained 的性能价值。
- F 如果仍错误,立即停止,采集 Metal GPU Capture 对比两个 RenderPass 的资源绑定与混合/Depth/Stencil,不能宣布 bug 已修好,也不得重新关闭 Retained。
6. 当前工程和发行状态
Mac 仓库 /Users/tei/dev/MapLibreParticleLab branch particle/main、当前起点 HEAD b0f34cc9a299a75af1f7e708c63d75eaa42e9ca2。源码改动尚未 commit/push/tag/publish;正式 V26 SOURCE_LOCK 指向 fce326a9009d3fc21eb91c5df362f0769163d07e,它是旧发行身份,不代表此次候选已发布。
Windows NavSea 当前 JS 已取消 iOS 独享 E:Debug Android/iOS 均选 E,Release BASELINE。正式修复后需在新 Native 实际安装成功、用户批准测试窗口后,把共用 JS Test Mode 切到 F/RC 进行对照;不能在旧 V26 上提前切换造成已知异常。
当前明确结论:这是保留 Retained 的候选修复,尚未通过 Native 编译与设备画面确认,不能写成 CLOSED/PASS。