From 21a10816435a82de3471bdb55ed83223101f5689 Mon Sep 17 00:00:00 2001 From: tei Date: Sun, 11 Oct 2026 10:01:10 +0900 Subject: [PATCH] feat: prepare STEP-114 guarded fast smoke validation --- .../STEP-114_FastPath安全准入与闭环判定_v1.md | 78 +++++ ...astPath安全准入与TestApp闭环_Codex任务_v1.md | 41 +++ ...STEP-114_FastPath安全准入与TestApp闭环_v1.md | 77 ++++ .../STEP-114_独立FastSmoke场景与RDC验收_v1.md | 172 +++++++++ ...ath安全准入与TestApp闭环_Codex完成报告_v1.md | 134 +++++++ .../ParticleCoreLayerValidationActivity.kt | 91 ++++- .../particle/Step114FastSmokeScene.kt | 329 ++++++++++++++++++ src/mln/renderer/particle_fast_path.hpp | 11 + src/mln/renderer/render_orchestrator.cpp | 14 +- .../renderer/retained_underlay_cache.test.cpp | 80 +++++ 粒子开发记录.md | 11 + 11 files changed, 1032 insertions(+), 6 deletions(-) create mode 100644 docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md create mode 100644 docs/40_任务/STEP-114_FastPath安全准入与TestApp闭环_Codex任务_v1.md create mode 100644 docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md create mode 100644 docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md create mode 100644 docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md create mode 100644 platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step114FastSmokeScene.kt diff --git a/docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md b/docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md new file mode 100644 index 000000000..0b94217bd --- /dev/null +++ b/docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md @@ -0,0 +1,78 @@ +# STEP-114 Fast Path 安全准入与闭环判定 v1 + +日期:2026-10-11(JST)。结论:**PARTIAL;安全子集源码与独立最小场景已交付,真实 Fast 尚未验收。** + +## 1. 基线、证据等级与阅读 + +Mac / Darwin 24.6.0 / x86_64,`/Users/tei/dev/MapLibreParticleLab`,`particle/main@b02e54de3d45fcf612126ae8f037cba204f590ea`。源码版本 `13.6.1-particle.23`,没有发行或改 Source Lock。实际模型 GPT-6.1-Sol / high 已从当前本地 rollout 的 `turn_context` 核实。 + +已读根 AGENTS、PARTICLE_BASELINE、项目规范全部八份、项目总则 README 与四份有效总则、本任务、STEP-112 调查/完成报告、STEP-113 完成报告/Step(含两轮 RDC 追加)、实际 Shared Core 和 Test App 源码。Integration 只读 AGENTS、README、WORKFLOW、TEAM_MODEL、C-001/C-002、Contract 版本规则、RFC-001/002/004、ADR-001、COORD-005 决议;不写跨项目文件,不改 RN/JS API/ABI。 + +SOURCE-PROVEN 表示本轮源码控制流;RETAINED-EVIDENCE 表示本机保留的旧 RDC JSON;IMPLEMENTED 表示已改源码;NOT RUN 表示未编译/运行。旧设备证据不是本轮改动的设备验收。 + +## 2. R 的真实 SourceType 与初建失败点 + +不能从 `unknown-dependency` 推断 source 一定是 CustomMVTVector:它还覆盖未知依赖、缺 revision 登记等情况。 + +本轮只读 `artifacts/maplibre-particle/android-validation/step113/source-change-after-full.json`:Native source 表为 mapple/raster、navsea_delivery/vector、org.maplibre.annotations/annotations、rn-weather-fields/raster-array、step113-source-probe/geojson。R0 `[0,63)` 的依赖为 mapple、navsea_delivery、**tide**;第2层 `tide-spots` 是 `symbol`、sourceId=`tide`,source 表却没有 tide。R0 的 sourceRevision=0、sourceReady=false、Build=0、fallback=unknown-dependency。 + +源码 `style/conversion/source.cpp::convertVectorSource` 创建 `VectorSource`;其构造函数指定 `SourceType::Vector`;`RenderSource::create` 创建 `RenderVectorSource`。因此该 Test App 的正式 PBF source **不是 CustomMVTVector**。任务书列出的 CustomMVTVector 限制是独立安全约束,本轮完整保留。没有改旧报告,也没有补造 tide 数据、删原正式 PBF 层或修改服务 Style。 + +`retainedSegmentSourceState` 原先先读取 revisionFor,缺少 tide 的登记会返回 nullopt,从而提前返回默认 unknown-dependency。现在在读取 revision 前发现缺失 RenderSource 时明确返回 `missing-source`;本例的ready/stable=false、revision=0 不变。有旧tombstone记录的缺源也提前返回revision=0,而不暴露旧登记hash;它仍不得Build/Reuse,RevisionBook登记机制不变。这是诊断修正,**不改变缓存准入**。source 存在但无 revision、CustomMVTVector、未知 layer dependency 仍 fail-closed。 + +后续即使 Host 合法补齐 tide,R 仍包含大量 Symbol,Placement 首建阻塞不会自动消失。缺源与 Placement 是不同失败点。 + +## 3. 已支持的本地 GeoJSON/circle 子集 + +FAST_SMOKE 选择本地 GeoJSON 非空点与 circle R,不新增 source 类型授权。其现有追踪链: + +| 输入或事件 | 源码链与保护 | +|---|---| +| GeoJSON setGeoJSON | `GeoJSONSource::setGeoJSONData` 替换 immutable Impl 并通知 SourceChanged;Fast non-camera signature 比较 sources;Full `diffSources` 推进全局 content 和本 source revision | +| Source 增删/同ID重建 | `createRenderTree` 的 sourceDiff added/removed/changed 都 advanceSource;RevisionBook 保留 tombstone;PreparedTree 在 Full 更新对象前释放 | +| 异步 GeoJSON 数据/布局 | `RenderGeoJSONSource::update` → GeoJSONTile::updateData;capturedData 和 weakPtr 防陈旧/销毁回调;GeometryTile correlationID 保护 pending;`onLayout` 每次更换 layoutResult 都 onTileChanged;RenderSource → orchestrator 推进 source-local revision 和全局 content,再 invalidate | +| Tile 未就绪/淡入 | `isLoaded`、hasFadingTiles、source assessment、#17 与 R 稳定性检查继续保留 | +| Layer paint/layout/顺序 | immutable layers signature、per-R content fingerprint、resolved plan/topology;有 transition/repaint 仍阻止 | +| Camera/viewport/pixelRatio | #6/#7/#8、RetainedUnderlayKey 与 revision state;旧 texture 不能按新矩阵复用 | +| Style reload | styleLoaded/signature、配置/层序/Source 新身份、PreparedTree 生命周期保护;trace 在实际看到 loaded→false 时重置 | +| Image/Sprite | imageDiff、spriteLoaded、imageManager readiness、missing/unused image callbacks 推进全局 revision;#8 先捕获新 images 输入 | +| Glyph/fontFaces/URL | non-camera signature、glyph URL/font identity 追踪、onGlyphsLoaded 推进全局 revision;不删除资源门禁 | +| Annotation | #24 pending 检查、source/tile/图片变化与层表;实际 Symbol 继续纳入全场结构检查 | +| context/reset/destroy | markContextLost、Renderer.clearData 清 PreparedTree/trace,orchestrator 推进 revision;析构 detach observer 前排空 source/tile 工作;Test App 取消 callback、dispose runtime、删除自有 marker,MapView 原生命周期仍由 Activity 执行 | + +这一闭环是现有实现的源码依据,**不是本轮已跑出的 Source mutation/GPU PASS**。异步过渡中 Full 也可能继续展示尚未更新的旧 bucket;要求 Fast 与同一时刻合法 Full 等价,而非承诺提交 API 后同步完成画面更新。成功布局后必须失效并正确重建;不可用两张稳定末态截图代替事件顺序证据。 + +R source policy、RevisionBook、GPU Build/Reuse 算法本轮零语义修改。新增 C++ 用例验证既有 revision/cache 状态机组合;unit 中显式推进 epoch/标记构建只用于状态机测试,不是实际 Native 帧或 GPU 成功的证据。 + +## 4. C、Annotation 与 Scene 聚合 + +`continuousRangeSupportsParticleFastPath` 要求每个 C layer 都 startsRetainedDynamicRange,并至少有粒子动画层。WeatherColor 与 RasterParticle 符合;普通 circle、Symbol、内建 Annotation Symbol 不符合。`particlePreparedSceneSupported = retainedSupported && fastContinuousSupported`,并要求单一 C。#22/#23 可以共享 C 不支持根因。 + +本轮没有修改 #22/#23、C GPU 复合流程、层序或 AnnotationManager。不能证明非粒子 C 与既有 Fast prepare/postRender 等价,故不放行。 + +独立 FAST_SMOKE 在确认 `map.annotations.isEmpty()` 后,仅从本场景 Style 删除自动生成且空的 `org.maplibre.annotations.points` layer,保留内建 source;若缺层或 remove 失败则 BLOCKED。Native annotation 更新规则未变,正式 PBF 五场景不删除该层。baseline 层序应为 background、R circle、weather-color、raster-particle,实际 JSON 仍需 RDC 核对。 + +ANNOTATION_BLOCKED 重新加载一个正常 Style,保留自动内建层并用现有 `map.addMarker` 加真实 marker;可单次修改其 position。内建 Symbol 位于 C,#22/#23 必须继续阻止 Fast。从该场景切回 baseline 时只删除本场景拥有的 marker,不能清除其他 Annotation。若存在其他 Annotation,baseline 拒绝启动。该场景转换含合法 Style reload,不是生产 Annotation 零重载支持的证明。 + +## 5. Placement:安全局部修复与 BLOCKED + +正常 Continuous Full:placementIsRecent=false 则 placeLayers/commit,placementChanged=true;recent=true 则 setPlacementStale,controller.hasTransitions=true;关闭 transitions 时正常 Full changed=true、fade=1。含结构性 Symbol 的 R 无可证明的首次 Build 稳定窗口。 + +本轮保持 Symbol R 的三项 gate、scene placementStable、全图碰撞、fade/transition 和跨 source 规则。完整 GPU symbol 输入、bucket/atlas/opacity/duplicate/anchor 的结果证书仍不存在;建立它需要独立结果签名或更大生命周期设计。**Symbol R 准入修复 BLOCKED,不能删除 changed/stale/fade 条件。** + +唯一行为修复限定于**完整非空层表没有任何结构性 Symbol**:此时 controller 的 stale/transition 没有 Symbol 像素消费者,单独阻止 #13 不代表画面可能变化。`particleFastPathHasPlacementTransition` 按完整 Style 表检查;隐藏/空数据的 Symbol 与 Annotation Symbol 一律保守保护。production 还核对层表数量与 renderLayers 数量,不一致保持原 transition;空表也保守保留。 + +这是 Shared Core 内部实现优化,无 Android/iOS 分叉,不新增控制 API、不禁止正常 Full Placement、不修改原始 controller。其余23项 Gate、source/cache/scene 条件全部继续生效。新增 Symbol 尚未进入旧表时,immutable layers 的 non-camera signature 不相等,且 Annotation pending 单独阻止 Fast;Full 更新后表重新包含 Symbol。未取得新层表前不得只凭这个 helper 放行。 + +反例边界:同场只要存在一个隐藏 Symbol、空 Symbol、C Annotation Symbol,仍使用原 controller transition;缺层表、source mutation、未完成 tile layout、image/glyph 更新、camera/viewport、style reload、debug/context 问题都不能被本修复抵消。未来若引入新的 Placement-capable layer 类型,应先扩展结构检测及测试;当前源码仅 Symbol 的 LayerTypeInfo 使用 CrossTileIndex::Required。 + +## 6. 实施结果、验证方案和限制 + +- IMPLEMENTED:缺源明确诊断、无 Symbol 场景的 #13 限定检查;3个非 Jest C++ 用例源码;独立4场景及 UI/Intent、真实 Native 快照与操作前后自动采样;中文文档和追加记录。 +- 保留 fail-closed:CustomMVTVector、未知 revision、缺源;非粒子 C;所有 Symbol R Build/Placement 保护。 +- NOT RUN:Native/Kotlin 编译、C++ 测试、Android build/设备/GPU/Fast 命中、像素对照、性能、iOS Metal 设备验证。 +- 保留:正式 STEP-113 五个 PBF 场景实现原样、STEP-102 fixture 原样、STEP-110/111 未跟踪文件原样。新增菜单明确标识独立 smoke,不冒充63/113层等价。 + +验收步骤和命令见 `docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md`;Native 定向命令也直接写在本轮完成报告。baseline 必须取得真实 Native Full>0、Fast 增长、CacheBuild>0、Reuse 增长、completed fast-rendered、24 PASS/knownMask 完整/decisionMismatch=0;否则维持 PARTIAL,不能标 Fast Path 已修复。 + +Source/Image/Camera/缓存禁用/Reload 的反向证据需保存事件前/提交后/100/500/1500ms 快照,必要时以更早的精确帧复核;未捕获瞬间 gate 不能用计数猜测。后台/恢复/Teardown、Symbol/Annotation 与原 PBF 回归、双平台共享源码独立检查仍为 RDC/用户待执行项。 diff --git a/docs/40_任务/STEP-114_FastPath安全准入与TestApp闭环_Codex任务_v1.md b/docs/40_任务/STEP-114_FastPath安全准入与TestApp闭环_Codex任务_v1.md new file mode 100644 index 000000000..775759ea0 --- /dev/null +++ b/docs/40_任务/STEP-114_FastPath安全准入与TestApp闭环_Codex任务_v1.md @@ -0,0 +1,41 @@ +# STEP-114:Fast Path 安全准入受控修复与 Test App 闭环(Codex 任务 v1) + +## 执行授权与基线 +- 用户明确同意继续受控修复,不要求完全复刻 NavSea 113 层。 +- Mac 仓库 /Users/tei/dev/MapLibreParticleLab,particle/main 起点 b02e54de3d45fcf612126ae8f037cba204f590ea。Integration 最新 main 80667d6c343411d03a760425438e3d292f452118 经 RDC 验证。 +- Codex 固定 GPT-6.1-Sol High (gpt-6.1-sol / high)。先读 AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范、docs/00_项目总则/README.md 及四份有效总则、STEP-112/113 报告、Integration 的正式 Contracts/RFC/ADR,严格遵守项目边界。 +- 本轮 Codex 只改代码与文档;不能运行 Android Gradle、ADB、Jest、CMake/Ninja/Bazel 大编译或正式打包;Native 定向验证由 RDC 另行执行,Android 编译由用户在授权条件下手工统一验收。 +- 不碰原有两份未跟踪 STEP-110/111 文件、无关文件、NavSea 源码和 Integration;禁止 git add -A / reset --hard / clean;不得变更 RN/JS 公共 ABI/API、发行版本或 Source Lock。 + +## 根因事实 +- STEP-113 STATIC_RC 66 Native layers,R[0,63) Build=0、unknown-dependency;C[63,66) 含天气色场、粒子、内部 Annotation Symbol。Fast=0。 +- 119 个最新 completed Full 帧:24 Gate shadow mismatch=0;#3 cache invalid、#4 revision mismatch、#22 unsupported continuous layer、#23 unsupported scene 全部失败;#12/#13 placement 105 帧失败。 +- RenderOrchestrator::retainedSegmentSourceState 对 CustomMVTVector 明确 fail-closed,已有源码注释说明无 retained cache revision contract;不得删掉这个限制来伪造 Fast。 +- continuousRangeSupportsParticleFastPath 要求 C 全层以 Particle dynamic API 标记;内部 Annotation Symbol 不满足,且 #23 聚合依赖部分与 #22 重合。 +- STEP-112 证明含结构性 Symbol 的 R 在旧 Placement stale/changed/fade 时可能根本无法进入首次 Cache Build;禁止只删条件或假设视觉没变。 + +## 实施步骤(依次、受控) +1. 做当前源码静态闭环:明确 Test App 对应 SourceType、Source/Tile/Style/Glyph/Image/Annotation mutation 追踪、R 的初建失败点、C 准入及 Scene 复合条件、Placement 控制流。 +2. R 缓存:只对确有完整 Source revision 与异步失效追踪的类型建立安全准入;若 CustomMVTVector 无法严格证明,保留其 fail-closed。优先为已支持的保守 source 子集提供可用的独立 R-cache 正向测试。保留 Source mutation、Tile update、camera、style reload、image/glyph、destroy/reset 的反向用例。 +3. C 非粒子层:只有证明原有 GPU 复合内容/Source revision/Placement/Annotation 在静止场景与 Full 等价、变化时 Full 回退,才能受限放行。否则不可移除 Gate #22:可通过单独 Test App 最小场景或安全 Layer 拓扑证明能进入 Fast。不要丢失原始正式 PBF 5 场景。 +4. Symbol R Placement:基于已提交的真实 Placement/资源 revision 与 transition/fade 审核是否有安全稳定窗口;如须新增独立结果签名或其他跨平台设计,停止该项并在报告中写 BLOCKED,不做危险简化。 +5. 优先实现独立 Test App FAST_SMOKE 场景(如安全可行),按 UI 菜单明确区分正式 PBF 与独立最小场景。使用 Native 真实 Full/Fast/CacheBuild/Reuse 计数和24 Gate JSON,测试稳定、Source mutation、Annotation、Pan/Zoom、Reload、前后台/Teardown;不许伪造 Frame 结果或直接注入 candidate=true。 +6. 所有公共 Shared Core 代码仍跨 Android/iOS 共用,不得建立平台分叉。任何需要改公共语义/API的部分暂停,留给 Integration RFC。 + +## 测试/运行要求 +- 可改/新增现有非 Jest C++ 相关测试源,但 Codex 不执行测试/编译。 +- 必须写出 RDC 可以执行的精确定向 Mac Native 检查命令;后续 Android 编译和设备 Gate 集中一次进行。 +- 只证明能证明的部分;可产出 PARTIAL / BLOCKED 与可用局部修复,不允许“关掉Gate提升Fast数字”。 + +## 必做输出 +1. 本次实际修改的 C++、Android Test App 和非 Jest 测试源(不能安全改的部分写明零修改)。 +2. docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md:R/C/Placement 证据、可能的反例、实现范围、安全前提、实际测试方案。 +3. docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md:当轮修改、状态、验证事实及后续步骤。 +4. docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md:模型全名档位、改动文件、影响/兼容、RDC 具体命令、已做/未做的验收、Git check。 +5. 追加中文 粒子开发记录.md:当前改动、当前状态、下一步。 +6. 新增 Test App 入口时更新相关场景说明,清楚区分真实 PBF 与独立 smoke。 +7. 文档必须实际写入并保留,不能任务后要求用户补交。 + +## Git 收尾 +- Codex 只能执行无构建型静态检查与 git diff --check,回报 git status 和必要的验证命令;禁止将未运行的测试写成 PASS。 +- Codex 完成后先停下由 RDC 独立审计/运行定向检查;阶段结果达到合理门槛后由 RDC scoped commit 到 particle/main 并 push origin,核验远端 SHA。若未达真实 Fast,不得标示 Fast Path 已修复。正式发行独立处理。 diff --git a/docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md b/docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md new file mode 100644 index 000000000..9a4329453 --- /dev/null +++ b/docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md @@ -0,0 +1,77 @@ +# STEP-114 Fast Path 安全准入与 Test App 闭环 v1 + +日期:2026-10-11(JST)。状态:**进行中;源码交付 PARTIAL,运行闭环待 RDC/用户,Symbol R 准入 BLOCKED。** + +## 目标、基线与范围 + +- Mac / Darwin 24.6.0 / x86_64;`/Users/tei/dev/MapLibreParticleLab`;`particle/main@b02e54de3d45fcf612126ae8f037cba204f590ea`。 +- 实际 GPT-6.1-Sol(gpt-6.1-sol)/ high 已从本会话执行记录核实。没有模型升级、子代理或另一 Codex 实例。 +- Native/Test App 源码版本 `13.6.1-particle.23` 未改;Integration 契约仅只读,RN/JS公共API/ABI、Source Lock、正式发行不变。 +- 只修改 Shared Core 内部安全子集判定/缺源诊断、现有非 Jest 测试、Android Test App、本Step中文文档和开发记录追加。 +- 禁止 Codex 执行编译/测试/Gradle/ADB/Jest/CMake/Ninja/Bazel、commit/push、发行、跨项目写入或触碰原有 STEP-110/111 文件。 + +## 按顺序完成的源码工作 + +- [x] 静态闭环:Test App source类型、R初建失败、mutation/异步布局/资源/Annotation追踪、C/scene复合条件、Placement两分支。 +- [x] 本机旧STEP-113完整JSON回读发现:navsea_delivery是Vector;R依赖tide但Native source表缺tide。缺revision默认unknown-dependency不能作为CustomMVT已出现的证据。 +- [x] R缺失 RenderSource 提前返回missing-source,仍ready/stable=false、revision0;不补假数据,不放行CustomMVT/未知revision。 +- [x] 保守GeoJSON/circle R正向独立场景;source admission/cache GPU算法不改,3个C++用例源码补充结构保护与revision/cache状态机覆盖。 +- [x] 非粒子C继续fail-closed;独立scene只在确认没有Annotation实例时移除空内建层,正式PBF五场景原样保留。 +- [x] 无Symbol全场的controller stale不影响Symbol像素消费者;限定#13只在完整层表有结构性Symbol时阻止。空表/数量不一致仍保守,隐藏或空Symbol不豁免。 +- [x] Symbol R真稳定窗口审查:现有commit/fade证据不足,保留全部Build/scene条件;停止更大结果签名设计,明确BLOCKED。 +- [x] 独立FAST_SMOKE、CIRCLE_C_BLOCKED、ANNOTATION_BLOCKED、SYMBOL_R_BLOCKED;菜单/Intent、低频真实Native UI、单次Source/Image/Camera/Annotation/Cache命令与操作前后文件、Reload/前后台/Teardown取证入口。 +- [x] 调查、场景/定向验收说明、Step、中文完成报告、粒子开发记录实际落盘。 +- [ ] Mac Native配置/编译和3个新用例及指定回归实际运行。 +- [ ] Kotlin/Android一次手工编译、真实NativeFast>0、视觉与反向/生命周期设备闭环。 +- [ ] Symbol R结果证书、CustomMVT完整revision契约、非粒子C复合等价证明:未实现,维持BLOCKED/fail-closed。 +- [ ] iOS Metal实机/性能与生产Fast:NOT VERIFIED,不能由smoke替代。 + +## 当前结果与限制 + +没有新的编译、测试、APK、设备、GPU计数或性能结果。SOURCE-PROVEN/IMPLEMENTED与旧RETAINED-EVIDENCE分开;整体PARTIAL,不标Fast Path已修复。旧PBF缺tide诊断修正不代表该R可以Build;即使补源仍受SymbolPlacement保护。 + +预期FAST_SMOKE的真实Native层序4层、R[0,2)/C[2,4),必须取得Build/Reuse/Full/Fast与24Gate同帧证据。UIsceneReady只代表加载/命令成功,实际Native结构由导出JSON检查。三个负场景继续阻止Fast。Source等操作自动100/500/1500ms取证仍可能错过瞬态,缺证据如实记NOT VERIFIED。 + +## 已做检查与精确验收入口 + +Codex仅源码/文档阅读、已有JSON解析、Git只读状态、静态函数/范围/哈希核对、git diff --check和新增文件空白核对。未执行任何构建/测试/设备/Git写操作。 + +精确命令:`docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md` 第2节Mac Native配置/target/过滤器/日志/失败停止;第3—5节用户后续手工Android/ADB、JSON检查和反向/生命周期矩阵。完成报告也列出Native最小命令。配置失败保留现场,不能使用旧runner或自行安装依赖伪造通过。 + +## 独立验收待填写 + +| 项目 | 当前事实 / RDC或用户证据待填 | +|---|---| +| 源码baseline/diff/hash与制品版本 | 未提交工作区源码;用户构建前保存明确diff/hash与dirty状态 | +| Mac configure/build/新用例3个与回归 | NOT RUN;记录exit/耗时/真实测试数/skipped/日志/首错 | +| APK/ZIP/ABI/大小/SHA/签名 | NOT RUN;唯一现有Validation脚本由用户手工 | +| FAST_SMOKE真实层表/plan/cache/Fast | NOT RUN;4层R[0,2)/C[2,4),decisionMismatch=0,Fast/Reuse delta>0 | +| Source异步失效/Tile/Image/Camera/Cache恢复 | NOT RUN;保存五phase文件与精确变化帧,不从末态猜Gate | +| Circle C/Annotation/Symbol R负场景 | NOT RUN;保持阻止并具备可见内容与真实更新 | +| Reload/前后台/Teardown/重新进入 | NOT RUN;销毁前文件不是销毁完成证据 | +| 原STEP-113五场景与STEP-102回归 | NOT RUN;保留正式PBF全部定义,缺源改报missing-source | +| iOS Metal设备/像素/CPU/GPU性能 | NOT VERIFIED;不以Mac状态机测试替代 | +| Git提交/推送/远端HEAD | NOT RUN且本轮禁止;后续另行授权RDCscoped收尾 | + +## 必做输出与下一步 + +调查:`docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md`。 + +验收:`docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md`。 + +报告:`docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md`。 + +以上及本记录、源文件、开发记录已由Codex实际生成。接下来由RDC独立审计并执行MacNative定向验证;用户一次手工Android/设备闭环。若缺libuv、编译失败或真实Fast仍为0,保留首错/24Gate再回交修复;不得继续扩大Symbol/CustomMVT/C放行或进入发行。 + + +## RDC 独立审核追加(2026-10-11) + +**结论:源码/差异/接口静态审核 PASS;Native 与 Android 编译、Fast 帧 NOT VERIFIED;整体 STEP-114 PARTIAL。** + +- RDC 独立阅读本轮 Native diff、C++ 新用例、STEP-114 Android Test App 新场景与菜单、生命周期接线;git diff --check 退出码 0。本轮只有范围内的五个 tracked 改动和新增 Kotlin/中文文档;原 STEP-110/111 未跟踪任务书不修改、不暂存。 +- 新增三项 Step114FastPathSafety 用例存在,覆盖无 Symbol、隐藏/Annotation Symbol 和 Source revision/cache 状态机,但**没有实际运行**。 +- Step113PbfGateScene.kt SHA-256 为 6e7befeb90ae172aaeff4b636a7640274dfb1065d8c97744cbc6af8d22b4ccae,Step102PlacementFixture.kt SHA-256 为 e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7,均未变化。 +- Java SDK 只读核对实际提供 Style.removeLayer(String) 与 MapLibreMap.getAnnotations()。Kotlin/Gradle 编译未执行。 +- Mac 预检:pkg-config --exists libuv 返回 1,与先前 macos-metal CMake required libuv 失败一致。独立 C++ header -fsyntax-only 试验退出码 1,首错为 Xcode AppleClang 找不到标准 C++ 头文件 array,实际在进入项目源码检查前终止。日志存于 Mac /tmp/step114-header-check.log。两项均是环境阻塞,不是本轮 Native 测试 PASS 或已证明代码错误;未安装/修改依赖。 +- 实际 Fast Path 命中、正反 GPU 渲染、Android 设备、性能、iOS 均未验证。正式 PBF 仍有 tide 缺源、Symbol R Placement、内建 Annotation C 的阻断。STEP-114 独立最小场景属于待用户一次 Android 手工构建集中验收内容,不能用旧 APK。 +- 后续 scoped Git 收尾若执行仅表示 PARTIAL 源码及测试入口已归档,**不代表正式发布或 Fast Path 修复完成**,不打 Tag/Source Lock。 diff --git a/docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md b/docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md new file mode 100644 index 000000000..f40c2a7cd --- /dev/null +++ b/docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md @@ -0,0 +1,172 @@ +# STEP-114 独立 Fast Smoke 场景与 RDC 验收 v1 + +日期:2026-10-11。状态:**源码交付;下列编译、测试和设备操作全部 NOT RUN。** Codex 不执行本页命令。Mac Native 由 RDC/普通 Terminal 独立执行;Android 构建、安装、ADB 与实体机步骤由用户在后续授权条件下手工统一执行。 + +## 1. 场景入口和范围 + +Test App:`STEP 093 → STEP-114 独立 FAST_SMOKE(非正式 PBF)`。正式网络 PBF 仍在 `STEP 093 → STEP-113 FZG1-PBF GATE`,保留 PBF63、STATIC_RC、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE 原实现。 + +| 独立场景 | 预期层序/保护 | 待验收结果 | +|---|---|---| +| FAST_SMOKE | R=background + 非空本地 GeoJSON/circle;C=WeatherColor + RasterParticle。无 Annotation 实例时删除空内建 Annotation layer,保留 source | Native 实际4层;R Build/Reuse 和 Full/Fast 必须真实增长 | +| CIRCLE_C_BLOCKED | baseline 后追加普通 circle C;Annotation 层仍按独立空场景移除 | 实际5层,#22/#23 阻止 Fast;R 可正常 Build/Reuse,不等于整个 scene 可 Fast | +| ANNOTATION_BLOCKED | baseline 加正常自动内建 Annotation Symbol C,并创建真实 marker;不删该层 | 实际5层;#22/#23 阻止 Fast;marker 可见、可单次 position 更新 | +| SYMBOL_R_BLOCKED | baseline 的 R 内加本地16×16图标 Symbol;无需网络 glyph/sprite | 实际5层;Symbol R 首建仍被 Placement gate 阻止;不能把关闭 transitions 当修复 | + +全部使用既有离线 ECMWF canonical 双帧资产、固定预报 `1786060800000` ms、PAUSE、30fps目标和1024粒子,SDK camera 初始33.45/129.97、zoom10.5、bearing/pitch0;手势禁用且退出恢复。30fps目标不是实际帧率测量。actualForecastTimeObservedMs 保持 null,不能当成 Native 已回读请求时间。sceneReady 只表示加载回调与控制命令成功,不替代 Native 层表/valid/实际渲染核验。 + +不要在旧正式 V23 APK 上验收本轮新入口;版本号未变,必须保存本轮源码 diff/hash、build-info 的 dirty 状态与 APK SHA。不得发行或覆盖已有版本。 + +## 2. Mac Native 精确定向命令(仅 RDC 执行) + +基于现有 `macos-metal` preset 和实际 `mbgl-test-runner` target,无全仓测试、无 Jest。STEP-113 曾缺 libuv;本轮没有安装依赖。配置失败则停止,不运行旧 runner。 + +```bash +cd /Users/tei/dev/MapLibreParticleLab +bash <<'SH' +set -eu +mkdir -p artifacts/maplibre-particle/step114-native +if cmake --preset macos-metal > artifacts/maplibre-particle/step114-native/configure.log 2>&1; then + echo 'configure exit=0' +else + step114_rc=$? + tail -n 80 artifacts/maplibre-particle/step114-native/configure.log + exit "$step114_rc" +fi +if cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 > artifacts/maplibre-particle/step114-native/build.log 2>&1; then + echo 'build exit=0' +else + step114_rc=$? + tail -n 80 artifacts/maplibre-particle/step114-native/build.log + exit "$step114_rc" +fi +if build-macos-metal/mbgl-test-runner --gtest_filter='Step114FastPathSafety.*:FastGateTrace.*:ParticleFastPath.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*:RenderSegmentDiagnostics.*:RenderOrchestratorError.*:RenderOrchestratorTeardown.*:ImageManager.*:GlyphPBF.*' --gtest_color=no --gtest_output=xml:artifacts/maplibre-particle/step114-native/tests.xml > artifacts/maplibre-particle/step114-native/tests.log 2>&1; then + tail -n 20 artifacts/maplibre-particle/step114-native/tests.log +else + step114_rc=$? + tail -n 80 artifacts/maplibre-particle/step114-native/tests.log + exit "$step114_rc" +fi +SH +``` + +记录 configure/build/tests 各退出码、耗时、实际测试数量与是否 skipped;失败回传首个真实 error、target、必要上下文与日志路径。新增测试应实际发现3个 `Step114FastPathSafety` 用例,否则不能把空过滤器执行成功作为 PASS。这些主要是纯状态机/结构判定测试,**不能证明 Android GPU Fast 命中、帧像素一致或 iOS 设备通过**。 + +## 3. Android 后续一次集中手工验收 + +由用户手工,在设备 ABI=x86_64 且 Java17/SDK 环境满足现有规范时,运行唯一既有 Validation Pipeline;不自行改 ABI、build 参数或依赖,不全量 clean。 + +```bash +cd /Users/tei/dev/MapLibreParticleLab +./tools/particle_android_validation/build_validation_apk.sh +``` + +失败保留 build.log,禁止复用旧 APK。用户手工核对同包签名和数据后再安装;本页不授权卸载用户数据。下面 ADB 全部固定既有 FZG1-2 serial,仅作为用户后续手工命令: + +```bash +adb -s 192.168.200.139:5555 shell getprop ro.product.cpu.abi +adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario FAST_SMOKE +``` + +启动后预热10秒,再观察60秒。在开始和结束分别点击“导出当前 Native 快照”。标签 `STEP114Gate` 每30秒只输出真实 Native 摘要,完整JSON写 App 私有 files。生命周期计数可以含预热,必须比较同 lifecycle/sceneGeneration 的两次计数 delta;不要称整个累计为60秒帧率。 + +菜单“单次更新 R GeoJSON”“Pan/Zoom 往返”“单次 Image 变更”“禁用/恢复缓存”保存 before/submitted/100/500/1500ms 的5份快照。一次只做一项,等恢复稳定并手动导出后再做下一项。自动文件有 actionSequence 和 phase,不追加 candidate 输入或人工 frame 结果。 + +每个负场景单独启动、预热、观察、导出,不把以下命令连续执行当验收: + +```bash +adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario CIRCLE_C_BLOCKED +adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario ANNOTATION_BLOCKED +adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step114_scenario SYMBOL_R_BLOCKED +``` + +ANNOTATION_BLOCKED 点击“单次更新真实 Annotation”,截图应见位置变化,Native 保持 fail-closed。FAST_SMOKE 中点击该操作会明确 BLOCKED(没有 marker),不创建隐藏 Annotation 或假事件。 + +## 4. 回读完整快照(用户手工) + +在普通 bash Terminal 保存全部 step114 JSON,不通过 Logcat 分块拼接。路径来自当前 Debug App: + +```bash +cd /Users/tei/dev/MapLibreParticleLab +bash <<'SH' +set -eu +mkdir -p artifacts/maplibre-particle/android-validation/step114 +adb -s 192.168.200.139:5555 shell run-as org.maplibre.android.testapp ls files > /tmp/step114-files.txt +tr -d '\r' < /tmp/step114-files.txt > /tmp/step114-files-normalized.txt +while IFS= read -r step114_file; do + case "$step114_file" in + step114-*.json) + adb -s 192.168.200.139:5555 exec-out run-as org.maplibre.android.testapp cat "files/$step114_file" > "artifacts/maplibre-particle/android-validation/step114/$step114_file" + ;; + esac +done < /tmp/step114-files-normalized.txt +SH +adb -s 192.168.200.139:5555 logcat -d -v time -s STEP114Gate:I '*:S' > artifacts/maplibre-particle/android-validation/step114/device.log +adb -s 192.168.200.139:5555 logcat -b crash -d > artifacts/maplibre-particle/android-validation/step114/crash.log +adb -s 192.168.200.139:5555 exec-out screencap -p > artifacts/maplibre-particle/android-validation/step114/screen.png +``` + +每阶段独立命名截图/日志;同名覆盖前先归档。App files 自动快照只在显式操作和生命周期变化时创建,数量无硬上限;验收后由用户管理归档,Codex 不删文件。100ms采样也可能错过一帧的变更窗口,未捕获不能标反向 Gate PASS,需保留日志再复核。 + +下面是读取**实际导出文件**的正向检查;把路径替换为稳定 FAST_SMOKE 手动快照的明确文件名(不要随意用最后的 teardown/旧场景文件): + +```bash +python3 - 'artifacts/maplibre-particle/android-validation/step114/替换为稳定FAST_SMOKE文件.json' <<'PY' +import json, sys +from pathlib import Path +d = json.loads(Path(sys.argv[1]).read_text()) +assert d['scenario'] == 'FAST_SMOKE' and d['sceneReady'] and d['blockedReason'] is None +n = d['native']; t = n['fastGateTrace'] +assert n['valid'] and n['styleLayerCount'] == 4 +assert [l['layerType'] for l in n['styleLayers']] == ['background', 'circle', 'weather-color', 'raster-particle'] +assert [s['strategy'] for s in n['segments']] == ['RETAINED', 'CONTINUOUS'] +r, c = n['segments'] +assert (r['beginIndex'], r['endExclusive'], c['beginIndex'], c['endExclusive']) == (0, 2, 2, 4) +assert not r['placementCapable'] and r['placementGatePassed'] +assert r['cache']['sourceReady'] and r['cache']['sourceStable'] and r['cache']['supported'] +assert r['cache']['build'] > 0 and r['cache']['reuse'] > 0 +assert t['fullRendered'] > 0 and t['fastRendered'] > 0 and t['decisionMismatch'] == 0 +assert len(t['gates']) == 24 +fs = [f for f in t['frames'] if f['completed'] and f['outcome'] == 'fast-rendered' + and f['post']['configRevision'] == n['configRevision'] + and f['post']['topologyRevision'] == n['topologyRevision'] + and f['post']['contentRevision'] == n['contentRevision']] +assert fs, '完整窗口没有真实完成的 Fast 帧' +for f in fs: + assert f['candidate'] and f['decisionConsistent'] and f['knownMask'] == 0xffffff and f['failMask'] == 0 + assert f['gateStatus'] == ['PASS'] * 24 + assert not f['cacheInputsTruncated'] and f['cacheInputs'] + assert all(x['valid'] and x['buildRevision'] == x['staticRevision'] for x in f['cacheInputs']) + assert f['post']['fullCpuFrame'] == f['pre']['fullCpuFrame'] + assert any(x['actualExecution'] == 'composite' for x in f['post']['retained']) +print('仅此快照 Native 正向证据通过;反向/生命周期/视觉/双平台仍需独立验收') +PY +``` + +不能通过修改导出的 JSON、补造 frame、重命名旧文件或放宽断言制造结果。 + +## 5. 定向反向、生命周期与回归矩阵 + +| 项目 | 必需证据/接受条件 | +|---|---| +| 稳定 baseline | 两次同生命周期快照 Fast/Reuse delta>0;至少一个 completed Fast 的24 Gate全通过;Build>0;orange R circle可见,天气/粒子正常;decisionMismatch=0 | +| Source mutation/异步 Tile | 单次sourceMutationCount增长;sources 表及 lastMutation 定位 step114-r-geojson;content/source revision变化;#8/#5/ready/pending 等真实拒绝证据、成功布局后 Full/Build 增长再恢复 Fast。tombstone/失败布局用Native状态机用例补充,不冒充GPU验证 | +| Image 更新 | images signature/content/global revision 导致 Full;重建后恢复。baseline图标未引用也保守失效;不能宣称已验证 glyph atlas像素 | +| Glyph | baseline完全无文字;本轮未新增 glyph mutation Test App控制。已有 GlyphPBF/ImageManager 测试仅资源回归;真实 Symbol glyph URL/font/atlas变化属于 Symbol结果证书阻塞,NOT VERIFIED,不能以image替代 | +| Pan/Zoom 往返 | SDK cameraObservedFromSdk与Native gate/key对应,Full/Build增量;恢复相机后不能直接复用旧key;截图无陈旧circle/错位 | +| 缓存禁用/恢复 | 禁用时#2失败、实际Full/live,无completed Fast增长;恢复后合法Build与Fast恢复;比较截图确认R内容/层序一致 | +| Reload | 保存reload-before与加载后快照;sceneGeneration变更、Native当前层表/plan正确,重新Build;trace lifecycle仅在实际Native reset时变,不要求UI generation=Native lifecycle | +| 非粒子C/真实Annotation | CIRCLE_C_BLOCKED/ANNOTATION_BLOCKED稳定窗口#22/#23失败、无completed Fast;真实marker可见/位置变化;#24是短暂pending保护,不要求在3秒末态仍FAIL | +| Symbol R | 本地icon可见;R placementCapable=true,正常Full placementGatePassed=false/Build0或对应安全fallback;Fast不放行,scene #23可能另挡;无证书不改判定 | +| 前后台 | baseline导出、手工Home后台10秒、回前台观察/导出;pause-before文件、恢复后Native增长、无死循环/错层/crash。此处未强制改变WeatherRuntime播放语义 | +| Teardown/重建 | 退出场景/关闭Activity产生teardown-before,dispose与callback取消;用户确认MapView销毁返回/无crash,重新启动baseline。teardown-before不是销毁完成证明 | +| 原PBF5场景 | 分别采集STEP-113五场景;仍保留全部基础63层定义/顺序及Annotation;缺tide时应明确missing-source,CustomMVT若实际出现仍unknown;不要求生产Fast>0、不篡改旧统计 | +| 原STEP-102 | T1/T2/T3/T4 fixture和既有动态poll回归;尤其含Symbol路径不因本轮无Symbol优化被放行 | +| iOS/Metal | 同Shared Core源码的Mac Metal定向测试先执行;iOS实机与GPU像素/性能未验收。Android Fast成功不能替代此项 | + +## 6. 收口规则 + +记录各场景来源commit/dirty diff、APK SHA/版本、device serial/ABI、真实Native层表/plan、before/after文件与SHA、24Gate、Full/Fast/Build/Reuse增量、截图、首错及crash日志。processSimulationAdvance是进程级,不作为单Map Fast证据。post的原始 placementTransitionActive仍可能true,但无Symbol的preFastPlacementTransition=false是本轮受控变化,decisionMismatch应仍0。 + +没有真实Fast或反向证据不足,保持 PARTIAL;Symbol证书/CustomMVT/C放行分别继续 BLOCKED/fail-closed。不得根据静态检查、runner退出0或APK成功宣称整个Fast Path修复/性能PASS。Codex未commit/push,RDC后续Git收尾另需明确授权与scoped路径,不包括STEP-110/111或正式发行。 diff --git a/docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md b/docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md new file mode 100644 index 000000000..27899d840 --- /dev/null +++ b/docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md @@ -0,0 +1,134 @@ +# STEP-114 Fast Path 安全准入与 Test App 闭环 Codex 完成报告 v1 + +日期:2026-10-11(JST)。 + +## 1. 执行结论 + +**PARTIAL:受控安全子集源码、独立最小场景与中文交付已完成;真实 Fast、编译和设备闭环未验收。Symbol R 准入 BLOCKED,CustomMVTVector 与非粒子 C 继续 fail-closed。** + +核心结果:完整场景无任何结构性 Symbol 时,全局 Placement controller 的 stale 单独不再阻止 #13;有隐藏/空Symbol或AnnotationSymbol时保持原判定。R缺源明确报missing-source,仍拒绝Build/Reuse,revision登记机制不变。独立FAST_SMOKE用现有安全GeoJSON/circle R + WeatherColor/Particle C及真实Native计数;另交付3个反向场景,不直接注入candidate或人工Frame结果。 + +本机旧STEP-113 JSON明确navsea_delivery为普通Vector,R还依赖未在Native source表中的tide;不能把该unknown-dependency归因于CustomMVTVector。这项新事实已记录于调查,旧报告与正式PBF五场景不改。 + +## 2. 实际模型、Git与版本 + +- **GPT-6.1-Sol(gpt-6.1-sol)/ high**。实际从本机 `/Users/tei/.codex/sessions/2026/10/11/rollout-2026-10-11T09-44-12-01a1286a-d6a4-7e32-b91a-a9f3e63d8f3e.jsonl` 的最新 `turn_context.model/effort/cwd` 核实;未切换/升级模型,未启动子代理、另一实例或后台Codex。 +- Mac / Darwin 24.6.0 / x86_64;仓库 `/Users/tei/dev/MapLibreParticleLab`。 +- 起点branch=`particle/main`,start HEAD=`b02e54de3d45fcf612126ae8f037cba204f590ea`;无分支/worktree变更,未commit/push,收尾HEAD保持起点。 +- 起点tracked clean;已有STEP-110/111/114三份未跟踪任务书。STEP-110/111原样保护,STEP-114只读不修改;暂存区未写。 +- `git submodule status` 起点全部空格前缀,无未初始化或偏移标记;未操作子模块。 +- Native/Test App=`13.6.1-particle.23`;没有改版本、RN公共方法/类型、Source Lock或发行。当前旧APK不包含本轮新源码。 +- Integration相关正式Contracts/RFC/ADR只读;任务提供main=`80667d6c343411d03a760425438e3d292f452118`采用既有RDC事实,没有同步/fetch/pull或远端回读,没有跨项目写入。 + +## 3. 实际修改文件(tracked) + +| 文件 | 实际内容与影响 | +|---|---| +| `src/mln/renderer/particle_fast_path.hpp` | 内部结构检测helper;非空完整层表无Symbol才可忽略controller transition;生产24Gate evaluator与失败掩码未改 | +| `src/mln/renderer/render_orchestrator.cpp` | #13输入按全场结构限定,层表数量不符保守;缺RenderSource先报missing-source;CustomMVT拒绝仍保留 | +| `test/renderer/retained_underlay_cache.test.cpp` | 3个Step114FastPathSafety用例源码:无Symbol/其它Gate保护,隐藏/空/AnnotationSymbol保护,revision/cache注册/异步失效/全局失效状态机 | +| `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt` | 独立菜单/Intent/状态恢复、统一场景退出、诊断区域与前后台生命周期;原STEP-113菜单明确标正式PBF | +| `粒子开发记录.md` | 仅末尾追加本轮中文状态与下一步 | + +## 4. 实际新增文件 + +1. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step114FastSmokeScene.kt`:独立离线FAST_SMOKE/3负场景,真实Native低频UI、操作前后文件、Source/Image/Camera/缓存/真实Annotation操作及Reload/生命周期取证。 +2. `docs/20_调查/STEP-114_FastPath安全准入与闭环判定_v1.md`:R/C/Placement控制流、旧JSON事实、mutation追踪、安全前提/反例及停止范围。 +3. `docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md`:场景说明、Mac精确定向命令、后续Android用户手工命令、实际JSON正向检查与反向/生命周期矩阵。 +4. `docs/50_Step记录/STEP-114_FastPath安全准入与TestApp闭环_v1.md`:已做/未做勾选、真实结果与待验收表。 +5. 本报告。 + +用户提供的未跟踪STEP-114任务书不是本轮生成物。旧STEP-110/111未跟踪文件未修改/暂存;原STEP-113场景类与STEP-102 fixture未修改。 + +## 5. 安全范围、兼容与零修改部分 + +R准入/RevisionBook/GPUcache算法零语义修改;缺源只修正诊断原因。没有证明CustomMVT完整revision契约,保留其拒绝。普通Vector和GeoJSON不是CustomMVT,不混淆类型。独立GeoJSON子集现有sourceDiff/immutable signature/onTileChanged/资源epoch/key保护详见调查。 + +非粒子C准入与Scene复合条件零修改,#22/#23保留。FAST_SMOKE独立Style在没有Annotation实例时移除其空内建layer;保留source,不改AnnotationManager。ANNOTATION_BLOCKED重新加载合法Style,保留内建layer并创建真实marker;不是生产零重载Annotation能力。原正式PBF五场景完整保留。 + +Symbol R的Build gate、commit/fade、scene placementStable、全图碰撞、controller计算、资源链均零修改;无最终GPU输入结果证书,停止新增签名设计。#13改动只影响完整无Symbol场景;空表/数量不一致保守,有一个隐藏/空Symbol也按原transition阻止。尚未进入旧表的新layer被immutable signature、Annotation pending和其它原Gate保护。 + +Shared Core没有平台分叉,没有新RN/JS/JNI控制API/ABI。既有可选诊断schema和24Gate顺序未改;缺源理由从模糊unknown-dependency变为已有理由missing-source是有意诊断差异。preFastPlacementTransition为本轮限定后的实际输入,post原始placementTransitionActive可能仍true;此差异不是shadow mismatch,也不是修改controller。 + +风险/限制:Native与Kotlin未编译;FAST_SMOKE尚未实测;本地sceneReady不是Native运行PASS;自动短延迟采样可能漏瞬态;App私有快照文件随显式操作增长;性能/像素/GL与Metal差异未测。Future新Placement-capable类型须同步扩展结构检测,当前只有Symbol声明CrossTileIndex::Required。 + +## 6. 与任务书差异与停止点 + +按步骤完成源码审查与可安全范围实现;任务允许PARTIAL。没有危险地放行CustomMVT、Annotation C或Symbol R,也没有复刻NavSea113层。R cache正向利用现有已支持GeoJSON,不新增未证source准入。新增无Symbol场景#13限定是调查中确认的安全局部修复;缺源更准确诊断使真正失败点可见。 + +真实Fast/反向/lifecycle闭环仍待执行。Symbol R结果证书需要更大跨平台设计,列BLOCKED;若涉及公共语义/API,回Integration RFC,不自行扩改。任务允许改测试源但禁止Codex运行编译/测试,遵守该边界。 + +## 7. 精确定向 RDC 验收命令 + +下面只交RDC/普通Terminal执行。进入仓库后逐项执行,任何失败立即停止链,保留完整日志,不用旧runner;带完整退出码保护的可复制脚本在测试说明第2节。 + +```bash +cd /Users/tei/dev/MapLibreParticleLab +cmake --preset macos-metal +cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 +build-macos-metal/mbgl-test-runner --gtest_filter='Step114FastPathSafety.*:FastGateTrace.*:ParticleFastPath.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*:RenderSegmentDiagnostics.*:RenderOrchestratorError.*:RenderOrchestratorTeardown.*:ImageManager.*:GlyphPBF.*' --gtest_color=no +``` + +renderer=Metal,Debug preset,parallel=3,target仅mbgl-test-runner。记录命令/exit/耗时/log路径/实际用例数量/skipped;新增3个Step114用例必须被发现并执行。STEP-113缺libuv的配置失败日志仍是既有证据,本轮未再次配置、未安装依赖;若仍失败,保留首错并记BLOCKED,不能宣称测试通过。 + +Android由用户后续一次手工运行 `./tools/particle_android_validation/build_validation_apk.sh`;其后手工安装/启动/取证命令逐条见测试说明第3—5节,固定serial=`192.168.200.139:5555`。入口Intent=`--es step114_scenario FAST_SMOKE`,三个负场景名为CIRCLE_C_BLOCKED/ANNOTATION_BLOCKED/SYMBOL_R_BLOCKED。完整快照为App files/step114-*.json;保存source/image/camera/cache动作前、提交后、100/500/1500ms证据。Codex没有执行这些命令。 + +## 8. 已执行检查与未执行验收 + +已执行:前置规范/任务/实际源码阅读,Git branch/HEAD/status/submodule只读;本地当前rollout模型字段核实;保留的STEP-113JSON解析、SourceType与缺tide取证;无构建静态函数/范围/哈希/追加完整性核对;tracked及新增文件空白检查。 + +**NOT RUN**:CMake/Ninja/Bazel/Native编译链接、C++测试、Kotlin编译、Gradle/APK/AAR、ADB/安装/logcat/截图/设备、Jest、性能/GPU/像素对照、iOS实机、发行、commit/push/远端HEAD。没有本轮编译/测试失败日志,也不能据此推断会通过。3个新增C++用例是源码交付,不是运行PASS;状态机markBuildSucceeded不是GPU成功证据。 + +静态检查的最终实际结果与保护哈希在第12节记录。终态仍有本轮tracked/untracked改动,未暂存、未提交。 + +## 9. 产物 + +实际产物仅源文件、测试源、四份中文文档与开发记录追加。无本轮APK/AAR/XCFramework、版本/SHA、构建/设备日志或运行截图。artifact目录的STEP-113JSON属于旧RDC证据,本轮只读未修改,不作为本轮新产物。 + +## 10. 剩余阻塞与未完成事项 + +| 项目 | 状态/原因 | +|---|---| +| 独立smoke真实Fast | 未验证;必须NativeFull/Fast/Build/Reuse及24Gate和截图共同证明 | +| Mac配置/编译/测试 | 未执行;旧libuv阻塞是否仍存在未知 | +| Android编译与设备闭环 | 未执行,留用户一次手工统一验收 | +| Source异步变化/资源/camera/cache/Reload/前后台/销毁 | 已有源码和精确取证入口,运行证据待执行;捕获失败不可假称安全反向PASS | +| Glyph实际结果/资源失效 | 未新增TestApp glyph控制;无Symbolbaseline不含glyph像素;现有资源回归不能代替Symbol证书 | +| Symbol R首次Build | BLOCKED;commit/fade没有充分稳定结果证书,不做危险简化 | +| CustomMVT/非粒子C | fail-closed;无完整revision/复合等价证明 | +| PBF缺tide/大量Symbol | 缺源诊断已清楚,原数据/业务层不改;不承诺生产Fast | +| iOS/Metal设备与CPU/GPU收益 | NOT VERIFIED;不以Android或Mac状态机结果替代 | + +## 11. 独立验收证据与后续 + +RDC先核对本报告/调查/源码diff与安全前提,再跑Mac定向命令;用户后续统一Android。baseline必须真实4层与R[0,2)/C[2,4),至少一个completed fast-rendered、knownMask=0xffffff/failMask=0、decisionMismatch=0、cacheInputs valid/revision相等,Full>0、Build>0、同生命周期Fast/Reuse delta>0;同时截图确认Rcircle/天气/粒子及反向重建正确。测试说明含读取真实JSON的断言。 + +负场景必须继续阻止Fast并正确显示/更新内容;Source/Tile/Image/Camera/缓存禁用/Reload/前后台/Teardown、旧PBF5场景及STEP-102独立回归仍需日志/快照与可见结果。只见Fast数字不等于安全/性能PASS。若未达真实Fast维持PARTIAL,失败保留现场再返回受控修复。 + +本轮禁止Git提交/push;后续达合理门槛且获授权后RDC仅scoped提交本轮明确路径并核对origin远端HEAD。不得含原STEP-110/111,不发行,不自行创建后续Step。 + +## 12. 收尾静态事实 + +- `git diff --check` 退出0;五个tracked修改路径,无其它tracked修改。四份新中文文档与新Kotlin文件各执行no-index空白检查,无诊断;Markdown代码围栏成对。 +- 生产`evaluateParticleFastPath`和`particleFastGateFailureMask`与HEAD提取的完整函数逐字相同;C范围准入、R全局/局部稳定性函数逐字相同,CustomMVT拒绝分支仍在。3个新增用例仅核对源码数量,未运行。 +- 原`Step113PbfGateScene.kt`与HEAD字节相同,SHA-256=`6e7befeb90ae172aaeff4b636a7640274dfb1065d8c97744cbc6af8d22b4ccae`;`Step102PlacementFixture.kt`同样未变,SHA-256=`e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7`。 +- 开发记录的HEAD原始162365字节前缀完整保留,原SHA-256=`0479875a30577cb497b973f89a0d74b3b8b8d300d5dfb683ed3a537749b37b3f`;仅9行/1583字节追加。 +- STEP-110 SHA-256=`33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`,STEP-111=`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`,与既有保护值相同;本次任务书只读SHA-256=`415c20df0e8d514b68dfa79cd6d9394e4c6d2c4f215c402d9a4a365862c87cfc`。 +- 旧RDC完整`source-change-after-full.json`只读SHA-256=`24a06fa1cb072a628c650723c58a880895ec656f4972fa55d3cb064bfb2aabda`;本轮新场景需要的两个cycle各metadata.json/field.f32已静态确认存在。 +- 一次用Python stdin追加中文记录的尝试因UTF-8源码解析退出1,未写文件;随后用apply_patch完成追加。最终原前缀核对通过。这是工具文本编码失败,不是构建/测试失败。 +- 终态`particle/main@b02e54de3d45fcf612126ae8f037cba204f590ea`;`git diff --cached --name-only`为空。status为本轮5个tracked修改、5个新增文件,以及原有STEP-110/111/114三任务书;未commit/push/回读远端。 + +以上只证明静态一致性、范围与保护文件完整性,不作为Native/Kotlin编译、GPU/Fast或全任务PASS。 + + +## RDC 独立审核追加(2026-10-11) + +**结论:源码/差异/接口静态审核 PASS;Native 与 Android 编译、Fast 帧 NOT VERIFIED;整体 STEP-114 PARTIAL。** + +- RDC 独立阅读本轮 Native diff、C++ 新用例、STEP-114 Android Test App 新场景与菜单、生命周期接线;git diff --check 退出码 0。本轮只有范围内的五个 tracked 改动和新增 Kotlin/中文文档;原 STEP-110/111 未跟踪任务书不修改、不暂存。 +- 新增三项 Step114FastPathSafety 用例存在,覆盖无 Symbol、隐藏/Annotation Symbol 和 Source revision/cache 状态机,但**没有实际运行**。 +- Step113PbfGateScene.kt SHA-256 为 6e7befeb90ae172aaeff4b636a7640274dfb1065d8c97744cbc6af8d22b4ccae,Step102PlacementFixture.kt SHA-256 为 e44912970dd6353787be7a3e5f9924fe1249e2305d495f1e25e707352c97d3c7,均未变化。 +- Java SDK 只读核对实际提供 Style.removeLayer(String) 与 MapLibreMap.getAnnotations()。Kotlin/Gradle 编译未执行。 +- Mac 预检:pkg-config --exists libuv 返回 1,与先前 macos-metal CMake required libuv 失败一致。独立 C++ header -fsyntax-only 试验退出码 1,首错为 Xcode AppleClang 找不到标准 C++ 头文件 array,实际在进入项目源码检查前终止。日志存于 Mac /tmp/step114-header-check.log。两项均是环境阻塞,不是本轮 Native 测试 PASS 或已证明代码错误;未安装/修改依赖。 +- 实际 Fast Path 命中、正反 GPU 渲染、Android 设备、性能、iOS 均未验证。正式 PBF 仍有 tide 缺源、Symbol R Placement、内建 Annotation C 的阻断。STEP-114 独立最小场景属于待用户一次 Android 手工构建集中验收内容,不能用旧 APK。 +- 后续 scoped Git 收尾若执行仅表示 PARTIAL 源码及测试入口已归档,**不代表正式发布或 Fast Path 修复完成**,不打 Tag/Source Lock。 diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 07e3b6f3f..1822e3d99 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -67,6 +67,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var step113DiagnosticsMaxLines = 6 private var step113Scene: Step113PbfGateScene? = null private var step113Scenario: Step113PbfGateScene.Scenario? = null + private var step114Scene: Step114FastSmokeScene? = null + private var step114Scenario: Step114FastSmokeScene.Scenario? = null + private var step114DiagnosticsHeight: Int? = null + private var step114DiagnosticsMaxLines = 6 private var step102Scenario: Step102PlacementFixture.Scenario? = null private var step102Tick = 0L private val step102Handler = Handler(Looper.getMainLooper()) @@ -356,6 +360,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } else intent.getStringExtra("step113_scenario") step113Scenario = savedStep113 ?.let { value -> Step113PbfGateScene.Scenario.values().firstOrNull { it.name == value } } + val savedStep114 = if (savedInstanceState?.containsKey("step114_scenario") == true) { + savedInstanceState.getString("step114_scenario") + } else intent.getStringExtra("step114_scenario") + step114Scenario = savedStep114 + ?.let { value -> Step114FastSmokeScene.Scenario.values().firstOrNull { it.name == value } } super.onCreate(savedInstanceState) Log.i( TAG, @@ -369,6 +378,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { diagnostics = findViewById(R.id.validationDiagnostics) mapView.addOnDidFailLoadingMapListener { errorMessage -> step113Scene?.onStyleLoadFailed(errorMessage) + step114Scene?.onStyleLoadFailed(errorMessage) if (navSeaPbfMode || purePbfMode) { Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage") } else if (realWeatherMode) { @@ -389,7 +399,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { updateNavSeaPbfButtonState() updatePbfModeButtonState() updateRetainedCacheButtonState() - if (step113Scenario != null) startStep113Scene(step113Scenario!!) + if (step114Scenario != null) startStep114Scene(step114Scenario!!) + else if (step113Scenario != null) startStep113Scene(step113Scenario!!) else if (step102Scenario != null) loadStep102Style() else if (step101Scenario != null) loadStep101Style() else if (step097Topology != null) loadStep097StencilStyle() @@ -700,6 +711,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { menu.add(0, 18, 18, "STEP-101 TEARDOWN") menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY") menu.add(0, 20, 20, "STEP-113 FZG1-PBF GATE") + menu.add(0, 21, 21, "STEP-114 独立 FAST_SMOKE(非正式 PBF)") setOnMenuItemClickListener { item -> when (item.itemId) { in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId]) @@ -714,6 +726,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 18 -> showStep101Menu(anchor) 19 -> showStep102Menu(anchor) 20 -> showStep113Menu(anchor) + 21 -> showStep114Menu(anchor) } true } @@ -777,7 +790,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private fun showStep113Menu(anchor: android.view.View) { PopupMenu(this, anchor).apply { Step113PbfGateScene.Scenario.values().forEachIndexed { index, scenario -> - menu.add(0, index, index, "STEP-113 ${scenario.name}") + menu.add(0, index, index, "STEP-113 正式 PBF · ${scenario.name}") } menu.add(0, 10, 10, "EXPORT LAST SNAPSHOT") menu.add(0, 11, 11, "MUTATE PROBE SOURCE ONCE") @@ -826,6 +839,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun stopStep113Scene() { + // Existing style-switch entry points all call this method; also stop the + // independent smoke before another scene takes ownership of this Map. + stopStep114Scene() step113Scene?.stop() step113DiagnosticsHeight?.let { originalHeight -> diagnostics.maxLines = step113DiagnosticsMaxLines @@ -836,6 +852,72 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { step113Scenario = null } + private fun showStep114Menu(anchor: android.view.View) { + PopupMenu(this, anchor).apply { + Step114FastSmokeScene.Scenario.values().forEachIndexed { index, scenario -> + menu.add(0, index, index, "独立 smoke · ${scenario.name}") + } + listOf("导出当前 Native 快照", "单次更新 R GeoJSON", "Pan/Zoom 往返", "Reload 当前场景", + "单次 Image 变更", "禁用/恢复缓存", "单次更新真实 Annotation", "退出独立 smoke") + .forEachIndexed { index, title -> menu.add(0, 10 + index, 10 + index, title) } + setOnMenuItemClickListener { item -> + when (item.itemId) { + in Step114FastSmokeScene.Scenario.values().indices -> + startStep114Scene(Step114FastSmokeScene.Scenario.values()[item.itemId]) + 10 -> step114Scene?.export() + 11 -> step114Scene?.mutateSource() + 12 -> step114Scene?.panZoom() + 13 -> step114Scene?.reload() + 14 -> step114Scene?.mutateImage() + 15 -> step114Scene?.toggleCache() + 16 -> step114Scene?.mutateAnnotation() + 17 -> { stopStep113Scene(); loadValidationStyle(active, enabled, "STEP114_EXIT") } + } + true + } + show() + } + } + + private fun startStep114Scene(scenario: Step114FastSmokeScene.Scenario) { + stopStep113Scene() + stopStep102Scenario() + stopStep099Scenario() + stopStep093DiagnosticsPolling() + step101Handler.removeCallbacksAndMessages(null) + step101Scenario = null + step097Topology = null + step093Mode = false + pureParticleMode = false + purePbfMode = false + navSeaPbfMode = false + realWeatherMode = false + weatherRuntime?.dispose() + weatherRuntime = null + styleReady = false + ++styleLoadEpoch + step114Scenario = scenario + retainedCacheEnabled = true + updateRetainedCacheButtonState() + step114DiagnosticsHeight = diagnostics.layoutParams.height + step114DiagnosticsMaxLines = diagnostics.maxLines + diagnostics.maxLines = 10 + diagnostics.layoutParams = diagnostics.layoutParams.apply { height = (156 * resources.displayMetrics.density).toInt() } + status.text = "STEP-114 独立 smoke · $scenario · 待真实 Native 验收" + step114Scene = Step114FastSmokeScene(this, maplibreMap) { diagnostics.text = it }.also { it.start(scenario) } + } + + private fun stopStep114Scene() { + step114Scene?.stop() + step114DiagnosticsHeight?.let { originalHeight -> + diagnostics.maxLines = step114DiagnosticsMaxLines + diagnostics.layoutParams = diagnostics.layoutParams.apply { height = originalHeight } + } + step114DiagnosticsHeight = null + step114Scene = null + step114Scenario = null + } + private fun showStep102Menu(anchor: android.view.View) { PopupMenu(this, anchor).apply { Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario -> @@ -2481,8 +2563,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } } else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() } - override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume() } - override fun onPause() { step113Scene?.pause(); mapView.onPause(); super.onPause() } + override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume(); step114Scene?.resume() } + override fun onPause() { step113Scene?.pause(); step114Scene?.pause(); mapView.onPause(); super.onPause() } override fun onStop() { step101Handler.removeCallbacksAndMessages(null) step093Handler.removeCallbacks(step093DiagnosticsPoll) @@ -2508,6 +2590,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { override fun onSaveInstanceState(outState: Bundle) { outState.putString("step113_scenario", step113Scenario?.name) + outState.putString("step114_scenario", step114Scenario?.name) outState.putBoolean(STATE_ACTIVE, active) outState.putBoolean(STATE_ENABLED, enabled) outState.putInt(STATE_MASK_MODE, maskMode) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step114FastSmokeScene.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step114FastSmokeScene.kt new file mode 100644 index 000000000..8742938e2 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step114FastSmokeScene.kt @@ -0,0 +1,329 @@ +package org.maplibre.android.testapp.activity.particle + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject +import org.maplibre.android.annotations.Marker +import org.maplibre.android.annotations.MarkerOptions +import org.maplibre.android.camera.CameraPosition +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.PropertyFactory.iconImage +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 +import org.maplibre.geojson.Point +import java.io.File + +/** 独立离线最小场景;不代表正式 PBF/NavSea。所有帧与缓存计数仅来自 Native。 */ +internal class Step114FastSmokeScene( + private val context: Context, + private val map: MapLibreMap, + private val display: (String) -> Unit +) { + enum class Scenario { FAST_SMOKE, CIRCLE_C_BLOCKED, ANNOTATION_BLOCKED, SYMBOL_R_BLOCKED } + + private val handler = Handler(Looper.getMainLooper()) + private var generation = 0 + private var running = false + private var paused = false + private var ready = false + private var blocked: String? = null + private var scenario = Scenario.FAST_SMOKE + private var runtime: WeatherRuntime? = null + private var marker: Marker? = null + private var savedGestures: List? = null + private var commands = JSONObject() + private var mutation = 0 + private var cameraAlternate = false + private var actionSequence = 0L + private var lastLogMs = 0L + private val poll = object : Runnable { + override fun run() { + if (!running || paused) return + try { + val native = JSONObject(map.getRenderSegmentDiagnostics()) + val trace = native.optJSONObject("fastGateTrace") + val segments = native.optJSONArray("segments") ?: JSONArray() + display(buildString { + append("STEP-114 $scenario · 独立 smoke · ${if (ready) "READY/待验收" else blocked ?: "LOADING"}\n") + append("Native Full=${trace?.optLong("fullRendered")} Fast=${trace?.optLong("fastRendered")} ") + append("windowFast=${trace?.optLong("windowFastRendered")} mismatch=${trace?.optLong("decisionMismatch")}\n") + for (i in 0 until segments.length()) { + val s = segments.getJSONObject(i) + val c = s.optJSONObject("cache") + append("${s.optString("strategy")}[${s.optInt("beginIndex")},${s.optInt("endExclusive")}) ") + append("B=${c?.optLong("build") ?: 0} Reuse=${c?.optLong("reuse") ?: 0} ") + append("${s.optString("actualExecution")} ${c?.optString("lastFallbackReason") ?: ""}\n") + } + append("固定预报/相机;操作前后快照保存在 files/step114-*.json") + }) + val now = SystemClock.elapsedRealtime() + if (now - lastLogMs >= 30000L) { + lastLogMs = now + Log.i(TAG, "scenario=$scenario ready=$ready full=${trace?.optLong("fullRendered")} " + + "fast=${trace?.optLong("fastRendered")} mismatch=${trace?.optLong("decisionMismatch")}") + } + } catch (error: Exception) { fail(error) } + handler.postDelayed(this, 3000L) + } + } + + fun start(next: Scenario = Scenario.FAST_SMOKE) { + stop() + running = true + scenario = next + ready = false + blocked = null + commands = JSONObject() + mutation = 0 + cameraAlternate = false + lastLogMs = 0L + val epoch = ++generation + display("STEP-114 $scenario · 独立离线场景加载中;正式 PBF 五场景另在 STEP-113") + val ui = map.uiSettings + savedGestures = listOf(ui.isScrollGesturesEnabled(), ui.isRotateGesturesEnabled(), + ui.isTiltGesturesEnabled(), ui.isZoomGesturesEnabled(), + ui.isDoubleTapGesturesEnabled(), ui.isQuickZoomGesturesEnabled()) + ui.setAllGesturesEnabled(false) + map.cancelTransitions() + map.setRetainedUnderlayEnabled(true) + val plan = JSONObject().put("schemaVersion", 1).put("segments", JSONArray() + .put(JSONObject().put("strategy", "RETAINED")) + .put(JSONObject().put("strategy", "CONTINUOUS").put("startLayerId", COLOR_LAYER))) + commands.put("requestedPlan", plan).put("planAccepted", map.setRenderSegmentPlan(plan.toString())) + if (!paused) handler.post(poll) + map.setStyle(Style.Builder().fromJson(styleJson())) loaded@ { style -> + if (!running || epoch != generation) return@loaded + try { + check(commands.getBoolean("planAccepted")) { "RenderSegmentPlan 未接受" } + check(map.annotations.isEmpty()) { "存在非本场景 Annotation,禁止移除其层" } + if (scenario != Scenario.ANNOTATION_BLOCKED) { + // 只在这个没有 Annotation 实例的独立场景移除空内建层。 + // 保留内建 source;生产场景和 Native Annotation 行为均不修改。 + check(style.getLayer(ANNOTATION_LAYER) != null && style.removeLayer(ANNOTATION_LAYER)) { + "空内建 Annotation 层未能移除,保留 fail-closed 现场" + } + commands.put("emptyBuiltinAnnotationLayerRemoved", true) + } else { + check(style.getLayer(ANNOTATION_LAYER) != null) { "内建 Annotation 层缺失" } + marker = map.addMarker(MarkerOptions().position(LatLng(33.45, 129.97))) + commands.put("realAnnotationAdded", true) + } + if (scenario == Scenario.SYMBOL_R_BLOCKED) { + style.addImage(IMAGE, image(Color.YELLOW)) + style.addLayerBelow(SymbolLayer(SYMBOL_LAYER, PROBE_SOURCE) + .withProperties(iconImage(IMAGE)), COLOR_LAYER) + commands.put("structuralSymbolInR", true) + } + map.cameraPosition = camera(false) + initializeFields() + ready = true + handler.removeCallbacksAndMessages(null) + if (!paused) handler.post(poll) + } catch (error: Exception) { fail(error) } + } + } + + private fun styleJson(): String { + val layers = JSONArray() + .put(JSONObject().put("id", "step114-background").put("type", "background") + .put("paint", JSONObject().put("background-color", "#172c40"))) + .put(JSONObject().put("id", "step114-r-circle").put("type", "circle").put("source", PROBE_SOURCE) + .put("paint", JSONObject().put("circle-radius", 12).put("circle-color", "#ff8800"))) + .put(JSONObject().put("id", COLOR_LAYER).put("type", "weather-color").put("source", FIELD_SOURCE) + .put("paint", JSONObject().put("weather-color-opacity", 0.62))) + .put(JSONObject().put("id", PARTICLE_LAYER).put("type", "raster-particle").put("source", FIELD_SOURCE) + .put("paint", JSONObject().put("raster-particle-active", true) + .put("raster-particle-particle-count", 1024))) + if (scenario == Scenario.CIRCLE_C_BLOCKED) { + layers.put(JSONObject().put("id", "step114-c-circle").put("type", "circle").put("source", PROBE_SOURCE) + .put("paint", JSONObject().put("circle-radius", 5).put("circle-color", "#00ffff"))) + } + return JSONObject().put("version", 8).put("name", "STEP-114 independent offline smoke") + .put("sources", JSONObject() + .put(PROBE_SOURCE, JSONObject().put("type", "geojson") + .put("data", JSONObject(Feature.fromGeometry(Point.fromLngLat(129.97, 33.45)).toJson()))) + .put(FIELD_SOURCE, JSONObject().put("type", "raster-array").put("host-field-submission", true))) + .put("layers", layers).toString() + } + + private fun initializeFields() { + val host = WeatherRuntime(map, WeatherRuntime.Config(FIELD_SOURCE, COLOR_LAYER, PARTICLE_LAYER)) + runtime = host + val submitted = JSONArray() + for (cycle in listOf("2026080700", "2026080706")) { + val path = "integration/particle-weatherv2/ecmwf-hres-wind-20260807/$cycle" + val meta = context.assets.open("$path/metadata.json").bufferedReader().use { it.readText() } + val data = context.assets.open("$path/field.f32").use { it.readBytes() } + val result = host.submitWindField(meta, data) + submitted.put(result.name) + check(result.accepted()) { "离线 Field 提交失败: $cycle $result" } + } + commands.put("fieldSubmit", submitted) + .put("forecastSeek", host.setForecastTime(FORECAST_MS).name) + .put("forecastPause", host.setPlaybackState(WeatherRuntime.PlaybackState.PAUSE).name) + .put("particleVisibility", host.setParticleVisible(true).name) + .put("targetFps", host.setTargetFps(WeatherRuntime.TargetFps.FPS_30).name) + check(host.state == WeatherRuntime.State.READY && listOf("forecastSeek", "forecastPause", + "particleVisibility", "targetFps").all { commands.getString(it) == "ACCEPTED" }) { + "固定预报/粒子控制未全部接受: $commands" + } + } + + // 操作前、提交后与短延迟分别保存真实 Native 窗口;不等待、不强制 Full、不构造 Gate 输入。 + private fun action(name: String, change: () -> Unit) { + if (!running || !ready || paused) return + val epoch = generation + val id = ++actionSequence + try { + capture("$id-$name-before") + change() + commands.put("lastAction", name).put("actionSequence", id) + capture("$id-$name-submitted") + for (delay in listOf(100L, 500L, 1500L)) { + handler.postDelayed({ + if (running && !paused && epoch == generation) { + try { capture("$id-$name-after-${delay}ms") } catch (error: Exception) { fail(error) } + } + }, delay) + } + } catch (error: Exception) { fail(error) } + } + + fun mutateSource() = action("source") { + val source = checkNotNull(map.style?.getSourceAs(PROBE_SOURCE)) + ++mutation + source.setGeoJson(Feature.fromGeometry(Point.fromLngLat(129.97 + (mutation % 2) * 0.01, 33.45))) + commands.put("sourceMutationCount", mutation) + } + + fun panZoom() = action("pan-zoom") { + cameraAlternate = !cameraAlternate + map.cameraPosition = camera(cameraAlternate) + commands.put("cameraAlternate", cameraAlternate) + } + + fun mutateImage() = action("image") { + val count = commands.optInt("imageMutationCount") + 1 + checkNotNull(map.style).addImage(IMAGE, image(if (count % 2 == 1) Color.RED else Color.YELLOW)) + commands.put("imageMutationCount", count) + } + + fun toggleCache() = action("cache-toggle") { + val enabled = !commands.optBoolean("cacheDisabled", false) + map.setRetainedUnderlayEnabled(!enabled) + commands.put("cacheDisabled", enabled) + } + + fun mutateAnnotation() = action("annotation") { + val annotation = checkNotNull(marker) { "请先选择 ANNOTATION_BLOCKED" } + ++mutation + annotation.position = LatLng(33.45, 129.97 + (mutation % 2) * 0.01) + commands.put("annotationMutationCount", mutation) + } + + fun reload() { + if (!running) return + try { capture("reload-before") } catch (error: Exception) { fail(error); return } + start(scenario) + } + + private fun envelope(native: JSONObject, phase: String) = JSONObject() + .put("schema", "step114-fast-smoke").put("schemaVersion", 1).put("appVersion", BuildConfig.VERSION_NAME) + .put("scenario", scenario.name).put("sceneGeneration", generation).put("sceneReady", ready) + .put("blockedReason", blocked ?: JSONObject.NULL).put("sampleElapsedMs", SystemClock.elapsedRealtime()) + .put("phase", phase).put("evidence", "INDEPENDENT-SMOKE; 非PBF/非113层等价") + .put("commands", JSONObject(commands.toString())).put("forecastRequestedMs", FORECAST_MS) + .put("actualForecastTimeObservedMs", JSONObject.NULL) + .put("fieldBasis", "既有离线 ECMWF canonical 双帧;非NavSea当前QBIN") + .put("cameraObservedFromSdk", JSONObject().put("latitude", map.cameraPosition.target?.latitude) + .put("longitude", map.cameraPosition.target?.longitude).put("zoom", map.cameraPosition.zoom) + .put("bearing", map.cameraPosition.bearing).put("pitch", map.cameraPosition.tilt)) + .put("native", native) + + private fun capture(phase: String) { + val record = envelope(JSONObject(map.getRenderSegmentDiagnostics()), phase) + write(record, phase) + } + + private fun write(record: JSONObject, phase: String) { + val file = File(context.filesDir, "step114-${scenario.name}-${generation}-${SystemClock.elapsedRealtime()}-$phase.json") + file.writeText(record.toString(), Charsets.UTF_8) + Log.i(TAG, "snapshot=${file.name} scenario=$scenario generation=$generation phase=$phase") + } + + fun export() { + if (!running) return + try { capture("manual") } catch (error: Exception) { fail(error) } + } + + fun onStyleLoadFailed(message: String) { if (running) fail(IllegalStateException(message)) } + + private fun fail(error: Exception) { + ready = false + blocked = "BLOCKED: ${error.message}" + display("STEP-114 $scenario $blocked") + Log.e(TAG, "scene-blocked", error) + } + + fun pause() { + if (running) try { capture("pause-before") } catch (error: Exception) { fail(error) } + paused = true + handler.removeCallbacksAndMessages(null) + } + + fun resume() { + paused = false + handler.removeCallbacksAndMessages(null) + if (running) handler.post(poll) + } + + fun stop() { + if (running) try { capture("teardown-before") } catch (error: Exception) { Log.e(TAG, "capture-failed", error) } + running = false + ++generation + handler.removeCallbacksAndMessages(null) + runtime?.dispose() + runtime = null + marker?.let { map.removeMarker(it) } + marker = null + savedGestures?.let { flags -> + val ui = map.uiSettings + ui.setScrollGesturesEnabled(flags[0]); ui.setRotateGesturesEnabled(flags[1]) + ui.setTiltGesturesEnabled(flags[2]); ui.setZoomGesturesEnabled(flags[3]) + ui.setDoubleTapGesturesEnabled(flags[4]); ui.setQuickZoomGesturesEnabled(flags[5]) + } + savedGestures = null + ready = false + Log.i(TAG, "scene-stop generation=$generation;MapView 销毁验收由 Activity/设备日志确认") + } + + companion object { + const val TAG = "STEP114Gate" + private const val ANNOTATION_LAYER = "org.maplibre.annotations.points" + private const val PROBE_SOURCE = "step114-r-geojson" + private const val FIELD_SOURCE = "step114-fields" + private const val COLOR_LAYER = "step114-color" + private const val PARTICLE_LAYER = "step114-particles" + private const val SYMBOL_LAYER = "step114-r-symbol" + private const val IMAGE = "step114-image" + private const val FORECAST_MS = 1786060800000L + + private fun camera(alternate: Boolean) = CameraPosition.Builder() + .target(LatLng(33.45, if (alternate) 130.0 else 129.97)) + .zoom(if (alternate) 11.0 else 10.5).bearing(0.0).tilt(0.0).build() + + private fun image(color: Int): Bitmap = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888) + .apply { eraseColor(color) } + } +} diff --git a/src/mln/renderer/particle_fast_path.hpp b/src/mln/renderer/particle_fast_path.hpp index af20f79be..428591a56 100644 --- a/src/mln/renderer/particle_fast_path.hpp +++ b/src/mln/renderer/particle_fast_path.hpp @@ -1,11 +1,22 @@ #pragma once +#include + #include #include #include namespace mln { +// A stale controller alone cannot affect pixels in a scene with no Symbol layers. +// Check the complete structural layer table, including hidden/empty Symbols and +// Annotation Symbols. Never substitute this for per-R placement/build evidence. +inline bool particleFastPathHasPlacementTransition(const std::vector& layers, + bool controllerTransition) noexcept { + return controllerTransition && (layers.empty() || renderSegmentHasPlacementCapableLayer( + layers, 0, static_cast(layers.size()))); +} + enum class ParticleFastPathMissReason : uint8_t { None, NoPreparedTree, diff --git a/src/mln/renderer/render_orchestrator.cpp b/src/mln/renderer/render_orchestrator.cpp index 196592ef1..f0110e831 100644 --- a/src/mln/renderer/render_orchestrator.cpp +++ b/src/mln/renderer/render_orchestrator.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -1179,7 +1180,13 @@ RenderOrchestrator::RetainedSegmentSourceState RenderOrchestrator::retainedSegme // Keep its ordinary renderer active and fail closed only for cache reuse. for (const auto& sourceId : dependencies.sourceIds) { const auto source = renderSources.find(sourceId); - if (source != renderSources.end() && source->second->baseImpl->type == style::SourceType::CustomMVTVector) { + if (source == renderSources.end()) { + // A missing source also has no revision-book entry. Report the + // concrete dependency failure before revisionFor collapses it to unknown. + result.reason = "missing-source"; + return result; + } + if (source->second->baseImpl->type == style::SourceType::CustomMVTVector) { return result; } } @@ -1278,7 +1285,10 @@ ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(Ti } } } - result.placementTransition = placementController.hasTransitions(timePoint); + const bool controllerTransition = placementController.hasTransitions(timePoint); + result.placementTransition = controllerTransition && + (retainedStyleLayers.size() != renderLayers.size() || + particleFastPathHasPlacementTransition(retainedStyleLayers, controllerTransition)); for (const auto& entry : renderSources) { result.fadingTiles = result.fadingTiles || entry.second->hasFadingTiles(); } diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index f07ee90f5..21d9bbdf4 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -526,6 +526,86 @@ TEST(ParticleFastPath, ParticleOnlyStableRevisionIsCandidate) { EXPECT_EQ(decision.reason, ParticleFastPathMissReason::None); } +TEST(Step114FastPathSafety, PlacementFreeSceneIgnoresControllerStaleOnly) { + EXPECT_TRUE(particleFastPathHasPlacementTransition({}, true)); // Missing evidence stays closed. + const std::vector layers{ + {0, "background", "background", true, std::nullopt, std::nullopt}, + {1, "probe", "circle", true, "probe-source", std::nullopt}, + {2, "color", "weather-color", true, "field", std::nullopt}, + {3, "particle", "raster-particle", true, "field", std::nullopt}}; + auto input = fastPathInput(); + input.hasPlacementTransition = particleFastPathHasPlacementTransition(layers, true); + EXPECT_FALSE(input.hasPlacementTransition); + EXPECT_TRUE(evaluateParticleFastPath(input).candidate); + // This narrow change does not override any source/tile/resource/camera gate. + input.contentRevisionMatches = false; + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::ContentMutation); + input.contentRevisionMatches = true; + input.hasPendingChanges = true; + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PendingChanges); + input.hasPendingChanges = false; + input.cameraMatches = false; + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::CameraChanged); + input.cameraMatches = true; + input.unsupportedContinuousLayer = true; + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::UnsupportedContinuousLayer); + input.unsupportedContinuousLayer = false; + input.annotationMutation = true; + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::AnnotationMutation); +} + +TEST(Step114FastPathSafety, HiddenEmptyAndAnnotationSymbolsKeepPlacementProtection) { + for (const bool visible : {false, true}) { + for (const char* id : {"symbol-in-R", "symbol-in-C", "org.maplibre.annotations.points"}) { + const std::vector layers{ + {0, "background", "background", true, std::nullopt, std::nullopt}, + {1, id, "symbol", visible, "symbols", std::nullopt}}; + EXPECT_TRUE(particleFastPathHasPlacementTransition(layers, true)); + EXPECT_FALSE(particleFastPathHasPlacementTransition(layers, false)); + auto input = fastPathInput(); + input.hasPlacementTransition = particleFastPathHasPlacementTransition(layers, true); + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::PlacementOrFade); + EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false)); + EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true)); + EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 1.0f, false)); + } + } +} + +TEST(Step114FastPathSafety, SourceRevisionRequiresRegistrationAndAsyncCompletionInvalidatesBuild) { + RenderSegmentRevisionBook book; + const RenderSegmentDependencies dependency{{"local-geojson"}, {}, false}; + EXPECT_FALSE(book.revisionFor(dependency)); + const auto missing = assessRenderSegmentSources(dependency, [](const std::string&) + -> std::optional { return std::nullopt; }); + EXPECT_FALSE(missing.ready); + EXPECT_FALSE(missing.stable); + EXPECT_STREQ(missing.reason, "missing-source"); + book.advanceSource("local-geojson"); + const auto initial = book.revisionFor(dependency); + ASSERT_TRUE(initial); + RetainedUnderlayCache cache; + EXPECT_EQ(cache.evaluate(key(), *initial, true), RetainedUnderlayDecision::Build); + cache.markBuildSucceeded(); + EXPECT_EQ(cache.evaluate(key(), *initial, true), RetainedUnderlayDecision::Reuse); + book.advanceSource("local-geojson"); // Immutable source data change. + const auto submitted = book.revisionFor(dependency); + ASSERT_TRUE(submitted); + EXPECT_NE(initial, submitted); + EXPECT_EQ(cache.evaluate(key(), *submitted, false, "source-not-loaded"), RetainedUnderlayDecision::Fallback); + EXPECT_FALSE(cache.isValid()); + book.advanceSource("local-geojson"); // Models onTileChanged after asynchronous layout. + const auto completed = book.revisionFor(dependency); + ASSERT_TRUE(completed); + EXPECT_NE(submitted, completed); + EXPECT_EQ(cache.evaluate(key(), *completed, true), RetainedUnderlayDecision::Build); + cache.markBuildSucceeded(); + EXPECT_EQ(cache.evaluate(key(), *completed, true), RetainedUnderlayDecision::Reuse); + book.advanceGlobal(); // Image/glyph/style/context change remains global. + EXPECT_NE(completed, book.revisionFor(dependency)); + EXPECT_EQ(cache.evaluate(key(), *book.revisionFor(dependency), true), RetainedUnderlayDecision::Build); +} + TEST(ParticleFastPath, RevisionCameraAndViewportMismatchReject) { auto input = fastPathInput(); input.revisionMatches = false; diff --git a/粒子开发记录.md b/粒子开发记录.md index 8c244a622..1f8caaf6c 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1144,3 +1144,14 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 2026-10-11 STEP-113 RDC:标准Android Validation APK构建exit=0;FZG1-2安装成功,五场景稳定Logcat摘要(Native层64/66/66/66/67)均decisionMismatch=0;STATIC_RC Full875/Fast0,PARTIAL_RCRR Full861/Fast0,发现cache-invalid外#22/#23同时阻断。完整113层、手动Source变化、性能和回归未验收,整体PARTIAL;详细证据追加于STEP与报告。 - 2026-10-11 STEP-113第二轮:FZG1-2已保存Source变更前后各120帧完整Snapshot,decisionMismatch=0;单次Source mutation=1,Native最后变更为step113-source-probe、影响C1;稳定119帧同时命中#3/#4/#22/#23。已确认内建annotation symbol进入C,且#22/#23可共享根因;Mac Native GTest因缺libuv未运行,NavSea完整113层/性能仍PARTIAL。详见STEP记录追加段落。 + +### 2026-10-11 STEP-114 Fast Path 安全准入与独立 Test App(源码交付 PARTIAL) + +- 实际模型已核实 GPT-6.1-Sol(gpt-6.1-sol)/ high;Mac particle/main 起点 b02e54de3d45fcf612126ae8f037cba204f590ea。无模型切换、子代理、commit/push/发行。 +- Shared Core 仅修正完整无 Symbol 场景被 controller stale 误挡的 #13 输入;隐藏/空/Annotation Symbol、空表/数量不一致继续保守。CustomMVTVector、非粒子 C、Symbol R Build/scene/资源/碰撞保护均保留 fail-closed。 +- 回读旧 STEP-113 完整 JSON:navsea_delivery 实际是 Vector;R 依赖 tide-spots→tide 但 Native source 表缺 tide。缺源提前明确报 missing-source,不补假源、不放行缓存、不改原 PBF 五场景。 +- 新增独立 FAST_SMOKE 与 Circle C/真实 Annotation/Symbol R 三个反向场景;只在独立空 Annotation 场景移除空内建 layer,使用 Native 真实 Full/Fast/Build/Reuse/24Gate,单次操作自动保存前后短延迟快照。新增3个非 Jest C++用例源码。 +- 中文调查、场景/RDC精确验收命令、Step与完成报告已实际落盘;仅静态检查,未编译/测试/Gradle/ADB/Jest/设备。真实 Fast 未验收,Symbol R 结果证书 BLOCKED,不标 Fast Path 已修复。 +- 下一步 RDC 独立审计/执行 Mac Native 定向检查;用户一次手工 Android/设备闭环并保存源码diff/制品SHA/逐帧/截图证据。详见 docs/报告/STEP-114_FastPath安全准入与TestApp闭环_Codex完成报告_v1.md 与 docs/60_测试/STEP-114_独立FastSmoke场景与RDC验收_v1.md。 + +- 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,后续用户一次手工构建验收。