17 KiB
STEP-117 PBF R 首次缓存 Build 安全证据 v1
日期:2026-10-11(JST)。结论:安全准入 BLOCKED;本轮保持 missing-source 与 Symbol placement fail-closed,交付七项非 Jest C++ 回归源码。没有实现 R 零贡献准入或 Symbol 结果证书,没有新的 Native/Android PASS。
1. 工作区与证据等级
Mac.lan / Darwin 24.6.0 / x86_64,仓库 /Users/tei/dev/MapLibreParticleLab,particle/main@43e5f5b91598b9c3d3f1c788052d4bbd2d616668。起点 tracked clean;STEP110/111 与 STEP117 任务书未跟踪。实际模型 GPT-6.1-Sol(gpt-6.1-sol)/ high,由当前 rollout 01a12912-9a0c-7712-9624-793f04b3f681 的 turn_context.model/effort 核实。Integration 本地只读 HEAD 为 80667d6c343411d03a760425438e3d292f452118,没有跨仓写入或远端操作。Android VERSION 仍 13.6.1-particle.23。
已读根 AGENTS、PARTICLE_BASELINE、全部项目规范、总则 README 与四份有效总则、STEP117 任务、STEP112–116 关键任务/调查/Step/完成报告/验收文件;Integration AGENTS、instructions、README、WORKFLOW、TEAM_MODEL、contracts/rfc/adr/status/compatibility。本报告用以下等级区分事实:
- 源码事实:当前 HEAD 的控制流和所有权;不等同运行安全证明。
- 保留实测:STEP116 原设备 JSON 与报告;不是 STEP117 新二进制。
- 测试源码:本轮已写、全部未编译和未运行。
- 待证明设计:尚未实现的证书、影子比较与平台验证。
2. 原 120 帧重新核对
原文件:artifacts/maplibre-particle/android-validation/step113-retest-20261011_121308/static_rc/native-full.json;274,693 字节;SHA-256:cd491d2acca97119f1d4f2d6f77eb7b229c17b90d4be1181a59202308429b219。本轮只读解析完整 JSON,逐帧复算 failMask,不修改或纳入 Git。
实测源码 c48a72a9d492ea0d05e4d58464f9643453158d50,APK SHA a8b7f4e0f8e026bde8835e18a04876062be84cce3a3749a511cb08605d9f8a35,设备 FZG1-2 / 192.168.200.139:5555。STEP116 报告记录安装 SHA 相同、90.008 秒 FastDelta=0 / FullDelta=920。本轮未接设备。
原正式 63 基础层保留;Native 65 层,R[0,63)、C[63,65);C 仅 rn-weather-color、rn-weather-particles。正式 Style fingerprint 为 e0f9c2d1df38ae5d5953b561c52efe2f8590336de80b988e1d2935580d30b996。生产 sources JSON 只有 mapple/raster 与 navsea_delivery/vector;Native 另有 org.maplibre.annotations/annotations、rn-weather-fields/raster-array。没有 tide。R index=2 的 tide-spots 为可见 Symbol,source/source-layer 均为 tide。
| 实测项 | 复核结果 |
|---|---|
| 120 帧范围 | sequence 5631–5750;119 completed Full / 0 Fast / 1 pending |
| 24 Gate | 120/120 knownMask=0xffffff、decisionConsistent=true;decisionMismatch=0 |
| #3 / #4 / #23 | 各失败120帧 |
| #12 / #13 | 各失败105帧 |
| #22 | 最近窗口失败0;生命周期累计6,不称启动全程零失败 |
| R cache | Build=0 / Reuse=0;textureCreate=0 / compositeCreate=0;fallback=missing-source |
| R dependency | mapple、navsea_delivery、tide;missingSourceIds=[tide];placementGatePassed=false |
| pre Placement 状态 | 105帧 false/非零/true;15帧 true/0/false(changed/fade/transition) |
| post Placement 状态 | 104帧 false/非零/true;15帧 true/0/false;pending 的 post=null |
| fade 范围 | pre/post 均为0至0.9914085268974304 |
这解释了一个容易误判的现象:15帧的 #12/#13 通过,并不建立 R cache;这些帧仍有 #3/#4/#23,且上一完成树的 changed=true。Fast #13 使用当前 controller transition,R 首建使用当前 Full 的三条件,二者取样点与职责不同。不得拼接 pre 与另一帧 post 来造出通过状态。
3. 缺 tide:新 RenderItem 为零与 GPU 零贡献的区别
3.1 Style 与 Source 创建
src/mln/style/style_impl.cpp::parse/addSource/addLayer 先加入 parser.sources,再加入 layers。addLayer 在 source 存在时检查兼容性;source 不存在时仍保留原层(源码 TODO verify source)。这正是原 Style 能保留 tide-spots 而没有 tide 的路径。removeSource 会拒绝仍被 Layer 引用的 source;同名恢复不能依靠违法删除调用,合法整份 Style 更换及后续创建也必须覆盖。
RenderSource::create 根据真实 SourceType 创建 RenderVectorSource、RenderRasterSource、RenderGeoJSONSource 等。原 navsea_delivery 是普通 Vector,不能将它归因为 CustomMVTVector;后者的未知 revision 准入限制继续保留。
3.2 Full 的注册、停用和绘制时序
| 环节 | 当前源码事实 | 能支持的结论 / 不能支持的结论 |
|---|---|---|
| createRenderTree source/layer 匹配 | render_orchestrator.cpp 在已有 sourceImpls 的外循环中收集 LayerRenderItem;缺源层不匹配,updateList 初始false |
本轮缺源层无新 RenderItem;不能单凭此证明旧 group 已移出 registry |
| renderability 更新 | 每次 source 更新后比较 isLayerRenderable 与 updateList,调用 markLayerRenderable | 有 source 的正常 Full 能排队停用;所有 sources 为空时此循环不执行,不能推广为任意状态的清理保证 |
| RenderLayer::markLayerRenderable | 更新 CPU bool;activateLayerGroup 创建 Add/RemoveLayerGroupRequest | bool=false 不是 request 已执行的证明;该方法没有清除 drawables、renderTilesOwner、placementData |
| RenderSymbolLayer 停用 | 同时为普通 group 与 collisionTileLayerGroup 排队停用 | 需覆盖两类 group;不能仅数普通 RenderItems |
| layerRemoved | RenderLayer 调用虚方法 removeAllDrawables;Symbol 的实现清除两类 group 的 drawable,然后停用 | 删除 Layer 与缺源导致不 renderable 的生命周期不同,不能互相替代 |
| processChanges | 移动 pendingChanges 后逐个 execute,RemoveLayerGroupRequest 删除对应 registry entry | 删除 registry 不等于销毁 Layer 所持 group/bucket/atlas;指针仍可能持有旧数据 |
| Renderer Full | tree.prepare → source/layer upload → updateLayers → processChanges → cache评估 → tweakers/group upload → Build/live draw → present/endFrame | Cache 评估位于处理 changes 之后,但此前没有生成覆盖旧数据与成功提交的零贡献证书 |
| Cache Build | R 使用 orchestrator 的 layer group range 双 pass;没有只遍历新 LayerRenderItem | 缺源层无 RenderItem不等于range里无旧group;需要独立registry与层身份审计 |
本轮实际读取 render_layer.hpp/.cpp、render_symbol_layer.hpp/.cpp、change_request.cpp、render_orchestrator.hpp/.cpp、render_tree.hpp、renderer.cpp、renderer_impl.cpp、retained_underlay_cache.hpp/.cpp。当前 src/include 中未找到名为 ResourceManager 的类;实际资源所有者是 gfx Context/backend、RetainedUnderlayCache 的 offscreen/composite、LayerGroup/Drawable 与 RenderTile bucket/atlas,以及 DynamicTextureAtlas/ImageManager/GlyphManager。GL 的 Texture2DPool(src/mln/gl/resource_pool.hpp)管理纹理复用,不提供“某个缺源层本帧零绘制”的语义证书。
限定的正向事实:新加载且从未获得 source 的 tide-spots,没有匹配 RenderItem;新 RenderLayer 默认 isRenderable=false。仍缺的事实:原设备 JSON 没有 per-layer registry identity、两类 group 的已执行停用、旧 drawables/tiles/placementData 清空和成功 Full 提交代次。不能从其他 sources 已 Loaded、cache textureCreate=0 或 pending-changes Gate=PASS 推出这些条件全部成立。尤其 task 要求“无旧 Renderable 数据”,单纯停用并不满足它。
3.3 失效链已存在,但不存在缺源的受控依赖状态
retainedSegmentSourceState 在读 revision 前要求每个 sourceId 存在;缺失即返回 ready=false/stable=false/revision=0/reason=missing-source。renderSegmentDependencies 从完整层表收集 source 和 Symbol 全局前提,不因隐藏/空桶删依赖。RevisionBook 没有为从未出现的 tide 登记;删除墓碑可保留,但不代表当前 source 存在或 loaded。
sourceDiff.added/removed/changed 增加 content revision 与对应 source revision,先删/建 RenderSource,再 update。同名同类型但 immutable Impl 改变属于 changed。Fast 的 non-camera signature 比较 immutable sources 身份,下一帧在 createRenderTree 之前拒绝旧输入;已有 PreparedTree 在 Full 创建前释放。异步 onTileChanged 增加全局 content 和局部 source revision并 invalidate。Image/Glyph/Style/Context 的全局 epoch、层身份 fingerprint、camera/viewport key也各自保留。
因此“同名 source 出现后可拒绝旧 Fast”的机制已有源码依据;不能由此反推不存在的 source 可合法登记成 loaded/stable/零 revision。新的缺源状态需要绑定 source absent identity、完整层集合、应用后 registry/旧对象状态和成功绘制代次;source 出现、替换、异步 layout、context改变必须撤销它。当前没有这些存储和核验,不实施空依赖准入。
4. Symbol 首次 Build 的安全根因
4.1 两个 Full 分支和第二重 scene 锁
PlacementController::placementIsRecent 按 commitTime+updatePeriod 判断;transitions关闭则总返回false。Continuous createRenderTree:
- 不 recent:placementChanged=true;Placement::create → placeLayers → commit;setPlacement 清 stale,CrossTileSymbolIndex prune 与 source fading 更新。
- recent:placementChanged=false;setPlacementStale;controller.hasTransitions 在 enabled 时优先因 stale 返回true。
renderSegmentPlacementGatePassed 对结构性 Symbol 要求 !changed && fade==0 && !controllerTransition。每个分支至少违反一项。关闭 transitions 时 Full 为 true/1/false;duration=0但enable=true时fade=1,recent分支仍stale。等待更久、改300/30ms阈值或关闭fade无法提供证书。
renderer_impl.cpp 还有独立保护:retainedPlacementStable、retainedStaticStable、retainedSupported,以及 particlePreparedSceneSupported=retainedSupported && fastContinuousSupported。只放宽 per-R Gate不会使 #23 安全通过;只放宽 #13也不能自举 Build。Fast 成功后 prepareParticleFastFrame 才重置树的 changed/fade;调用它来准备第一次 Build会循环假定尚不存在的安全证据。
4.2 CPU 结果不等于最终 GPU 输入
读 placement.cpp::commit/updateLayerBuckets/updateBucketOpacities/updateBucketDynamicAttributeData、CrossTileSymbolIndex、SymbolBucket::updateVertices、SymbolLayerTweaker 与 DrawableAtlasesTweaker,以及 GL/Metal symbol shader:
- commit 以每个 crossTileID 的 text/icon placed 状态更新 opacity;显隐未变可继承旧 fadeStartTime。controller transition=false不等于所有桶在GPU中已上传或与旧帧等价。
- RenderTreeImpl::prepare 更新 bucket opacity/dynamic data;SymbolBucket 将 uploaded、placementChangesUploaded、dynamicUploaded 标记为false。SymbolBucket::upload 的标记不是成功GPU Drawn R的证书。
- opacity处理 duplicate、默认allow-overlap、justReloaded、variable anchors、orientation、text/icon配对;沿线重投影与camera相关。碰撞为全局 Placement,并可能跨 source/段产生依赖。
- shader实际使用
clamp(baseOpacity ± symbolFadeChange,0,1),再乘paint opacity。fade=0既可能为全透明新符号,也可能为已显示符号;fade非零也可能因clamp不再改变像素。用==0或>=1替代逐输入等价均不足。 - LayerTweaker 还使用tile/label/coord矩阵、size binders、paint插值、纹理尺寸与shader变体;DrawableAtlasesTweaker绑定glyph/icon atlas和sampler。
- retainedRangeContentFingerprint 只散列 layer.baseImpl 指针与index;它不包含 placement结果、opacity buffer、bucket身份、atlas纹理、uniform最终值。Source revision也不替代这些内容。
- Build成功计数来自双pass编码无捕获异常、finishBuild与markBuildSucceeded;随后主buffer present提交。现有实现没有完整的 per-R GPU输入证书或设备completion fence记录。将编码成功说成GPU执行完毕属于证据越界。
4.3 可继续的设计边界与反例
| 候选条件 | 反例 / 缺少的证明 |
|---|---|
| source loaded、无手势、静止90秒 | changed/stale循环继续;异步glyph/tile/image可改变结果 |
| Placement placed布尔相同 | opacity方向/基值、duplicate、anchor/orientation或dynamic vertices可不同 |
| fade=0且controller=false | 实测15帧changed=true,Build仍拒绝;刚出现符号可能base opacity=0 |
| 当前 RenderItems 为空 | 旧group可能仍在registry或旧数据留在RenderLayer;pending请求可能未执行 |
| layer fingerprint/source revision相同 | 不能区分同桶的dynamic opacity/atlas/uniform变化 |
| 两张末态截图看起来相同 | 未捕获变更瞬间、碰撞受害段及GPU提交代次,不能建立持续复用不变量 |
待证明证书至少应覆盖:R层身份/层序和drawable集合;bucket/tiles/crossTileID及跨段碰撞结果;text/icon显隐、opacity方向/基值、dynamic vertices、沿线/variable placement;paint/layout revision;glyph/image/atlas/shader/uniform版本;projection/camera/viewport/pixelRatio;context generation;已应用ChangeRequests及异步content epoch。只能在正常Full完成prepare/upload/tweakers、真实Build成功提交且资源一致时绑定,任何不一致先Full+invalidate/rebuild。不能只加入一个“结果稳定”bool。
这涉及 Placement→Bucket→Drawable/Atlas→Renderer提交生命周期的跨层实现,当前没有足够设备/图像证据保证安全。本轮停止该准入实现,不删或强制置假 placementChanged、symbolFadeChange、controllerTransition,不改24Gate。
5. 七项测试源码及真实覆盖限制
两个既有 test/CMakeLists.txt 已登记的测试文件内新增 Step117Safety.*,无需改构建表,全部 NOT RUN:
| 用例 | 实际测试目标 | 不覆盖的事实 |
|---|---|---|
| RealPlacementControllerHasNoFirstSymbolBuildWindow | 用实际Controller/Placement::create/placeLayers/commit执行120个逻辑时刻;两个分支均fallback,Build/Reuse为0 | 空桶生命周期最小复现,无原PBF碰撞/GPU |
| RealPlacementDisabledOrZeroDurationIsNotAResultCertificate | 实际enable=false与duration=0/enable=true,fade=1、stale行为继续保护 | 没有禁用生产fade |
| MissingAndSameNameRestorationCannotReuseAnOldCache | 缺源→创建→状态机构建/复用→删除墓碑→同名恢复必须新Build | 模拟status与markBuildSucceeded仅为cache状态机,无GPU成功 |
| CameraStyleAnnotationAndContextRequireRebuildBeforeReuse | camera/viewport/content/topology/config变化经过实际revision tracker和cache,使旧cache无效且#3/#4失败 | epoch输入是模拟,未触发真实Annotation/camera设备事件 |
| RealMissingSourceTreeHasNoRenderItemButStillFailsClosed | 真实createRenderTree:缺源Symbol零RenderItem/零group仍missing-source;实际sourceDiff创建、同名替换、移除;layer fingerprint保持不变而revision变化 | 使用step117-source无数据GeoJSON测试对象,未改生产tide;无旧GPU对象场景 |
| QueuedGroupRemovalIsNotAppliedUntilProcessChanges | 两个测试group实际进入registry,真实Remove请求入队前后遍历差异与pending变化 | 测试group代表两类生命周期,不是实际Symbol collision group或GPU Draw |
| RealAsyncCallbacksAndContextStyleCleanupAdvanceContentEpoch | 实际Tile/Glyph/Image observer分派、markContextLost/clearData推进content epoch;旧content输入单独命中#5 | 事件在测试线程注入,无实际网络并发/完整Renderer/设备 |
未新增Jest,未编译或运行这些测试。生产 src/include、所有Gate/计数、Android场景和63层零修改。既有Annotations.Step116图像用例保留;新测试不代替glyph/tile并发、跨段collision、old GPU drawable清理或Full/Fast像素验证。
6. 具体阻塞与协调需求
| 编号 | 无法继续的状态 | 应由谁处理 / 需要的产出 |
|---|---|---|
| B1 缺源零贡献 | 当前只有absent Source与新RenderItem缺席;无应用后双group/旧drawable数据/提交世代证书;revision=0不能登记为有效 | Shared Core制定absent依赖内部状态及销毁/禁用证书;Android/Metal分别验证初始缺源、曾有旧数据、合法Style替换、同名恢复 |
| B2 Symbol首次Build | 重算changed、recent stale;所有可达Full状态都不满足现有准入;无最终GPU结果等价/时间不变证书 | Shared Core提供跨层结果证书设计与影子比较实现任务;先影子不放行,证明后才讨论受控准入;粒子负责人组织双平台审查 |
| B3 正式tide语义 | 生产Style宣告tide-spots但sources不含tide;本仓库不能自行补业务数据、改服务器或删层 | Integration协调NavSea/Style所有者明确是否属于合法宿主动态注入、何时提供真实source及身份;改变公共缺源语义才走RFC/ADR,纯内部优化不强制新Contract |
| B4 独立验证 | 本轮没有Native编译、测试或设备新证据;STEP116历史Mac配置缺libuv | RDC复核当前环境、执行定向测试,首错保留;用户明确授权后由RDC/普通Terminal集中Android验收 |
B1/B2应形成具体设计/实现任务,不通过业务开关、假tide、删Symbol、改24Gate或STEP114烟测绕过。Integration是协调入口,本轮不向他人发消息、不写跨仓文件。当前代码阶段止于测试和证据交付;详见STEP117验收计划。原目标 Build>0、Reuse>0、FastDelta>0仍未完成。