From c48a72a9d492ea0d05e4d58464f9643453158d50 Mon Sep 17 00:00:00 2001 From: tei Date: Sun, 11 Oct 2026 11:50:03 +0900 Subject: [PATCH] fix: guard STEP-113 empty annotation and expose PBF blockers --- ..._STEP113原PBF_FastPath阻塞安全修复审查_v1.md | 73 +++++++++ ...13原PBF_FastPath实际阻塞修复_Codex任务_v1.md | 26 +++ ...STEP-116_STEP113原PBF_FastPath实际修复_v1.md | 54 +++++++ .../STEP-116_STEP113原PBF专用APP验收_v1.md | 151 ++++++++++++++++++ ...13原PBF_FastPath实际修复_Codex完成报告_v1.md | 112 +++++++++++++ .../activity/particle/Step113PbfGateScene.kt | 75 ++++++++- src/mln/annotation/annotation_manager.cpp | 21 ++- src/mln/annotation/annotation_manager.hpp | 1 + src/mln/renderer/render_segment_json.hpp | 10 ++ test/api/annotations.test.cpp | 49 ++++++ .../renderer/retained_underlay_cache.test.cpp | 105 ++++++++++++ tools/particle_android_validation/README.md | 8 +- .../verify_step113_fast_path.py | 43 ++++- 粒子开发记录.md | 10 ++ 14 files changed, 726 insertions(+), 12 deletions(-) create mode 100644 docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md create mode 100644 docs/40_任务/STEP-116_STEP113原PBF_FastPath实际阻塞修复_Codex任务_v1.md create mode 100644 docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md create mode 100644 docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md create mode 100644 docs/报告/STEP-116_STEP113原PBF_FastPath实际修复_Codex完成报告_v1.md diff --git a/docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md b/docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md new file mode 100644 index 000000000..6ca7a6611 --- /dev/null +++ b/docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md @@ -0,0 +1,73 @@ +# STEP-116 STEP-113 原 PBF Fast Path 阻塞安全修复审查 v1 + +日期:2026-10-11(JST)。结论:**部分实现(PARTIAL);空 Annotation 生命周期受控修复已落源码,缺源和 Symbol R 准入继续 BLOCKED。没有新的设备 Fast 证据。** + +## 1. 当前事实与范围 + +仓库 `/Users/tei/dev/MapLibreParticleLab`,`particle/main@e2d202b560ad0165101546597c43f2ef0ccf4542`,Darwin 24.6.0 / x86_64。起点 tracked clean,只有 STEP-110/111 和本次 STEP-116 任务书未跟踪。版本仍 `13.6.1-particle.23`,不能用版本号相同推断 APK 源码相同。Integration main `80667d6c343411d03a760425438e3d292f452118` 为用户提供的 RDC 核验事实,本轮只读相关规范,没有 fetch 或跨仓写入。 + +已读取 AGENTS、PARTICLE_BASELINE、项目规范全部八文件、总则 README 和四份有效总则、STEP-116 任务及 STEP-112/113/114/115 当前记录/报告和相关源码;Integration AGENTS、README、WORKFLOW、TEAM_MODEL、contracts、rfc、adr、instructions、status、compatibility 只读。本轮不使用子代理,不修改公开头文件、公开 API/ABI、Host/RN/JNI 控制协议、NavSea 或公网 Style。 + +## 2. 缺源 R:证据成立,空依赖准入不实施 + +保留文件 `artifacts/maplibre-particle/android-validation/step113/source-change-after-full.json`,SHA-256: + +`24a06fa1cb072a628c650723c58a880895ec656f4972fa55d3cb064bfb2aabda` + +Native 原层 index=2、ID=`tide-spots`、type=`symbol`、source=`tide`。R[0,63) 依赖 `mapple/navsea_delivery/tide`。Native 实际 Source 表是 `mapple:raster`、`navsea_delivery:vector`、`org.maplibre.annotations:annotations`、`rn-weather-fields:raster-array`、`step113-source-probe:geojson`;没有 tide。该文件属于旧 SOURCE_CHANGE 完整窗口,不冒充本次 STATIC_RC 实测。原记录中 fallback=unknown-dependency;当前 STEP-114 代码已提前报 missing-source。 + +`RenderOrchestrator::retainedSegmentSourceState` 在查 revision 前先检查缺源及 CustomMVTVector;普通 Vector 不走 CustomMVTVector 拒绝分支。`renderSegmentDependencies` 不因隐藏或当前桶为空删依赖。`RenderSegmentRevisionBook::revisionFor` 对未登记来源返回空值,保留删除墓碑用于同名重建;墓碑存在不能代替当前 Source 的 readiness。 + +源码链路审查: + +| 链路 | 当前实现与限制 | +|---|---| +| 零贡献候选 | createRenderTree 的 source/layer 匹配循环不会给缺源层生成 LayerRenderItem,updateList 默认 false;这支持“本轮没有对应新 RenderItem”,不等于全部旧 GPU 对象已完成禁用的证书 | +| 旧 GPU 生命周期 | RenderLayer::markLayerRenderable 调用 activateLayerGroup 提交 ChangeRequest;Symbol 还处理 collisionTileLayerGroup,layerRemoved 停用两类 group。CPU 状态、待处理 requests、旧 buckets 与实际执行时点需要一起核验;无 source 的循环边界也不能只靠 bool 推断 | +| Source 创建/删除/同名恢复 | createRenderTree 的 sourceDiff 递增全局内容及对应 source revision;prepare 输入的 immutable sources 身份变化使 NonCameraSignature 不匹配,必须先 Full | +| 同名更新/异步完成 | changed sourceDiff、onTileChanged → TileMutation → 内容/source revision + observer invalidate;feature-state 同样走 source 变更,不能按 HTTP 下载或 loaded 时间代替 | +| 全局输入 | layerDiff / 层序、imageDiff、Glyph URL/fontFaces、onGlyphsLoaded、Sprite/ImageMissing、context/style cleanup 保留原失效链 | + +因此本轮**没有把 missing-source 当 empty**,没有登记虚构 tide revision,没有补假源或删除 tide-spots。后续需要“该 R 在成功 Full 中实际参与 GPU 绘制的 group/碰撞 group 已排除、无待应用资源变化”的有界证据,以及 source 出现/恢复后下一次尝试必定 Full 的回归。当前仅 Source 表缺失不足以承担这项新准入契约。服务器正式 Style 缺源仍是 BLOCKED。 + +本轮只在现有诊断 JSON 的每段增加可选 `missingSourceIds`,由同次 snapshot 的依赖和 Source 表计算,包含缺失来源的具体 ID。查询不写 revision、不构造缓存、不修改首错和 24 Gate。 + +## 3. 空 Annotation C:局部修复与保护 + +旧 STATIC_RC Native 66 层:原63层 + weather-color + raster-particle + 自动 points Symbol。C 从 rn-weather-color 开始,普通 Symbol 不支持 particle dynamic range,阻挡 #22,并影响聚合 #23。SOURCE_CHANGE 的额外 circle 也必须保留。 + +仅在 Step113PbfGateScene 的 STATIC_RC 第二次 Style 加载完成时执行一次空层调整: + +1. 生产基础 ID 不含 points,生产Sources不得占用org.maplibre.annotations;SDK `map.annotations` 必须为空;实际层必须是 SymbolLayer、sourceId 为 org.maplibre.annotations,内部 Source 必须存在。来源由本次原始JSON无内部ID、Native加载路径与实际层共同核对,避免把生产同名Source误当空SDK来源。 +2. 使用 `Style.removeLayer(Layer)` 对象重载,保留可重加的 wrapper;Source 保留,原63层定义/顺序及生产 Sources 原样。 +3. 记录 `emptyBuiltinAnnotationLayerRemoved`、实例数0、Source保留、移除层ID/作用范围,保存 productionSources;poll/export 继续使用真实 Native 计数。静态预期正常 STATIC_RC 65 层,不能当实际计数。 +4. 不周期删除任何层。后续普通 Style.addLayer 保持原行为,native immutable layer/signature/topology 变化继续 Full;Style 更换后不对新 Style 再做移除。 +5. stop 在相同 Style 且该 ID 仍缺失时重加原 wrapper,已有同名层不覆盖。Style 身份改变或 Annotation/points 重新出现时,3秒 poll 撤销场景验收条件并报 BLOCKED;poll 是报告保护,不承担恢复可见内容的时序保证。 + +关键修复位于内部 `src/mln/annotation/annotation_manager.cpp/.hpp`:原 updateStyle 只在 Source 不存在时创建 points,Source 保留而层被删除会让后续 Marker 无绘制层。提取私有 ensurePointLayer,独立检查 Source 和层;首次添加 SymbolAnnotation、更新已有 SymbolAnnotation、Style loaded 三个入口修复缺失层。更新没有 geometry/icon 变化时也把 dirty 置真。已有同名 Layer 不被覆盖。 + +时序:Map::addAnnotation / updateAnnotation → AnnotationManager 修复 Style layer vector → Map::Impl::onUpdate 捕获新的 immutable layers → Fast NonCameraSignature/annotationMutation 拒绝旧树 → Full 执行正常 Annotation 渲染。恢复不依赖3秒poll,也没有在 renderer thread 的 updateData 内重写 Style。Shape Annotation 原接口/图层仍按原逻辑处理,碰撞、Fade、Glyph/Image 和 C 判断完全保留。 + +风险:这是 Shared Core 内部生命周期修复,所有后端共用;需 RDC 编译并跑真实图像用例,确认无锁/重入问题及旧 Annotation 行为回归。用户主动提供同名层时尊重其语义;若其配置本身无法绘制,该情况不被当作 Test App 空层安全条件。恢复失败在日志保留,不写成功。 + +## 4. Symbol R:首次 Build 仍 BLOCKED + +已读 PlacementController、Placement::placeLayers/commit/symbolFadeChange、RenderTreeImpl::prepare、SymbolBucket::updateVertices、renderer Full 准入和 scene 聚合。 + +| Continuous Full 分支 | 当前 Symbol R gate | +|---|---| +| placement 不 recent | placementChanged=true;placeLayers 后 commit、setPlacement,changed 本身阻断 | +| placement recent | setPlacementStale;hasTransitions 返回 true,阻断 | +| transitions 关闭 | placementIsRecent=false,Full changed=true;symbolFadeChange 返回1,不是 settled=0 | + +RenderTreeImpl::prepare 在 bucket updateVertices 中更新 opacity/dynamic attributes,并置 uploaded/dynamicUploaded/placementChangesUploaded 等状态。Placement commit 只提交 CPU placement/opacity 结果,不证明当前 R 的最终 GPU 输入一致。per-R `renderSegmentPlacementGatePassed` 和 `retainedSegmentIsStable`,全局 retainedPlacementStable / retainedSupported / preparedSceneSupported 均保留。隐藏/缺源/当前 placementData 为空的 Symbol 仍结构上具备 Placement 能力。 + +**需要的额外设计证据**:成功 Full 对应的 GPU 上传/绘制完成代次、R 使用的 bucket/tiles/跨瓦片ID与排序、跨Source碰撞与所有参与碰撞层的 Placement结果、text/icon opacity和dynamic vertices、atlas/glyph/image代次、Style/Source/layer identities与revision、camera/projection/viewport/pixelRatio、uniform fade及debug/3D条件、待处理requests/异步变更纪元。证书必须绑定一次成功 Cache Build,变化即 invalid/Full,并对 GL/Metal 验证等价及 shadow。现有代码没有完整证书,本轮不做跨层重写,不用毫秒阈值、loaded、无手势或counter装配代替。 + +## 5. 测试源码与结论边界 + +新增四个 `Step116Safety.*` C++ 用例:缺源/Source加入未loaded/异步revision/删除墓碑和恢复;JSON缺源审计与Placement并存、查询无副作用;Symbol两个Continuous分支及关闭transition保护、无变化/Source与全局资源revision;Annotation/层序/disable/reload多失败 shadow 全集。revision用例验证的是账本契约,不伪造实际 Glyph/Tile 回调或 GPU完成。 + +新增三个 `Annotations.Step116*` 用例:空points移除后真实添加在render前恢复并比对既有point_annotation图像;已有Marker no-op update恢复且出图;已有同名层不覆盖、reload恢复且出图。这些是测试源码,全部 NOT RUN;设备的annotation/Style、async mutation、GL/Metal回退还须独立验收。 + +本轮实现后预期消除原 STATIC_RC 的空内部 C Symbol;无法承诺 #23 清除,也无法承诺 R Build/Fast>0。旧快照、AST或diff检查均不构成新制品安全、像素或性能 PASS。 diff --git a/docs/40_任务/STEP-116_STEP113原PBF_FastPath实际阻塞修复_Codex任务_v1.md b/docs/40_任务/STEP-116_STEP113原PBF_FastPath实际阻塞修复_Codex任务_v1.md new file mode 100644 index 000000000..b0d9cb153 --- /dev/null +++ b/docs/40_任务/STEP-116_STEP113原PBF_FastPath实际阻塞修复_Codex任务_v1.md @@ -0,0 +1,26 @@ +# STEP-116:STEP-113 原 PBF Fast Path 实际阻塞修复 + +## 授权、当前基线与配置 +用户要求继续修复,并以原 STEP-113 专用 Android APP 的 STATIC_RC 真正进入 Fast 为目标,而非 STEP-114 简化 FAST_SMOKE。当前仓库 Mac /Users/tei/dev/MapLibreParticleLab,particle/main HEAD e2d202b560ad0165101546597c43f2ef0ccf4542。Integration 已由RDC fetch 并核实 main 80667d6c343411d03a760425438e3d292f452118。仅内部 Shared Core 安全修复与 Android 专用 Test App;不改跨平台公开 API/ABI/协议,不改 NavSea、不发行。 + +Codex 模型必须为 GPT-6.1-Sol High (gpt-6.1-sol / model_reasoning_effort=high)。先读根AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范、当前 docs/00_项目总则 README及四份有效总则,读 STEP-112/113/114/115、Integration instructions/README/WORKFLOW/TEAM_MODEL/contracts/rfc/adr/status/compatibility,按事实和边界执行。Codex只编辑、静态检查、中文报告;不可执行Native/Gradle大编译、ADB、安装、Jest、发版、commit/push。Native独立审计由RDC;Android编译安装由用户手工集中一次完成。 + +## 已确定的真正阻塞 +STEP-113 STATIC_RC:正式63个PBF基础层,Test App Native实际66层;R[0,63) Build0、Fast0、Source fallback unknown-dependency,C[63,66)中有自动添加的 org.maplibre.annotations.points Symbol,#22/#23持续失败。后续 STEP-114 源码/快照发现R依赖mapple、navsea_delivery和tide,Native source表实际没有tide,tide-spots Symbol层引用它;navsea_delivery实际是Vector,不是CustomMVT。即使R的missing-source得到解决,Symbol R首次cache Build仍被Placement的changed/stale/fade保护阻挡。不可把去掉一个Gate误写为全套修好。 + +## 工作:真正解决而非假通 +1. 通过静态源码和保留的 STEP-113完整JSON逐环确认R缺源的原层及fallback、真正的 SourceType、source/Style变化导致的revision与GPU生命周期。 +2. R的缺失源:只有能证明缺失源永远贡献零像素,且后续Source创建/恢复/同名更新强制缓存失效/Full重新处理时,才允许把它作为有界的empty依赖;否则保留fail-closed并明确报告服务器正式Style存在缺源而无法安全缓存。不能偷偷创建假的tide Source或删除原始tide-spots层。 +3. C的内部Annotation Symbol:可以在专用 Test App 确认地图无任何Annotation实例时,参照 STEP-114 仅移除 SDK 自动创建的空内部 Annotation 层,保留原正式63层定义、Source/Layer顺序不变;必须有代码防止遗留Annotation/Style添加时漏内容,并在日志、manifest明确记录原STYLE层原样、仅内部辅助层调整。其他已有原场景继续原语义,不为了Fast删除SOURCE_CHANGE中的测试circle。未经证明不能改Core C准入逻辑。 +4. Symbol R初建:必须读PlacementController、onRender、commit/fade、renderSegmentPlacementGatePassed、retainedRangeIsStable和full-frame时的状态。真正放宽必须有结果等价的Placement/atlas/glyph/tile/style/source/camera/opacity证据,并在变更时立即invalid/full;不能仅凭stable多少毫秒、场景没有手动操作、tile加载完成来放行。若现有Core没有可核实的已提交GPU结果签名,禁止危险跨层重写,列为BLOCKED并说明需要的精确额外数据/设计。 +5. 要保留24Gate shadow mismatch=0与实际Cache Build/Reuse/Full/Fast原生计数。可添加同帧诊断或快速失败细项,便于用户一次Android build后通过原STATIC_RC脚本迅速定位仍失败的Gate。保留PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE五场景、STEP114独立Smoke和STEP102原场景;不要改变公网PBF Style或NavSea,也不要求精确113层。 +6. 增加与安全修复对应的非Jest C++源码测试,含缺源/源加入/async mutation、Annotation、Symbol R /全局变化/无变化、Style/Source/Tile/Glyph/Image以及Layer顺序/disable/reload;不要假造rendered Fast、不要把不运行测试记PASS。 + +## 交付文件,全部本轮必须实际生成 +- 本轮实际源码和适当非Jest测试源码;修改Test App后更新必要README。 +- docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md:每条阻塞证据/解决方案/风险/未解项。 +- docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md:真实完成度及未运行项目。 +- docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md:严格按最新HEAD一次用户手工Android build/install→STEP115 preflight→原STEP113 STATIC_RC 75s以上→完整120帧/24Gate/源码/设备SHA/用户画面;失败后如何判断R/C/Symbol,不得依赖旧APK。 +- docs/报告/STEP-116_STEP113原PBF_FastPath实际修复_Codex完成报告_v1.md:实际模型全名档位、改动清单、公共API(必须不变)、逐项Implemented/Blocked/Not Run、Git/源码证据、RDC具体命令及用户构建/设备步骤。 +- 粒子开发记录.md追加中文结果与下一步;必要README与相关已有文档说明保持一致。 +- Codex运行git diff --check、静态核查;不运行Native编译、Android编译、Jest、ADB、安装、Git commit/push;本轮用户原有两个未跟踪STEP-110/111 MD一律保护。禁止git add -A/.、reset --hard、clean。完成后停止由RDC独立审计并scoped提交推送;若Fast尚未实测只能PARTIAL归档,不写Closed/PASS。 diff --git a/docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md b/docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md new file mode 100644 index 000000000..1c4c7e823 --- /dev/null +++ b/docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md @@ -0,0 +1,54 @@ +# STEP-116 STEP-113 原 PBF Fast Path 实际修复 v1 + +日期:2026-10-11(JST)。状态:**部分实现(PARTIAL),停止于源码交付,待 RDC 独立审计。** + +## 当前基线 + +- Mac / Darwin 24.6.0 / x86_64;`/Users/tei/dev/MapLibreParticleLab`。 +- branch `particle/main`,起点 HEAD `e2d202b560ad0165101546597c43f2ef0ccf4542`,本轮不提交,HEAD 不变。 +- 实际模型 GPT-6.1-Sol(gpt-6.1-sol)/ high,从当前 rollout 的 turn_context 核实;没有切换或第二实例。 +- Native Android 版本 `13.6.1-particle.23`,没有发版;Integration main 80667d6c 为用户给定 RDC 核验,相关文档只读。 + +## 交付与未完成 + +| 工作 | 状态 | 事实 | +|---|---|---| +| 强制前置/实际代码/旧完整JSON调查 | 已完成 | 原R缺 tide,navsea_delivery 为 Vector;GPU/source生命周期见审查文档 | +| 空Annotation生命周期 | 已实现,未验收 | Native add/update/style-loaded 恢复缺失points;Test App仅STATIC_RC一次移除空辅助层,保留Source,退出恢复,后续变更撤销验收条件 | +| 原63层/PBF资源/五场景 | 已保留源码语义 | 不改公网或tide-spots;SOURCE_CHANGE circle保留;STEP114/102源码冻结 | +| 缺源empty准入 | BLOCKED | 缺GPU零贡献证书;继续missing-source,不补假源 | +| Symbol首次Cache Build | BLOCKED | 无绑定成功Build的Placement/GPU最终输入证书,现有Gate全部保留 | +| 诊断与复测工具 | 已实现,未运行 | JSON缺源ID +独立Placement摘要;75秒实际等待、源码锁定/生命周期/blocked/24Gate拒绝条件 | +| 非Jest C++测试源码 | 已实现,NOT RUN | 四个Step116Safety、三个Annotations.Step116;真实图像检查交RDC | +| 四份中文文档/README/开发记录 | 已生成 | 具体路径见完成报告 | +| 真正Fast/像素/性能 | NOT VERIFIED | 没有新APK、设备计数、截图或FPS/CPU结论 | + +## 检查与收尾 + +Codex仅执行阅读、Git只读状态/diff、旧JSON与冻结文件SHA、Python AST解析、git diff --check及文档静态检查。无 CMake/Ninja/Bazel/Native编译或测试、Gradle/ADB/安装/Jest、commit/push/发布;没有改构建流水线、公开头文件/API/ABI/Host控制协议。 + +保护 STEP-110/111 未跟踪任务书。下一步 RDC 按 `docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md` 独立审计并锁定提交后的源码,用户集中一次手工 build/install,再执行 STEP115 preflight、原 STATIC_RC ≥75秒、完整120帧和24Gate、SHA及画面。不得使用旧APK或旧测试runner给本轮记PASS。 + +## RDC 后续结果填写(本轮尚未执行) + +- 审计后的源码HEAD/精确diff/SHA:待填写。 +- Native配置/编译/定向测试及首错/日志:NOT RUN。 +- 最新用户构建APK SHA、build-info、设备安装SHA:NOT RUN。 +- STATIC_RC实际层数/空层调整/原63层manifest/Source:NOT VERIFIED。 +- Cache Build/Reuse、Full/Fast增量、120帧/24Gate/mismatch:NOT VERIFIED。 +- Annotation恢复、Source/async/Style/资源/层序/disable/reload回退及用户画面:NOT VERIFIED。 +- 最终阶段结论:当前PARTIAL;缺源与Symbol仍BLOCKED。 + + +## RDC 独立核验追加(2026-10-11) + +**本轮结论:限定范围源码/接口及操作说明静态审查完成;Mac Native配置 BLOCKED;Android APK/设备Fast NOT RUN,整体PARTIAL。** + +- 起点粒子仓库 particle/main=e2d202b560ad0165101546597c43f2ef0ccf4542;Integration origin/main已由RDC fetch并与本地main同步,HEAD=80667d6c343411d03a760425438e3d292f452118。未接触NavSea与Integration正式文件。 +- Codex实际模型 gpt-6.1-sol/high;本轮实现仅原STEP-113 STATIC_RC 的 SDK 空Annotation辅助层、Native AnnotationManager缺层恢复、缺源只读JSON诊断和测试验证器,不改变正式63个PBF layer、原五场景其他分支、Fast Gate/caching的准入或公开控制面。新增7项非Jest C++用例源码,尚未执行。 +- RDC直接核对 Android Style.java 对象removeLayer(Layer)文档明确注明被移除Layer可以重新添加;与STEP116在stop中恢复原wrapper的目的相符。Native AnnotationManager恢复与事件生命周期仍须编译、图像和设备验证,不凭静态阅读标PASS。 +- RDC核对旧SOURCE_CHANGE实际完整JSON:Native FastGate gates每项有order,firstFrame的knownMask等字段存在;STEP116验收脚本的原JSON结构检查有数据来源,但并非本轮运行证据。 +- Mac shell当前无rg可执行,RDC将STEP116手动导出命令中的rg替换为标准grep -E,保留同样的文件筛选表达式。Python验收器ast.parse通过,git diff --check通过。 +- Mac Native按当前标准命令重新执行真正的cmake --preset macos-metal配置(使用/Users/tei/Library/Android/sdk/cmake/3.31.6/bin/cmake),退出码1:Required package libuv缺失,日志保存artifacts/maplibre-particle/step116-native/configure.log。严格串行停止,未执行构建目标、未运行7项C++测试。失败是环境依赖,不应改写成代码编译通过/失败或跳过继续。 +- 未执行Android Gradle、APK安装、ADB启动STEP113、新Fast计数、逐帧画面、iOS Metal回归、发行/tag/Source Lock。正式PBF缺tide以及Symbol R Placement仍保留fail-closed;此任务只是安全部分实现,并不解决原STATIC_RC无法进入Fast的全部原因。 +- 用户在本轮源码scoped Git锁定且push远端核验后,需手工一次集中Android编译与install -r,再执行STEP115同HEAD/安装SHA预检、原STATIC_RC 90秒、完整120帧+24Gate+截图与其它负场景验收。新APK前禁止把旧c6d13e1产物Fast0作为本轮修复验收。若仍Fast0,必须保留缺源、Placement和C实际结果并交跨平台Core负责人确定GPU结果证书。 diff --git a/docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md b/docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md new file mode 100644 index 000000000..33f7f96c7 --- /dev/null +++ b/docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md @@ -0,0 +1,151 @@ +# STEP-116 STEP-113 原 PBF 专用 APP 验收 v1 + +日期:2026-10-11(JST)。**以下命令交 RDC / 用户执行,Codex 本轮全部未运行。** 本轮源码状态 PARTIAL;缺 tide 与 Symbol R 准入仍阻塞,预期仍可能 Fast=0。不得拿 STEP114 Smoke 或旧 V23 APK 替代原 STATIC_RC。 + +## 1. RDC 源码锁定和 Native 定向验证 + +先重新读取 AGENTS、当前基线、STEP-116 审查/报告与 diff。审计包含 Native Annotation 修复的线程/生命周期、原63层逐层定义/顺序、移除对象重载、stop恢复、后续marker/style添加、五场景分支、生产Gate未变和七项新测试。执行只读命令: + +```bash +cd /Users/tei/dev/MapLibreParticleLab +git branch --show-current +git rev-parse HEAD +git status --short +git diff --check +git diff -- src/mln/annotation/annotation_manager.cpp src/mln/annotation/annotation_manager.hpp src/mln/renderer/render_segment_json.hpp test/api/annotations.test.cpp test/renderer/retained_underlay_cache.test.cpp platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt tools/particle_android_validation/verify_step113_fast_path.py tools/particle_android_validation/README.md 粒子开发记录.md +shasum -a 256 docs/40_任务/STEP-110_R0_Placement跨段归因只读调查_Codex任务_v1.md docs/40_任务/STEP-111_R段Placement结果等价与独立刷新可行性只读调查_Codex任务_v1.md +``` + +两任务书 SHA 应分别为 `33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`、`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`。不要暂存它们。RDC按获授权的scoped流程收尾后,记录真正的最新HEAD;本报告起点 e2d202b 并不包含本轮修改。构建前 tracked 必须 clean;原未跟踪任务书可保留,build-info 的 git_dirty 不能单独用来推断源码不一致。 + +Native 定向命令逐项执行,上一项失败立即停止,保存完整 stdout/stderr 与首个真实 error;历史缺 libuv 不当成新结果,不运行旧runner: + +```bash +cmake --preset macos-metal +``` + +```bash +cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 +``` + +```bash +build-macos-metal/mbgl-test-runner --gtest_filter='Step116Safety.*:Annotations.*:FastGateTrace.*:ParticleFastPath.*:RenderSegmentDiagnostics.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*' --gtest_color=no +``` + +Annotations.Step116 三用例使用现有 point_annotation 图像做实际渲染比较;不能仅检查 getLayer 指针就声称像素 PASS。账本/谓词用例不构成 GPU rendered Fast,Metal host 成功也不代替 Android OpenGL/设备回退。若依赖/编译阻塞,保持现场并在报告标 BLOCKED/NOT RUN,Android集中构建是否继续由用户安排。 + +## 2. 用户一次手工 Android 构建与安装 + +在锁定的最新 HEAD 上,用户手工依序执行,Codex/RDC不得代用户启动 Android 构建或安装: + +```bash +cd /Users/tei/dev/MapLibreParticleLab +git rev-parse HEAD +git status --short +./tools/particle_android_validation/build_validation_apk.sh +``` + +标准脚本使用 Java17、OpenGL/x86_64、strip native debug symbols,保存 build-info/build.log,检查 ZIP/ABI/体积/SHA。失败不得续用原路径旧 APK。 + +```bash +cat artifacts/maplibre-particle/android-validation/build-info.txt +shasum -a 256 artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +``` + +记录源码HEAD、branch、精确修改文件/SHA、构建日志、APK大小/ABI/SHA、设备序号及型号。不能仅凭 `13.6.1-particle.23` 或安装 Success 认定是新源码。 + +## 3. STEP115 preflight → 原 STATIC_RC ≥75秒 + +用户安装成功后,由 RDC / 用户执行,serial/adb路径明确: + +```bash +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --adb /Users/tei/Library/Android/sdk/platform-tools/adb --serial 192.168.200.139:5555 --preflight-only +``` + +preflight 检查当前 tracked clean、build-info.commit==HEAD、APK SHA==build-info.sha256、设备 ABI x86_64、安装包 SHA==本地 APK。STALE_APK / SOURCE_NOT_LOCKED / SHA失败时停止,必须先纠正版本。PREFLIGHT_PASS只代表版本一致。 + +```bash +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --adb /Users/tei/Library/Android/sdk/platform-tools/adb --serial 192.168.200.139:5555 --scenario STATIC_RC --seconds 90 +``` + +使用90秒,脚本在取得 PID 后实际等待完整90秒(≥75秒);加载太慢导致摘要不足则 INSUFFICIENT_SAMPLES / NO_READY_SUMMARY,不推断Fast。保留 result.json、logcat.txt、launch.txt、实际观察秒数。摘要须同sceneGeneration/lifecycle、planValid/basePreserved、24项、mismatch=0、累计计数单调;独立blocked日志和摘要撤销不能被早先Fast盖过。退出2为没有可接受Fast证据,退出3为版本/环境等阻塞,不能改成0制造PASS。 + +`FAST_OBSERVED`仅指真实 Native fastRendered 增量>0,不是安全/像素/性能终验。要求原63层manifest、NavSea原PBF/Sprite/Glyph/Vector URI及定义/顺序不变,仅声明SDK空辅助层调整;正常静态预期Native65层/C仅weather-color和raster-particle,实际以新导出为准。PUBLIC PBF若变成非63层,场景保留现场并 BLOCKED,不更换简化Style来通过。 + +## 4. 手动导出完整120帧及设备画面 + +保持同次 STATIC_RC 运行,用户在 STEP093 → STEP113 菜单选择 EXPORT LAST SNAPSHOT。这是最近一次3秒共享采样,不额外渲染。以下由 RDC / 用户回读,必须核对所选文件确属本次新运行: + +```bash +mkdir -p artifacts/maplibre-particle/android-validation/step116 +step116_file="$(/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 shell run-as org.maplibre.android.testapp ls -t files | tr -d '\r' | grep -E '^step113-STATIC_RC-.*[.]json$' | head -n 1)" +test -n "$step116_file" +/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 exec-out run-as org.maplibre.android.testapp cat "files/$step116_file" > artifacts/maplibre-particle/android-validation/step116/static-rc-full.json +shasum -a 256 artifacts/maplibre-particle/android-validation/step116/static-rc-full.json +/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 exec-out screencap -p > artifacts/maplibre-particle/android-validation/step116/static-rc-screen.png +/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 logcat -b crash -d > artifacts/maplibre-particle/android-validation/step116/crash.log +``` + +`test -n`失败立即停止后续命令。同一个Full JSON先做离线证据结构检查(不是安全测试): + +```bash +python3 -B - <<'PY' +import json +from pathlib import Path +p = Path('artifacts/maplibre-particle/android-validation/step116/static-rc-full.json') +v = json.loads(p.read_text()) +assert v['scenario'] == 'STATIC_RC' and v['sceneReady'] and not v['blockedReason'] +assert v['commands']['emptyBuiltinAnnotationLayerRemoved'] is True +assert v['commands']['annotationInstancesAtRemoval'] == 0 +assert v['commands']['annotationSourcePreserved'] is True +assert v['styleParity']['baseLayerCount'] == 63 and v['styleParity']['baseLayersPreserved'] +assert not v['styleParity']['full113Equivalent'] +n = v['native']; t = n['fastGateTrace']; frames = t['frames'] +assert n['valid'] and len(frames) == t['windowCount'] == 120 +assert len(t['gates']) == 24 and t['decisionMismatch'] == 0 +assert [g['order'] for g in t['gates']] == list(range(1, 25)) +assert all(f['knownMask'] == 0xffffff and f['decisionConsistent'] and len(f['gateStatus']) == 24 for f in frames) +assert all(a['sequence'] < b['sequence'] for a, b in zip(frames, frames[1:])) +complete = [f for f in frames if f['completed']] +assert complete and all(f['post'] is not None for f in complete) +print('结构完整;仍须独立核对源码/制品SHA、同次generation/lifecycle及用户画面') +print('生命周期 Full/Fast:', t['fullRendered'], t['fastRendered']) +print('窗口 completed/Full/Fast:', len(complete), t['windowFullRendered'], t['windowFastRendered']) +for s in n['segments']: + if s['strategy'] == 'RETAINED': + print('R', s['beginIndex'], s['endExclusive'], s['missingSourceIds'], + s['placementGatePassed'], s['cache']) +print('窗口失败项:', [(g['order'], g['reason'], g['windowFailures']) for g in t['gates'] if g['windowFailures']]) +PY +``` + +允许最后一帧 pending,但不能把其post作为当前Full。pre为上次完成树、post/full为本次完成树,避免跨帧拼状态。环形窗口可能覆盖Mutation瞬间,必须变更前/后分别及时手动导出,若没捕获则如实 NOT VERIFIED。120条帧不等于120条completed。 + +用户确认基础PBF/标注/天气色带/粒子可见、无漏内容/闪烁/层序错误;截图归档及用户判断不能代替同条件Full/Fast像素比较。当前缺源/Placement未解时应记录 Fast0 和每项失败,不宣布性能提升。 + +## 5. 失败定位与受保护回归 + +| 结果 | 判断与下一步 | +|---|---| +| missingSourceIds含tide,R Build0 | 正式Style缺源仍fail-closed;不补假源、不删tide-spots,记录Source类型/缺失层ID | +| 缺源消失但Symbol placementGatePassed=false | 独立Symbol准入阻塞,检查Full changed/stale/fade与pre/post;不要用等候时间放行 | +| #22仍失败 | 查Native C范围实际层清单;空层调整是否被撤销/失败,普通circle或真实Annotation仍应阻挡 | +| #23仍失败 | 查retainedSupported和preparedScene聚合,消除#22不保证#23通过 | +| mismatch>0 / unknown Gate /版本不符 | 停止记录通过,保留首错和完整证据 | +| Fast>0 | 先核查没有缺源/场景撤销、原Style完整,继续资源变化/回退/画面对照;仅记FAST_OBSERVED | + +原五场景仍在。每项独立运行并保留导出,不连续启动冒充采样: + +```bash +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario PBF63 --seconds 90 +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario PARTICLE_OFF --seconds 90 +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario PARTIAL_RCRR --seconds 90 +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario SOURCE_CHANGE --seconds 90 +``` + +这些场景可能预期Fast0,脚本退出2是受保护负例结果,不自动视为回归失败或改预期。SOURCE_CHANGE保留circle与Annotation;用户执行一次原 MUTATE PROBE SOURCE ONCE,导出前后commands和native lastMutation/affectedSegmentIndices,区分同时发生的真实瓦片更新。 + +Native七项新测试之外,还需 RDC/用户记录:遗留Annotation拒绝移除;STATIC_RC后添加真实Marker(同步恢复points再Full、像素可见),no-op更新;额外Style.addLayer/重排不删除新内容;disable、退出到STEP102、Style reload及后台恢复;Source创建/同名更新/async Tile、Glyph/Image变化必定invalid/Full。Test App没有新增自动Mutation来假装覆盖这些事件;若当前UI无法触发,标该项设备NOT VERIFIED并使用对应Native真实测试/另行授权的操作。STEP114四场景回归使用其既有命令;Smoke结果只归其独立场景。 + +最终提交证据应包含最新HEAD/精确源码diff、Native日志/首错、用户build-info/build.log/APK及安装SHA、设备信息、原STATIC_RC摘要和完整JSON、manifest、24Gate/真实Build/Reuse/Full/Fast增量、回退与画面。编译成功、preflight成功或静态检查都不把STEP-116判为CLOSED/PASS。 diff --git a/docs/报告/STEP-116_STEP113原PBF_FastPath实际修复_Codex完成报告_v1.md b/docs/报告/STEP-116_STEP113原PBF_FastPath实际修复_Codex完成报告_v1.md new file mode 100644 index 000000000..6f2746c34 --- /dev/null +++ b/docs/报告/STEP-116_STEP113原PBF_FastPath实际修复_Codex完成报告_v1.md @@ -0,0 +1,112 @@ +# STEP-116 STEP-113 原 PBF Fast Path 实际修复 Codex 完成报告 v1 + +日期:2026-10-11(JST)。**源码交付 PARTIAL;原 STATIC_RC Fast 未实测,缺 tide 与 Symbol R 准入仍 BLOCKED。已按授权完成可控修改后停止,交 RDC 独立审计。** + +## 1. 实际模型与工作区 + +- 实际模型全名 **GPT-6.1-Sol(gpt-6.1-sol)/ high**。读取本地会话 `01a1286a-d6a4-7e32-b91a-a9f3e63d8f3e` rollout 的 `turn_context.model/effort` 得到 gpt-6.1-sol/high;未切换模型、无子代理或第二Codex进程。 +- 机器 Darwin 24.6.0 / x86_64;仓库 `/Users/tei/dev/MapLibreParticleLab`,branch `particle/main`。 +- 起点/交付HEAD:`e2d202b560ad0165101546597c43f2ef0ccf4542`,未commit/push,未回读远端HEAD。起点tracked clean,STEP-110/111/116任务书未跟踪;子模块状态正常,未操作。 +- Android Native版本仍 `13.6.1-particle.23`。没有新APK/AAR,不能使用原STEP113 APK代表此次源码。 +- Integration main `80667d6c343411d03a760425438e3d292f452118` 采用用户给定RDC事实,只读AGENTS/规范/contract/coordination;没有NavSea/Integration写入、同步或发行。 +- 已读强制前置:根AGENTS/PARTICLE_BASELINE、全部项目规范、总则README及四份有效总则、STEP112/113/114/115现行交付及STEP116任务、相关实际实现和保留完整JSON。详细范围见调查文件。 + +## 2. 已实现 / 阻塞 / 未运行 + +| 项目 | 状态 | 结果 | +|---|---|---| +| 空SDK Annotation C | 已实现(Implemented),未验收 | STATIC_RC确认零实例/内部身份后仅移除一次points,保留Source、原63层;stop恢复,后续添加/Style替换撤销验收条件 | +| 后续真实Annotation不漏内容 | 已实现,未验收 | 私有ensurePointLayer在Native Symbol add/update及style-loaded同步恢复缺层;no-op update也dirty,在Map::onUpdate采样前完成,poll只报告,不承担恢复时序 | +| 缺源R | 阻塞(Blocked) | tide-spots依赖不存在tide;缺已执行GPU零贡献证书,不实现empty准入,维持missing-source | +| Symbol R Build | 阻塞 | Continuous重算changed、不重算stale两分支均阻断;无最终GPU/Placement/atlas等证书,不放宽Gate | +| 诊断/原生计数 | 已实现 | 可选missingSourceIds、独立Placement状态、真实Fast UI;24Gate及生产首错/计数不改 | +| STEP115复测工具 | 已实现,未运行 | tracked源码锁定;PID后实际≥75秒等待;同生命周期/24Gate/blocked撤销/计数reset拒绝,仍校验APK与设备SHA | +| C++测试源码 | 已实现,未运行(Not Run) | 四个Step116Safety账本/诊断/保守Gate用例,三个Annotations.Step116真实渲染回归 | +| 真正原PBF Fast、视觉/性能 | 无法验证 | 没有新构建、设备120帧或Fast增长,不写Closed/PASS | + +未删除tide-spots、SOURCE_CHANGE circle、任何正式Symbol;没有改FPS/forecast/原PBF URI、Core C准入、Placement算法、cache成功计数或制造Fast。STEP114独立Smoke和STEP102 fixture未修改。非STATIC_RC四场景保留原Annotation语义;所有场景共享的Native Annotation修复仅在缺Source/points时补回,已有Layer不替换。 + +## 3. 实际文件清单 + +修改的源码/测试/工具: + +1. `src/mln/annotation/annotation_manager.cpp`:缺失points的同步恢复与no-op更新dirty。 +2. `src/mln/annotation/annotation_manager.hpp`:仅私有ensurePointLayer方法,无数据布局变化。 +3. `src/mln/renderer/render_segment_json.hpp`:同snapshot的每段missingSourceIds只读诊断。 +4. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt`:STATIC_RC空辅助层一次调整、恢复/撤销、manifest及独立阻塞摘要。 +5. `test/api/annotations.test.cpp`:三项实际出图/恢复/不覆盖/reload测试源码。 +6. `test/renderer/retained_underlay_cache.test.cpp`:四项缺源/async revision/资源全局/保守Symbol/多Gate诊断测试源码。 +7. `tools/particle_android_validation/verify_step113_fast_path.py`:时长和拒绝条件;本轮没有执行脚本。 +8. `tools/particle_android_validation/README.md`:更新当前复测边界并追加STEP116说明。 +9. `粒子开发记录.md`:仅末尾追加本Step。 + +新增四份中文文档(不覆盖历史版本): + +1. `docs/20_调查/STEP-116_STEP113原PBF_FastPath阻塞安全修复审查_v1.md`。 +2. `docs/50_Step记录/STEP-116_STEP113原PBF_FastPath实际修复_v1.md`。 +3. `docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md`。 +4. 本报告。 + +**公开API/ABI与控制协议不变**:无include公开头、C/Java/Kotlin/RN/JNI方法签名、公共配置、版本/发行坐标变更;新增私有方法不改AnnotationManager成员布局。现有可选诊断JSON每段增加missingSourceIds,Test App私有summary增加诊断字段;这是任务授权的观测扩展,不新增Host控制协议或准入契约。 + +## 4. 静态结果与保护证据 + +已经执行:仓库/branch/HEAD/status/submodule只读核对;前置文档及源码阅读;保留JSON解析和SHA;diff人工审查、Python AST解析(没有导入/运行脚本);git diff --check、新增文档空白/围栏和范围检查。最终结果见文末静态收尾。静态检查不等同C++/Kotlin编译或测试通过。 + +未执行:Native/CMake/Ninja/Bazel、链接/codegen/测试程序、Gradle/APK、ADB/安装/Logcat/设备截图、Jest、性能测量、commit/push/远端回读、发布。没有观察到新编译/测试失败,因为未执行;不能推断将通过。 + +保留旧JSON SHA=`24a06fa1cb072a628c650723c58a880895ec656f4972fa55d3cb064bfb2aabda`。 + +- STEP-110未跟踪文件 SHA=`33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`。 +- STEP-111未跟踪文件 SHA=`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`。 +- STEP102 Placement fixture SHA=`e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7`。 +- STEP114 FastSmoke源码 SHA=`94c28a111883fa784b0d9d81515c9e7ce7232d77a51eab48b4954705f1a39eb0`。 + +这些文件保持原样;保护任务书未暂存,开发记录原HEAD字节前缀保留。先前探索个别猜测路径不存在后,均按rg找到真实src/mln路径,未将不存在路径当证据或创建替代文件。 + +## 5. 精确定向交接 + +完整、按失败停止的命令和证据结构位于 `docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md`。必要顺序: + +1. RDC只读审计diff/七项测试、线程和安全边界,按独立授权scoped锁定本轮源码;不得包含保护任务书或无关文件。 +2. RDC Native:`cmake --preset macos-metal` → `cmake --build build-macos-metal --target mbgl-test-runner --parallel 3` → 定向filter `Step116Safety.*:Annotations.*:FastGateTrace.*:ParticleFastPath.*:RenderSegmentDiagnostics.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*`。首项失败即停,不用旧runner。 +3. 用户在最新锁定HEAD手工一次执行标准 `./tools/particle_android_validation/build_validation_apk.sh`,再明确serial手工 `adb install -r`,记录build-info/APK/安装SHA。 +4. RDC/用户:`python3 -B tools/particle_android_validation/verify_step113_fast_path.py --preflight-only`,成功后 `--scenario STATIC_RC --seconds 90`;默认serial为192.168.200.139:5555。版本/源码锁不符停止。 +5. 用户手动EXPORT LAST SNAPSHOT,RDC回读完整120帧,核对24Gate/mismatch0/pre-post、原63层/Source/空层manifest、真实Build/Reuse/Full/Fast;用户画面及安全回退单独验收。 + +预期仍有missing-source/Symbol阻塞。即使C空层修复成立,仍不得保证#23、R Build或Fast成功。缺源empty需要GPU排除/资源时序证据,Symbol需要绑定成功Full/Build的结果证书;当前任务授权不足以通过危险跨层重写弥补这两项。收到真实RDC编译/测试错误后按固定闭环修复,本轮停止于实现交付。 + +## 6. 实际静态收尾 + +- `git diff --check` 退出0,无空白诊断;四份新文档逐份 `git diff --no-index --check /dev/null <明确路径>` 无诊断,围栏成对;验收文档bash代码块仅以 `bash -n` 解析,内嵌Python和复测工具仅AST解析,没有执行命令正文或导入工具。 +- 9个tracked文件修改,统计308行新增/12行删除;另有本轮四份文档新增。起点三份未跟踪任务书仍未跟踪,暂存区为空,branch/HEAD保持原值。 +- 开发记录HEAD原始164685字节前缀完全保留,原SHA=`0adbe3fbe366651e3a79369dbf13d2e494a2b4545e29efaad72b299b43d2f4dd`,仅追加1440字节(8行)。 +- 已逐字与HEAD比较:生产 `particle_fast_path.hpp`、公开 `render_segment.hpp`、`renderer_impl.cpp`、`render_orchestrator.cpp` 和 Activity接线完全未变;STEP110/111/102/114冻结SHA与上节相符,旧完整JSON也未修改。测试源码实际新增7个用例。 + +本轮实现与测试源的交付SHA-256(独立审计后若修正,应重算并记录,不能沿用此表冒充新源码): + +| 文件 | SHA-256 | +|---|---| +| Step113PbfGateScene.kt | d50281713ae551edc9a48b1550254602e4950c9ddca66855bb689620c9f6f2b6 | +| annotation_manager.cpp | 6e0faf6ab585362bfb1ee4d6e79163390041295331d7c8c652156963318bfe6a | +| annotation_manager.hpp | a4c18d12a12dd4be3ddca7c667e9c45da8a57d6108956e605e1e4fe0796932c0 | +| render_segment_json.hpp | 9c1fa02fa0e7a2eccf4aeb8fe346913fb87b62258c662c303db087399cf9ca1d | +| annotations.test.cpp | b316c23c557a46f819895b7ea74a415ff5c833d0751521c28a5dd8251166daf6 | +| retained_underlay_cache.test.cpp | cc1d26540008df3b74cc97fa13e54e77092f2edb596d4c8c56d7a15504bdb109 | +| verify_step113_fast_path.py | 87a797eee0e907078cd8f49dbceb83dd3affda9b00c51cc691c1f4c553eb705e | + +以上只证明交付范围和静态一致性,不证明Native/Kotlin可编译、测试通过或Fast安全。编译、设备、像素及性能仍全部未验收。 + + +## RDC 独立核验追加(2026-10-11) + +**本轮结论:限定范围源码/接口及操作说明静态审查完成;Mac Native配置 BLOCKED;Android APK/设备Fast NOT RUN,整体PARTIAL。** + +- 起点粒子仓库 particle/main=e2d202b560ad0165101546597c43f2ef0ccf4542;Integration origin/main已由RDC fetch并与本地main同步,HEAD=80667d6c343411d03a760425438e3d292f452118。未接触NavSea与Integration正式文件。 +- Codex实际模型 gpt-6.1-sol/high;本轮实现仅原STEP-113 STATIC_RC 的 SDK 空Annotation辅助层、Native AnnotationManager缺层恢复、缺源只读JSON诊断和测试验证器,不改变正式63个PBF layer、原五场景其他分支、Fast Gate/caching的准入或公开控制面。新增7项非Jest C++用例源码,尚未执行。 +- RDC直接核对 Android Style.java 对象removeLayer(Layer)文档明确注明被移除Layer可以重新添加;与STEP116在stop中恢复原wrapper的目的相符。Native AnnotationManager恢复与事件生命周期仍须编译、图像和设备验证,不凭静态阅读标PASS。 +- RDC核对旧SOURCE_CHANGE实际完整JSON:Native FastGate gates每项有order,firstFrame的knownMask等字段存在;STEP116验收脚本的原JSON结构检查有数据来源,但并非本轮运行证据。 +- Mac shell当前无rg可执行,RDC将STEP116手动导出命令中的rg替换为标准grep -E,保留同样的文件筛选表达式。Python验收器ast.parse通过,git diff --check通过。 +- Mac Native按当前标准命令重新执行真正的cmake --preset macos-metal配置(使用/Users/tei/Library/Android/sdk/cmake/3.31.6/bin/cmake),退出码1:Required package libuv缺失,日志保存artifacts/maplibre-particle/step116-native/configure.log。严格串行停止,未执行构建目标、未运行7项C++测试。失败是环境依赖,不应改写成代码编译通过/失败或跳过继续。 +- 未执行Android Gradle、APK安装、ADB启动STEP113、新Fast计数、逐帧画面、iOS Metal回归、发行/tag/Source Lock。正式PBF缺tide以及Symbol R Placement仍保留fail-closed;此任务只是安全部分实现,并不解决原STATIC_RC无法进入Fast的全部原因。 +- 用户在本轮源码scoped Git锁定且push远端核验后,需手工一次集中Android编译与install -r,再执行STEP115同HEAD/安装SHA预检、原STATIC_RC 90秒、完整120帧+24Gate+截图与其它负场景验收。新APK前禁止把旧c6d13e1产物Fast0作为本轮修复验收。若仍Fast0,必须保留缺源、Placement和C实际结果并交跨平台Core负责人确定GPU结果证书。 diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt index 68c24fac4..77f0a1674 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt @@ -13,6 +13,8 @@ import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style import org.maplibre.android.maps.WeatherRuntime +import org.maplibre.android.style.layers.Layer +import org.maplibre.android.style.layers.SymbolLayer import org.maplibre.android.style.sources.GeoJsonSource import org.maplibre.android.testapp.BuildConfig import org.maplibre.geojson.Feature @@ -43,11 +45,14 @@ internal class Step113PbfGateScene( private var exportSequence = 0L private var commandResults = JSONObject() private var savedGestures: List? = null + private var annotationStyle: Style? = null + private var detachedEmptyAnnotationLayer: Layer? = null private val poll = object : Runnable { override fun run() { if (!running || paused) return if (running) { try { + checkAnnotationOwnership() // One shared snapshot query per refresh; all UI/log/export uses this copy. val native = JSONObject(map.getRenderSegmentDiagnostics()) latest = envelope(native) @@ -63,6 +68,7 @@ internal class Step113PbfGateScene( append("STEP-113 $scenario · ${if (ready) "READY/PARTIAL" else failureReason ?: "LOADING"} · actual=${native.optInt("styleLayerCount")}\n") append("first=$firstFailure seq=${lastFrame?.optLong("sequence")} full=${trace?.optLong("fullRendered")} ") append("window=${trace?.optInt("windowCount")} mismatch=${trace?.optLong("decisionMismatch")}\n") + append("Fast=${trace?.optLong("fastRendered")} · 空Annotation调整=${commandResults.optBoolean("emptyBuiltinAnnotationLayerRemoved")}\n") append(rank.joinToString(" / ") { "${it.optString("reason")}=${it.optLong("failures")}" }) for (i in 0 until segments.length()) { val s = segments.getJSONObject(i) @@ -138,6 +144,8 @@ internal class Step113PbfGateScene( .put("deviceReferenceSelector", "用户STEP-112:RCRR_PREFIX[0,13)/[13,33)/[33,107)/[107,113)") .put("baseLayerManifest", layerManifest(base)).put("sprite", root.optString("sprite")) .put("glyphs", root.optString("glyphs")).put("vectorSource", JSONObject(source.toString())) + .put("productionSources", JSONObject(root.getJSONObject("sources").toString())) + .put("internalAnnotationPolicy", "仅STATIC_RC加载时移除空SDK辅助层;后续添加不再移除;原63层及Source原样") val hasField = scenario != Scenario.PBF63 if (hasField) { val sources = root.getJSONObject("sources") @@ -176,9 +184,10 @@ internal class Step113PbfGateScene( if (hasField) "当前NavSea源码RC:rn-weather-color" else "基础63层全RETAINED") .put("requestedPlan", JSONObject(plan)) commandResults.put("planAccepted", map.setRenderSegmentPlan(plan)) - map.setStyle(Style.Builder().fromJson(root.toString())) testStyle@ { + map.setStyle(Style.Builder().fromJson(root.toString())) testStyle@ { style -> if (!running || epoch != generation) return@testStyle try { + if (scenario == Scenario.STATIC_RC) removeEmptyAnnotationLayer(style, ids) map.cameraPosition = CameraPosition.Builder().target(LatLng(33.45, 129.97)) .zoom(10.5).bearing(0.0).tilt(0.0).build() if (hasField) initializeFields() @@ -192,6 +201,57 @@ internal class Step113PbfGateScene( } } + private fun removeEmptyAnnotationLayer(style: Style, baseIds: List) { + check(ANNOTATION_LAYER !in baseIds && map.annotations.isEmpty()) { + "存在生产同名层或遗留Annotation,禁止移除辅助层" + } + check(!parity.getJSONObject("productionSources").has(ANNOTATION_SOURCE)) { + "生产Style占用内部Annotation Source ID,禁止推断为空SDK依赖" + } + val layer = style.getLayer(ANNOTATION_LAYER) + check(layer is SymbolLayer && layer.sourceId == ANNOTATION_SOURCE) { + "内建Annotation层身份不符,保留现场" + } + check(style.getSource(ANNOTATION_SOURCE) != null) { "内建Annotation Source缺失" } + // The object overload preserves the detached wrapper for restoration. + check(style.removeLayer(layer)) { "空内建Annotation层移除失败" } + annotationStyle = style + detachedEmptyAnnotationLayer = layer + check(style.getSource(ANNOTATION_SOURCE) != null) { "不得移除内建Source" } + commandResults.put("emptyBuiltinAnnotationLayerRemoved", true) + .put("annotationInstancesAtRemoval", 0).put("annotationSourcePreserved", true) + parity.put("removedInternalLayer", ANNOTATION_LAYER) + .put("adjustmentScope", "STATIC_RC一次;仅SDK空辅助层;非生产63层") + logRecord("annotation-policy", JSONObject(commandResults.toString()).put("scenario", scenario.name)) + } + + private fun restoreEmptyAnnotationLayer(reason: String) { + val style = annotationStyle + val layer = detachedEmptyAnnotationLayer + try { + if (style != null && layer != null && map.style === style && style.getLayer(ANNOTATION_LAYER) == null) { + style.addLayer(layer) + } + commandResults.put("annotationRemovalEnded", reason) + } finally { + annotationStyle = null + detachedEmptyAnnotationLayer = null + } + } + + private fun checkAnnotationOwnership() { + val style = annotationStyle ?: return + if (map.style !== style) { + restoreEmptyAnnotationLayer("style-replaced") + fail(IllegalStateException("Style已替换,空层调整停止;新Style图层原样保留")) + } else if (map.annotations.isNotEmpty() || style.getLayer(ANNOTATION_LAYER) != null) { + // Native AnnotationManager restores the point layer synchronously + // at add/update, before Map::onUpdate. Polling only reports revocation. + restoreEmptyAnnotationLayer("annotation-or-layer-added") + fail(IllegalStateException("Annotation/辅助层已添加,保留内容并撤销STATIC_RC空层验收条件")) + } + } + private fun addFields(layers: JSONArray) { layers.put(JSONObject().put("id", COLOR).put("type", "weather-color").put("source", SOURCE) .put("paint", JSONObject().put("weather-color-opacity", 0.62))) @@ -276,6 +336,10 @@ internal class Step113PbfGateScene( .put("build", c.optLong("build")).put("reuse", c.optLong("reuse")) .put("invalidation", c.optLong("invalidation")).put("fallback", c.optLong("fallbackCount")) .put("reason", c.optString("lastFallbackReason"))) + rs.getJSONObject(rs.length() - 1) + .put("missingSourceIds", s.optJSONArray("missingSourceIds") ?: JSONArray()) + .put("placementCapable", s.optBoolean("placementCapable")) + .put("placementGatePassed", s.optBoolean("placementGatePassed")) } return JSONObject().put("schema", "step113-summary").put("schemaVersion", 1).put("scenario", scenario.name) .put("sceneReady", ready).put("blockedReason", failureReason ?: JSONObject.NULL) @@ -291,6 +355,11 @@ internal class Step113PbfGateScene( .put("overwritten", t.optLong("overwritten")).put("decisionMismatch", t.optLong("decisionMismatch")) .put("fullRendered", t.optLong("fullRendered")).put("fastRendered", t.optLong("fastRendered")) .put("gateFailureCounts", counts).put("retained", rs) + .put("commands", JSONObject(commandResults.toString())) + .put("sources", native.optJSONArray("sources") ?: JSONArray()) + .put("placementChanged", native.optBoolean("placementChanged")) + .put("placementTransitionActive", native.optBoolean("placementTransitionActive")) + .put("symbolFadeChange", native.opt("symbolFadeChange") ?: JSONObject.NULL) } fun export() { @@ -339,6 +408,8 @@ internal class Step113PbfGateScene( running = false ++generation handler.removeCallbacksAndMessages(null) + try { restoreEmptyAnnotationLayer("scene-stopped") } + catch (error: Exception) { Log.e(TAG, "annotation-layer-restore-failed", error) } runtime?.dispose() runtime = null savedGestures?.let { flags -> @@ -365,6 +436,8 @@ internal class Step113PbfGateScene( private const val PARTICLE = "rn-weather-particles" private const val PROBE_SOURCE = "step113-source-probe" private const val PROBE_LAYER = "step113-source-probe-circle" + private const val ANNOTATION_SOURCE = "org.maplibre.annotations" + private const val ANNOTATION_LAYER = "org.maplibre.annotations.points" private const val FORECAST_MS = 1786060800000L private fun planJson(s: Scenario, finalBaseId: String): String { diff --git a/src/mln/annotation/annotation_manager.cpp b/src/mln/annotation/annotation_manager.cpp index 669e26e0b..5affbfd70 100644 --- a/src/mln/annotation/annotation_manager.cpp +++ b/src/mln/annotation/annotation_manager.cpp @@ -75,6 +75,9 @@ void AnnotationManager::removeAnnotation(const AnnotationID& id) { } void AnnotationManager::add(const AnnotationID& id, const SymbolAnnotation& annotation) { + // Repair an explicitly removed empty helper layer before Map::onUpdate + // captures the style. A subsequent real annotation must never be hidden. + ensurePointLayer(); auto impl = std::make_shared(id, annotation); symbolTree.insert(impl); symbolAnnotations.emplace(id, impl); @@ -101,6 +104,9 @@ void AnnotationManager::update(const AnnotationID& id, const SymbolAnnotation& a const SymbolAnnotation& existing = it->second->annotation; + // Even an otherwise no-op update must publish the repaired style. + dirty = ensurePointLayer() || dirty; + if (existing.geometry != annotation.geometry || existing.icon != annotation.icon) { dirty = true; @@ -173,13 +179,18 @@ std::unique_ptr AnnotationManager::getTileData(const Canonic return tileData; } -void AnnotationManager::updateStyle() { +bool AnnotationManager::ensurePointLayer() { // Create annotation source, point layer, and point bucket. We do everything // via Style::Impl because we don't want annotation mutations to trigger // Style::Impl::styleMutated to be set. + bool changed = false; if (!style.get().impl->getSource(SourceID)) { style.get().impl->addSource(std::make_unique()); - + changed = true; + } + // Source and layer lifetimes are independent. Never replace a layer that + // the application has supplied with this ID. + if (!style.get().impl->getLayer(PointLayerID)) { std::unique_ptr layer = std::make_unique(PointLayerID, SourceID); using namespace expression::dsl; @@ -190,7 +201,13 @@ void AnnotationManager::updateStyle() { layer->setIconIgnorePlacement(true); style.get().impl->addLayer(std::move(layer)); + changed = true; } + return changed; +} + +void AnnotationManager::updateStyle() { + ensurePointLayer(); std::scoped_lock lock(mutex); diff --git a/src/mln/annotation/annotation_manager.hpp b/src/mln/annotation/annotation_manager.hpp index 0a1677c8f..2b7a4dc50 100644 --- a/src/mln/annotation/annotation_manager.hpp +++ b/src/mln/annotation/annotation_manager.hpp @@ -65,6 +65,7 @@ private: void remove(const AnnotationID&); void updateStyle(); + bool ensurePointLayer(); std::unique_ptr getTileData(const CanonicalTileID&); diff --git a/src/mln/renderer/render_segment_json.hpp b/src/mln/renderer/render_segment_json.hpp index 2a69be6ff..a2bfd1692 100644 --- a/src/mln/renderer/render_segment_json.hpp +++ b/src/mln/renderer/render_segment_json.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -323,6 +324,15 @@ inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics for (const auto& layer : segment.layers) writeStyleLayerDiagnostics(writer, layer); writer.EndArray(); writeStringArray(writer, "dependentSourceIds", segment.dependentSourceIds); + // Snapshot-only audit: do not infer an empty drawable result from an + // absent source, or hide it behind the first cache fallback reason. + writer.Key("missingSourceIds"); writer.StartArray(); + for (const auto& id : segment.dependentSourceIds) { + const auto found = std::find_if(snapshot.sources.begin(), snapshot.sources.end(), + [&](const auto& source) { return source.sourceId == id; }); + if (found == snapshot.sources.end()) writer.String(id.c_str()); + } + writer.EndArray(); writeStringArray(writer, "unknownDependencies", segment.unknownDependencies); writer.Key("placementCapable"); writer.Bool(segment.placementCapable); writer.Key("placementGatePassed"); writer.Bool(segment.placementGatePassed); diff --git a/test/api/annotations.test.cpp b/test/api/annotations.test.cpp index 074844124..bf239d198 100644 --- a/test/api/annotations.test.cpp +++ b/test/api/annotations.test.cpp @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -66,6 +68,53 @@ TEST(Annotations, SymbolAnnotation) { // } } +TEST(Annotations, Step116EmptyPointLayerRestoredBeforeRealAdd) { + AnnotationTest test; + test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + test.frontend.render(test.map); + auto& style = test.map.getStyle(); + ASSERT_NE(style.getSource(AnnotationManager::SourceID), nullptr); + ASSERT_TRUE(style.removeLayer(AnnotationManager::PointLayerID)); + test.map.addAnnotationImage(namedMarker("default_marker")); + test.map.addAnnotation(SymbolAnnotation{Point(0, 0), "default_marker"}); + // Check before rendering: a polling/render callback is too late to repair + // the immutable UpdateParameters captured by Map::onUpdate. + ASSERT_NE(style.getLayer(AnnotationManager::PointLayerID), nullptr); + test.checkRendering("point_annotation"); +} + +TEST(Annotations, Step116NoOpUpdateRepairsPointLayerAndSchedulesRender) { + AnnotationTest test; + test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + test.map.addAnnotationImage(namedMarker("default_marker")); + const SymbolAnnotation annotation{Point(0, 0), "default_marker"}; + const auto id = test.map.addAnnotation(annotation); + test.checkRendering("point_annotation"); + ASSERT_TRUE(test.map.getStyle().removeLayer(AnnotationManager::PointLayerID)); + test.map.updateAnnotation(id, annotation); + ASSERT_NE(test.map.getStyle().getLayer(AnnotationManager::PointLayerID), nullptr); + test.checkRendering("point_annotation"); +} + +TEST(Annotations, Step116ReloadRestoresHelperWithoutReplacingExistingLayer) { + AnnotationTest test; + const auto json = util::read_file("test/fixtures/api/empty.json"); + test.map.getStyle().loadJSON(json); + test.frontend.render(test.map); + auto& style = test.map.getStyle(); + ASSERT_TRUE(style.removeLayer(AnnotationManager::PointLayerID)); + auto supplied = std::make_unique(AnnotationManager::PointLayerID, + AnnotationManager::SourceID); + auto* identity = supplied.get(); + style.addLayer(std::move(supplied)); + test.map.addAnnotationImage(namedMarker("default_marker")); + test.map.addAnnotation(SymbolAnnotation{Point(0, 0), "default_marker"}); + EXPECT_EQ(style.getLayer(AnnotationManager::PointLayerID), identity); + style.loadJSON(json); + test.checkRendering("point_annotation"); + EXPECT_NE(style.getLayer(AnnotationManager::PointLayerID), nullptr); +} + TEST(Annotations, SymbolAnnotationTileBoundary) { // Almost exactly the same as SymbolAnnotation test above, but offset my // fractions of a degree tests precision issue from diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index 21d9bbdf4..c6aee2337 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -1200,6 +1200,111 @@ TEST(RenderSegmentLocality, ReadinessAndFadeFollowOnlyDependencies) { EXPECT_FALSE(assessRenderSegmentSources({{}, {}, true}, lookup(true, false)).ready); } +TEST(Step116Safety, MissingSourceIsNotAnEmptyDependencyEvenWithRevisionTombstone) { + const RenderSegmentDependencies dependencies{{"chart", "tide"}, {"symbol/glyph/image/placement"}, false}; + RenderSegmentRevisionBook book; + book.advanceSource("chart"); + EXPECT_FALSE(book.revisionFor(dependencies)); + bool exists = false; + bool loaded = false; + const auto lookup = [&](const std::string& id) -> std::optional { + if (id == "chart") return RenderSegmentSourceStatus{true, false}; + if (id == "tide" && exists) return RenderSegmentSourceStatus{loaded, false}; + return std::nullopt; + }; + auto assessment = assessRenderSegmentSources(dependencies, lookup); + EXPECT_FALSE(assessment.ready); + EXPECT_STREQ(assessment.reason, "missing-source"); + exists = true; + book.advanceSource("tide"); // source addition, before async tile delivery + const auto created = book.revisionFor(dependencies); + ASSERT_TRUE(created); + EXPECT_FALSE(assessRenderSegmentSources(dependencies, lookup).ready); + loaded = true; + book.advanceSource("tide"); // async layout/tile mutation + EXPECT_NE(book.revisionFor(dependencies), created); + EXPECT_TRUE(assessRenderSegmentSources(dependencies, lookup).stable); + exists = false; + book.advanceSource("tide"); // tombstone survives removal; readiness must not + ASSERT_TRUE(book.revisionFor(dependencies)); + EXPECT_FALSE(assessRenderSegmentSources(dependencies, lookup).ready); + exists = true; + book.advanceSource("tide"); // same-name restoration receives a new revision + EXPECT_NE(book.revisionFor(dependencies), created); +} + +TEST(Step116Safety, MissingSourceAuditDoesNotHidePlacementOrMutateSnapshot) { + RenderSegmentDiagnostics snapshot; + RenderSegmentDiagnosticsEntry segment; + segment.strategy = RenderSegmentStrategy::Retained; + segment.dependentSourceIds = {"navsea_delivery", "tide"}; + segment.placementCapable = true; + segment.placementGatePassed = false; + segment.cache.emplace(); + segment.cache->lastFallbackReason = "missing-source"; + snapshot.segments.push_back(segment); + RenderSourceReadinessDiagnostics source; + source.sourceId = "navsea_delivery"; + source.sourceType = "vector"; + snapshot.sources.push_back(source); + auto json = renderSegmentDiagnosticsToJSON(snapshot); + EXPECT_NE(json.find("\"missingSourceIds\":[\"tide\"]"), std::string::npos); + EXPECT_NE(json.find("\"placementGatePassed\":false"), std::string::npos); + EXPECT_EQ(renderSegmentDiagnosticsToJSON(snapshot), json); + source.sourceId = "tide"; + snapshot.sources.push_back(source); + EXPECT_NE(renderSegmentDiagnosticsToJSON(snapshot).find("\"missingSourceIds\":[]"), std::string::npos); + // Presence alone grants neither a successful Build nor a placement pass. + EXPECT_EQ(snapshot.segments[0].cache->build, 0u); + EXPECT_FALSE(snapshot.segments[0].placementGatePassed); +} + +TEST(Step116Safety, SymbolContinuousBranchesAndResourceChangesRemainProtected) { + const std::vector layers{ + {0, "tide-spots", "symbol", false, "tide", std::nullopt}}; + ASSERT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 0, 1)); + // Normal Full: recompute => changed; recent => stale/transition. + EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false)); + EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true)); + EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 1.0f, false)); // transitions disabled + const auto dependencies = renderSegmentDependencies(layers, 0, 1); + RenderSegmentRevisionBook book; + book.advanceSource("tide"); + const auto unchanged = book.revisionFor(dependencies); + EXPECT_EQ(book.revisionFor(dependencies), unchanged); + // Test the revision contract, not fabricated glyph/tile events or GPU completion. + for (const auto* event : {"style", "glyph", "image", "context/reload"}) { + SCOPED_TRACE(event); + const auto before = book.revisionFor(dependencies); + book.advanceGlobal(); + EXPECT_NE(book.revisionFor(dependencies), before); + } + for (const auto* event : {"source-update", "tile/layout"}) { + SCOPED_TRACE(event); + const auto before = book.revisionFor(dependencies); + book.advanceSource("tide"); + EXPECT_NE(book.revisionFor(dependencies), before); + } +} + +TEST(Step116Safety, AnnotationMutationAndLayerOrderDisableReloadKeepAllShadowFailures) { + auto input = fastPathInput(); + input.cacheValid = false; + input.annotationMutation = true; + input.unsupportedContinuousLayer = true; + input.supportedScene = false; + input.topologyMatches = false; // added/reordered layer + input.retainedEnabled = false; // explicit disable + input.hasPreparedTree = false; // style reload/context replacement + const auto decision = evaluateParticleFastPath(input); + EXPECT_FALSE(decision.candidate); + EXPECT_EQ(decision.reason, ParticleFastPathMissReason::NoPreparedTree); + const auto failures = particleFastGateFailureMask(input); + for (const auto bit : {0, 1, 2, 10, 21, 22, 23}) EXPECT_NE(failures & (1u << bit), 0u); + auto unmodified = fastPathInput(); + EXPECT_EQ(particleFastGateFailureMask(unmodified), 0u); // predicate-only; no rendered Fast claim +} + TEST(RenderSegmentLocality, MixedExecutionDrawsEachLiveRangeOnceInStyleOrder) { ResolvedRenderSegmentPlan plan; plan.valid = true; diff --git a/tools/particle_android_validation/README.md b/tools/particle_android_validation/README.md index 1e6c7ce90..f6261766d 100644 --- a/tools/particle_android_validation/README.md +++ b/tools/particle_android_validation/README.md @@ -114,7 +114,7 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \ ## STEP-113 原 PBF 专用 Test App Fast Path 复测 -本测试不使用 STEP-114 的简化 FAST_SMOKE。保持原 Step113PbfGateScene、PBF URL、63 个正式基础层与原 Native 诊断不变。需要用户手工完成新源码的标准 APK 编译和安装。工具先检查 build-info 中的 Git commit 与当前 HEAD、APK SHA、设备已安装 APK SHA 是否一致;任何不一致即拒绝并说明原因。 +本测试不使用 STEP-114 的简化 FAST_SMOKE。保留原 Step113PbfGateScene 的 PBF URL、63 个正式基础层与 Native 真实计数。STEP-116 仅对 STATIC_RC 的空 SDK Annotation 辅助层做一次移除及生命周期保护,并增加诊断;其余四场景不做此调整。需要用户在 RDC 独立审计并锁定源码后手工完成标准 APK 编译和安装。工具检查 tracked clean、build-info Git commit 与 HEAD、APK SHA、设备安装 SHA;不一致即拒绝。原 STEP-110/111 等未跟踪任务书可以保留。 在 Mac 的 MapLibreParticleLab 根目录依序执行: @@ -129,3 +129,9 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \ 前两项为用户手工 Android 构建和更新安装,不得在未授权情况下代劳。后两项由 RDC 或用户执行。脚本使用明确 ADB serial 启动原有 Activity 的 STEP-113 原场景,按当前 PID 还原 STEP113Gate 的 Base64 Logcat summary,计算实际 Fast/Full 计数差及 R Build/Reuse,保存到忽略 Git 的 artifacts/maplibre-particle/android-validation/step113-retest-时间/场景/ 目录。 FAST_OBSERVED 仅表示实际 Fast 数字增长,不代表像素等价、性能或所有安全回退已验收;NO_FAST_OBSERVED 表示仍未进入,记录完整 R fallback 和 24 Gate 聚合。STALE_APK / INSUFFICIENT_SAMPLES 不是 PASS。依次需要时用 --scenario PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE。SOURCE_CHANGE 的手动 Source mutation 仍须通过 App 原菜单做独立验证,脚本不会自行修改 Source。阶段事实详见 docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md。 + +## STEP-116 当前交接 + +本轮只交源码,缺 `tide` 与 Symbol R 缓存准入继续 fail-closed,不能宣称原 STATIC_RC 已出现 Fast。Native AnnotationManager 在后续真实 Symbol Annotation add/update 与 Style loaded 时同步恢复缺失 points;Test App stop 重加原空层,后续 Annotation/Style 变化撤销空层验收条件,不周期删除新内容。原63层和Source定义保留,内部Annotation Source不删除;源码预期STATIC_RC由66层变65层,须以最新设备manifest核验。 + +复测脚本现要求至少75秒,在PID出现后等待完整请求时长;建议 `--scenario STATIC_RC --seconds 90`。同generation/lifecycle、plan/base parity、24Gate、mismatch0、计数单调及blocked撤销均为必要条件。summary新增缺源ID/Placement/commands/Source表;120帧仍需用户手动导出。精确命令、用户一次手工build/install、RDC Native定向测试及阴性结果解释见 [STEP-116 专用APP验收](../../docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md)。历史STEP115记录保留为当时事实,不代表本轮设备已验收。 diff --git a/tools/particle_android_validation/verify_step113_fast_path.py b/tools/particle_android_validation/verify_step113_fast_path.py index 90d951751..8cbad5d6e 100755 --- a/tools/particle_android_validation/verify_step113_fast_path.py +++ b/tools/particle_android_validation/verify_step113_fast_path.py @@ -45,6 +45,9 @@ def inspect(apk, adb, serial): info = dict(line.split("=", 1) for line in BUILD_INFO.read_text(encoding="utf-8").splitlines() if "=" in line) source_head = call(["git", "-C", str(ROOT), "rev-parse", "HEAD"]) + tracked_changes = call(["git", "-C", str(ROOT), "status", "--porcelain", "--untracked-files=no"]) + if tracked_changes: + raise RuntimeError("SOURCE_NOT_LOCKED: tracked源码有未提交变更;RDC须先独立审计并scoped提交,用户再构建最新HEAD") if info.get("commit") != source_head: raise RuntimeError(f"STALE_APK: build-info commit={info.get('commit')}, " f"current HEAD={source_head}; 禁止用旧APK验收本轮修复") @@ -76,7 +79,7 @@ def extract(logcat, pid, expected): continue try: item = json.loads(found.group(2)) - if item.get("encoding") != "base64-utf8" or item.get("kind") != "summary": + if item.get("encoding") != "base64-utf8" or item.get("kind") not in ("summary", "blocked"): continue ident = (item["exportId"], item["kind"]) record = chunks.setdefault(ident, {"count": item["chunkCount"], "items": {}}) @@ -85,13 +88,18 @@ def extract(logcat, pid, expected): except (KeyError, ValueError, TypeError): continue result = [] - for (ident, _), record in chunks.items(): + for (ident, kind), record in chunks.items(): if len(record["items"]) != record["count"]: continue try: packed = "".join(record["items"][i] for i in range(record["count"])) sample = json.loads(base64.b64decode(packed, validate=True).decode("utf-8")) - if sample.get("scenario") == expected and sample.get("sceneReady") and not sample.get("blockedReason"): + if kind == "blocked": + sample["blockedReason"] = sample.get("reason") or "SCENE_BLOCKED" + sample["sceneReady"] = False + # Keep revoked/blocked summaries as negative evidence too; an + # earlier Fast counter must not conceal a later Annotation guard. + if sample.get("scenario") == expected: result.append((int(ident.split("-", 1)[0]), sample)) except (KeyError, ValueError, TypeError, UnicodeError): continue @@ -107,8 +115,8 @@ def run(): parser.add_argument("--seconds", type=int, default=75) parser.add_argument("--preflight-only", action="store_true") args = parser.parse_args() - if args.seconds < 65 and not args.preflight_only: - parser.error("至少65秒以取得首次及下一次30秒周期摘要;不是FPS测量") + if args.seconds < 75 and not args.preflight_only: + parser.error("原STEP-113场景至少75秒;摘要与完整120帧导出仍须分别验收") if not Path(args.adb).is_file(): raise RuntimeError(f"adb not found: {args.adb}") info, local_sha, head, device = inspect(Path(args.apk), args.adb, args.serial) @@ -136,12 +144,17 @@ def run(): if process_id is None: raise RuntimeError("APP_PROCESS_NOT_RUNNING after launch") print(f"APP_PID={process_id}") - time.sleep(max(0, args.seconds - 12)) + # PID acquisition can take less than 12 seconds. Never subtract the + # maximum poll duration from the actual scenario observation period. + observation_started = time.monotonic() + time.sleep(args.seconds) + observation_elapsed = time.monotonic() - observation_started raw_log = device("logcat", "-d", "-v", "brief", "-s", "STEP113Gate:I", "*:S") output.joinpath("logcat.txt").write_text(raw_log + "\n", encoding="utf-8") - samples = extract(raw_log, process_id, args.scenario) + all_samples = extract(raw_log, process_id, args.scenario) + samples = [s for s in all_samples if s.get("sceneReady") and not s.get("blockedReason")] if not samples: - status = "NO_READY_SUMMARY" + status = "SCENE_BLOCKED_OR_REVOKED" if any(s.get("blockedReason") for s in all_samples) else "NO_READY_SUMMARY" fast_delta = full_delta = 0 else: start, end = samples[0], samples[-1] @@ -149,8 +162,20 @@ def run(): full_delta = end["fullRendered"] - start["fullRendered"] if len(samples) < 2: status = "INSUFFICIENT_SAMPLES" + elif any(s.get("blockedReason") for s in all_samples): + status = "SCENE_BLOCKED_OR_REVOKED" + elif any((s.get("sceneGeneration"), s.get("lifecycle")) != + (start.get("sceneGeneration"), start.get("lifecycle")) for s in samples): + status = "SUMMARY_LIFECYCLE_CHANGED" + elif any(not s.get("planValid") or not s.get("basePreserved") for s in samples): + status = "INVALID_PLAN_OR_BASE_PARITY" + elif any(len(s.get("gateFailureCounts", [])) != 24 for s in samples): + status = "INCOMPLETE_GATE_SUMMARY" elif any(s["decisionMismatch"] != 0 for s in samples): status = "DECISION_MISMATCH" + elif any(b["fastRendered"] < a["fastRendered"] or b["fullRendered"] < a["fullRendered"] + for a, b in zip(samples, samples[1:])): + status = "COUNTER_RESET" elif fast_delta > 0: status = "FAST_OBSERVED" else: @@ -165,7 +190,9 @@ def run(): "scenario": args.scenario, "sourceReport": "STEP-113 original PBF test scene; not STEP-114 smoke", "durationSecondsRequested": args.seconds, + "durationSecondsObservedAfterPid": observation_elapsed, "completeSummarySamples": len(samples), + "blockedSummarySamples": [s for s in all_samples if s.get("blockedReason")], "status": status, "fastDelta": fast_delta, "fullDelta": full_delta, diff --git a/粒子开发记录.md b/粒子开发记录.md index 0a7430a4d..0561fc105 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1157,3 +1157,13 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 2026-10-11 STEP-114 RDC:独立核对安全Gate改动、3项C++测试源码、Android Test App接线及STEP-113/102冻结SHA,git diff --check PASS。Mac缺libuv,单独C++ header检查因Xcode找不到标准头array环境阻塞;Android/真机/Fast未执行,PARTIAL,后续用户一次手工构建验收。 - 2026-10-11 STEP-115:为原STEP-113专用APP增加Fast Path复测工具,严格校验源码HEAD、build-info APK SHA与设备安装SHA;旧包源码为c6d13e1,与现行21a1081不匹配,preflight正确拒绝。已验证旧Logcat可解码STEP113真实摘要,原五场景保持不改。尚无新Android编译/安装/真正Fast,下一步用户手工build/install后以STATIC_RC为先验收,状态PARTIAL。 + +### 2026-10-11 STEP-116 原 STEP-113 PBF Fast Path 受控修复(源码交付 PARTIAL) + +- 实际模型 GPT-6.1-Sol(gpt-6.1-sol)/ high,已读取会话turn_context确认;Mac particle/main起点e2d202b560ad0165101546597c43f2ef0ccf4542,无commit/push/发行/子代理。 +- 仅STATIC_RC确认空实例后一次移除SDK内部points层,保留Source及原63层;stop恢复,后续添加/Style替换撤销验收条件。Native AnnotationManager在Symbol add/update/style-loaded同步恢复缺层,no-op更新也dirty,避免依赖3秒poll漏Marker。 +- R缺tide仍missing-source,Symbol R无最终GPU结果证书仍BLOCKED;未补假源/删tide-spots/放宽Core C或Placement Gate。JSON加missingSourceIds,摘要同次保留Placement与真实Build/Reuse/Full/Fast。 +- 新增四个Step116Safety与三个Annotations.Step116非Jest测试源码;STEP115脚本修复实际等待时长并拒绝未锁定tracked源码、生命周期变化/撤销/计数reset。四份中文交付和README已落盘,开发记录仅追加。 +- 已做静态diff/AST/旧JSON及冻结SHA核查,未编译/运行Native测试/Gradle/ADB/安装/Jest;真实Fast与画面NOT VERIFIED。保护STEP110/111,保留STEP114/102源码。下一步RDC独立审计锁定源码,用户一次手工build/install,原STATIC_RC≥75秒与120帧/24Gate/SHA/画面闭环,详见docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md及本Step完成报告。 + +- 2026-10-11 STEP-116 RDC:审查Native空Annotation恢复、原STEP113受限空层移除、缺源诊断、7个C++测试源码与同HEAD/SHA复测脚本,git diff --check、Python AST通过。修正Mac测试命令rg为grep -E。实际CMake macos-metal配置exit1缺libuv,未继续C++测试/Android Gradle/真机Fast,PARTIAL;缺tide与Symbol R仍BLOCKED。