diff --git a/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md b/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md index 61403bbc1..36914d892 100644 --- a/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md +++ b/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md @@ -55,3 +55,12 @@ Mac Metal原生CMake配置首次因PATH找不到cmake而失败,改用既有/us 本轮源码锁 3d989a00a822989339a6d7cb4d23acc04396670e、APK SHA 57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135,Mac标准Android OpenGL x86_64 APK真编译/链接/打包PASS;FZG1-2安装SHA一致、preflight PASS。新STATIC_RC_NO_TIDE 90.010秒:Native64/R[0,62),missingSourceIds=[],Cache Build=1 / Reuse=3276(累计)、FastDelta=1819 / FullDelta=0、24Gate mismatch=0。原STATIC_RC 90.010秒:原Native65/R[0,63),missingSourceIds=[tide],Build=0、Reuse=0、FastDelta=0、FullDelta=305,fail-closed仍正常。新13项GTest运行/Metal/iOS/动态源变更与像素对比均未完成,保持ANDROID_FAST_CORE_PASS/OVERALL_PARTIAL。详细原始数据和sha见 docs/60_测试/STEP-119_FZG1_原PBF真实Fast验收结果_20261011.md。 - 2026-10-11 STEP119实机首次FAST_OBSERVED:APK源3d989a00a822989339a6d7cb4d23acc04396670e SHA=57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135,FZG1-2安装/preflight PASS;STATIC_RC_NO_TIDE 90秒 FastDelta1819/FullDelta0、R Build1/Reuse3276、24Gate mismatch0、Native64,missingSourceIds=[]。仅正向目标首次通过,负例/视觉/GTest/iOS未验,STEP119 PARTIAL。详见 docs/报告/STEP-119_Android真机Fast首次通过_阶段验收_v1.md。 + +## RDC STEP-119 RCRR 多配置续验(2026-10-11 JST) + +- Integration origin/main 经 RDC 同步核验为 `80667d6c343411d03a760425438e3d292f452118`;Particle 起点 `46c88b0209474376225b54ae0ad701881673743d`,仍在 `particle/main`;两份 STEP110/111 未跟踪任务文档未触碰。 +- 同一已安装 APK 源 `3d989a00a822989339a6d7cb4d23acc04396670e`,SHA `57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135`,真机原 `PARTIAL_RCRR` 80秒:R0 Build0/Reuse0/missing tide;R2/R3 各 Build1/Reuse536;FastDelta0/FullDelta596、decisionMismatch0。即局部复用有效、全帧 Fast 安全拒绝。 +- 真机导出原63层 `styleParity.baseLayerManifest` 确认 tide=2、sea=3、clip-outline=31、special=37;天气实际在原宽 C 中,不在 R0。由此修正早期错误的分段位置推测。 +- Test App 增加三种 `RCRR_*_NO_TIDE` 场景,分别测试窄 C、宽 C 与错误动态 R;基于上述真实 Manifest 校准成 Native64 精确 R/C 区间,未修改正式 PBF/原场景/24 Gate。验证器精确验三段 R 边界;4项 Python unittest PASS、Style Manifest 静态索引核对 PASS、git diff --check PASS。 +- **尚未完成**:新 Kotlin Test App 的用户手工 APK 编译及三场景真机 90秒回归、图像目视、Camera/Source/Style/Tile/Annotation/Context 真机失效和恢复、STEP119 13项 GTest 真跑、Metal/iOS。Mac 原有 libuv 缺失限制保持,不擅自安装或升级工具链。STEP-119 **PARTIAL,不 CLOSED**。 +- 文档:`docs/60_测试/STEP-119_RCRR多配置验收矩阵_v1.md`、`docs/报告/STEP-119_RCRR多场景局限性_阶段验收_v1.md`。原始真机记录位于 ignored `artifacts/maplibre-particle/android-validation/step119-rcrr-observe-20261011_172720/partial_rcrr/`。 diff --git a/docs/60_测试/STEP-119_RCRR多配置验收矩阵_v1.md b/docs/60_测试/STEP-119_RCRR多配置验收矩阵_v1.md new file mode 100644 index 000000000..21c6f121d --- /dev/null +++ b/docs/60_测试/STEP-119_RCRR多配置验收矩阵_v1.md @@ -0,0 +1,68 @@ +# STEP-119 RCRR 多配置实机与验证矩阵 v1 + +日期:2026-10-11(JST)。执行入口:Mac.lan RDC,Particle `particle/main`。状态:**一组真机完成;三组新配置已实现、静态核对通过但未构建/上机**。非最终验收。 + +## 1. 场景与边界 + +本轮沿用 STEP-119 源码 APK(commit `3d989a00a822989339a6d7cb4d23acc04396670e`,SHA-256 `57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135`),不重复已通过的 Android 原生完整编译。设备 `192.168.200.139:5555`,x86_64。当前远端 HEAD 起点为 `46c88b0209474376225b54ae0ad701881673743d`,其与 APK commit 的后续差异为已有报告/Step 文档,而非新 Android 源码。 + +不改正式 PBF/Sprite/Glyph、Source 接口、Shared Core、24 Gate、发布版本,不清理 STEP-110/111 两份未跟踪任务书。保留原 `PARTIAL_RCRR`、原 `STATIC_RC` 与 `STATIC_RC_NO_TIDE`,后续新场景只在测试副本中排除唯一 `tide-spots→tide` 孤立 Symbol 层,并恢复空 Annotation 辅助层的正常生命周期。 + +## 2. 已执行:原 PARTIAL_RCRR + +- 80.007 秒实机,PID `28169`,完整摘要 3 份,同源 APK/设备 SHA 一致。 +- 原始 Native 图层 66,原 63 PBF + 2 天气 + 1 SDK Annotation;R[0,3)、C[3,39)、R[39,64)、R[64,66)。 +- R0:Build 0、Reuse 0、Fallback 595、missingSourceIds=[tide];R2:Build 1、Reuse 536;R3:Build 1、Reuse 536。 +- 观测区间:FastDelta=0、FullDelta=596,最后 120 帧 Full=119、Fast=0,decisionMismatch=0。 +- Native 24 Gate 累计 `unsupported-continuous-layer=599`、`unsupported-scene=599`、`cache-invalid=595`;这些是场景累计,**不是**对应原因的独立帧数。 +- 结论:**全帧 Fast 安全拒绝通过;局部 R2/R3 的 Build/Reuse 已实际观察到**。这不构成 RCRR 加速性能通过。 + +原证据: +`artifacts/maplibre-particle/android-validation/step119-rcrr-observe-20261011_172720/partial_rcrr/result.json`,同目录有 `logcat.txt` 与 `screen.png`。 + +## 3. 基于真实设备 Style Manifest 校准分段 + +从设备 `run-as` 只读提取的 `step113-STATIC_RC-324220201.json`,包含当前实际 63 层 `baseLayerManifest`;其 PBF 指纹 `e0f9c2d1df38ae5d5953b561c52efe2f8590336de80b988e1d2935580d30b996` 与已通过 Fast 的场景一致。 + +原基础层:background=0、base-raster=1、tide-spots=2、sea-area-fill=3、clip-outline=31、special-pattern-line-754=37、最后的 place-labels-land-ja=62。天气插在 clip-outline **之后**。因此原 R0 是前三层(包括 tide),天气并未落入 R0;原 C1 是宽 C,包含天气与普通静态 PBF 层。禁止再用此前错误的“天气在 R0”解释原负例。 + +新增 Test App 变体(全部保留其余 62 原图层、天气数据与顺序;移除 SDK 空 Annotation 辅助层;Native 总层数预期 64): + +| 场景 | 期望 R/C 索引 | 主要目标 | +|---|---|---| +| `RCRR_C_NARROW_NO_TIDE` | R[0,31)、C[31,33)、R[33,63)、R[63,64) | C 仅含 WeatherColor/Particle;检验所有 R 缓存与整帧 Fast 能否真实通过 | +| `RCRR_C_WIDE_NO_TIDE` | R[0,2)、C[2,38)、R[38,63)、R[63,64) | C 混入普通 PBF;验证不支持的连续图层是否安全拒绝整帧 Fast | +| `RCRR_R_DYNAMIC_NO_TIDE` | R[0,33)、C[33,38)、R[38,63)、R[63,64) | 故意将动态天气放入 R;检验版本/连续输入不允许错误静态复用 | + +源级校准证据: +`artifacts/maplibre-particle/android-validation/step119_rcrr_style_structure.json`。该结果是**实际设备导出数据上的静态切分核对,不是新 APK GPU 验证**。Mac 通过 Python 直接 GET 正式 Style 曾收到 HTTP 403,因此优先复用了相同 Style 指纹的设备导出 Manifest;没有修改服务器资源。 + +## 4. 静态测试与当前缺口 + +新增 `tools/particle_android_validation/test_step119_rcrr_scenarios.py`,四项 Python unittest 全部 PASS:三种索引互相排斥、缺 tide/缺 Annotation 清理不得通过、范围尾部和段数不对必须拒绝、原 STATIC_RC_NO_TIDE 不回退为错误结构。验证器 `scene_parity_valid` 对 64 层 RCRR 三 R 段改用精确区间校验;原 63 层判断、90 秒观察、24 Gate 和 Fast 判据不放宽。`git diff --check` PASS。 + +**没有对新 Kotlin 场景做 Android 编译/安装**(遵守用户手工 Android 编译及本轮不重复已有完整编译限制)。新增三场景的 Kotlin 可编译性与 Native 真机结果必须记 `NOT VERIFIED`。新增 13 项 STEP-119 GTest 仍未运行;Mac Metal CMake 被既有 libuv 缺失阻塞,本轮不安装或升级工具链;iOS Metal/实体机亦未验证。 + +## 5. 下次同一次人工 APK 构建后的串行设备验收 + +源码 scoped commit/push 完成后,由用户按已有标准在 Mac 手工构建**新 HEAD** 的 x86_64 Android OpenGL Test APK,不得使用旧 `3d989a` APK冒充新场景;验证 build-info commit、APK SHA、设备 base.apk SHA 一致后,逐一运行: + +```bash +cd /Users/tei/dev/MapLibreParticleLab +./tools/particle_android_validation/build_validation_apk.sh +adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --preflight-only +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --scenario RCRR_C_NARROW_NO_TIDE --seconds 90 +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --scenario RCRR_C_WIDE_NO_TIDE --seconds 90 +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --scenario RCRR_R_DYNAMIC_NO_TIDE --seconds 90 +``` + +每步独立读取退出码与 result.json:完整 R/C、Source/Tile、R0/R2/R3 Build/Reuse/Invalidate、Fast/Full 差分、24 Gate、120 帧、生命周期/判定一致性。正向窄 C 必须先 Full 建立证书,再 Build 和成功 Reuse 后 Fast;若失败记录真实 gate,而不是降阈值。宽 C 和动态 R 是安全负例,`NO_FAST_OBSERVED/exit2` 若符合完整原因和分段预期属于负例通过,而非直接整体失败。禁止直接用这些负例宣称 Fast 不可用。 + +之后须补:静止视觉 Full/RCRR 同资源同视角人工航标、文字、Symbol、层序对照;Camera/Source/Style/Tile/Annotation/Context/cache toggle 的瞬时失效、Full 回退、重新稳定后的 Build/Reuse;13 GTest 真实执行和 Mac Metal/iOS 独立验证。视觉比对时动态粒子会造成像素差异,不能简单拿整屏逐像素一致作为静态 Symbol 结论。 + +## 6. 当前判定 + +`ANDROID_RCRR_FAIL_CLOSED_OBSERVED / NEW_VARIANTS_SOURCE_READY / STEP119_PARTIAL`。 + +主要已确认局限:多 R 缓存复用并不代表全帧 Fast;含缺源 R 会阻断、过宽 C 中非支持类型的连续层触发拒绝;未实测新窄 C 前,不能推断 RCRR 性能收益。待验证风险:多 RenderTarget/Composite 成本、全局 Symbol Placement/Atlas 证书联动、局部更新导致跨 R 全局失效、上层标记/文字的层序与图像等价、iOS/Metal 一致性。 diff --git a/docs/报告/STEP-119_RCRR多场景局限性_阶段验收_v1.md b/docs/报告/STEP-119_RCRR多场景局限性_阶段验收_v1.md new file mode 100644 index 000000000..4c11e82d3 --- /dev/null +++ b/docs/报告/STEP-119_RCRR多场景局限性_阶段验收_v1.md @@ -0,0 +1,64 @@ +# STEP-119 RCRR 多场景局限性阶段验收报告 v1 + +日期:2026-10-11 JST + +## 一、目标与结论 + +**结论:部分可行/阶段 PARTIAL。** Android 真机已证明一个原始四段 RCRR 场景能让后两段 R 分别 Build=1、Reuse=536;但最前 R 缺失 `tide`,同时宽 C 不满足当前 Fast 场景约束,整帧持续 Full。已新增三种区分 C 宽度及错误动态 R 的 Test App 对照入口,并据设备真实 Style 63 层清单完成静态区间核对。新增入口仍需用户手工编译 APK 后实测,不能宣称三种 GPU 场景 PASS;STEP-119 不 CLOSED。 + +## 二、实际基线和执行链 + +- RDC 设备:Mac.lan;Mac 仓库 `/Users/tei/dev/MapLibreParticleLab`,当前分支 `particle/main`,本轮起点 HEAD `46c88b0209474376225b54ae0ad701881673743d`。 +- Integration `main` 已通过 RDC `git fetch` + `git merge --ff-only`,本地/远端一致 `80667d6c343411d03a760425438e3d292f452118`;已读取 `PARTICLE_PROJECT_INSTRUCTIONS.md`、README、WORKFLOW、TEAM_MODEL、contracts/rfc/adr/status/compatibility、项目 AGENTS/规范/总则。 +- APK 源码锁:`3d989a00a822989339a6d7cb4d23acc04396670e`,SHA-256:`57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135`,ADB:`192.168.200.139:5555`。此次没有重复 Android 编译/安装。源 HEAD 与已安装 APK 的差异由先前文档提交构成;新测试源码改动后,后续新场景须重新进行用户手工 APK 构建、SHA 锁定。 +- 既有 `STATIC_RC_NO_TIDE` 正向 FastDelta=1819、FullDelta=0、Build=1、Reuse=3276 和原 `STATIC_RC` 63 层缺源负向 Fast=0/Full=305,均为已验收历史证据,没有重复执行。 + +## 三、已执行真机 RCRR 和证据 + +`PARTIAL_RCRR` 原场景运行 80.007 秒、PID=28169,3 个完整 Native 摘要,设备 SHA 一致。Native 总数 66;R[0,3) / C[3,39) / R[39,64) / R[64,66)。 + +| 指标 | 实际结果 | +|---|---| +| R0 | Build=0 / Reuse=0 / missingSourceIds=[tide] | +| R2 | Build=1 / Reuse=536 | +| R3 | Build=1 / Reuse=536 | +| 80 秒增量 | Fast=0 / Full=596 | +| 最近 120 帧 | Fast=0 / Full=119 | +| decisionMismatch | 0 | +| 24 Gate 累计 | unsupported-continuous-layer=599、unsupported-scene=599、cache-invalid=595 | + +执行进程退出码 0,表示**原生观测及取证成功**,不表示 RCRR Fast 成功。证据路径: +`artifacts/maplibre-particle/android-validation/step119-rcrr-observe-20261011_172720/partial_rcrr/result.json`、`logcat.txt`、`screen.png`。 + +### 关键事实校正 + +从设备 Debug App 的 `step113-STATIC_RC-324220201.json` 只读提取 `styleParity.baseLayerManifest`,正式 63 层索引:`tide-spots=2`、`sea-area-fill=3`、`clip-outline=31`、`special-pattern-line-754=37`。其生产 Style 指纹与当日 Fast 成功场景一致。原 R0 只有 background / base-raster / tide-spots;**天气在 C 内而不是 R0**。原 C 范围很宽,混合了普通海图图层和天气动态层。此前对 R0 图层组成的初步猜测已经被真实 Manifest 修正,未写入最终推荐结论。 + +## 四、新增三种 RCRR Test App 入口(未上机) + +- `RCRR_C_NARROW_NO_TIDE`:R[0,31) / C[31,33) / R[33,63) / R[63,64),C 只包含两天气层。用于验证全段缓存和最终 Fast 的可能性。 +- `RCRR_C_WIDE_NO_TIDE`:R[0,2) / C[2,38) / R[38,63) / R[63,64),C 混合天气与静态 PBF。用于验证白名单边界和宽 C 的实际代价。 +- `RCRR_R_DYNAMIC_NO_TIDE`:R[0,33) / C[33,38) / R[38,63) / R[63,64),天气被放入错误 R。用于验证失效与 Full 安全拒绝。 + +代码变更严格局限于 Test App `Step113PbfGateScene.kt`、`verify_step113_fast_path.py` 和新增 `test_step119_rcrr_scenarios.py`,保留原所有场景。生产 PBF63 的定义、服务器 URL、天气实际字段及图层相对顺序不变;只在明确 NO_TIDE 测试副本排除已确认的唯一孤立层。对新场景增加精确 R 范围匹配,不降低原验证器 24 Gate 或 FAST_OBSERVED 条件。 + +源级校验:设备实际 Manifest 分段结构 3/3 与预期一致;Python unittest 4/4 PASS;`git diff --check` PASS。没有 Android Kotlin/NDK 编译或新 APK/设备验收,**三种新增场景为 SOURCE_READY / NOT VERIFIED**。 + +## 五、当前 RCRR 模式的局限性 + +1. **全帧 Fast 是全局准入。** R2/R3 的 Cache 能各自 Build/Reuse,但 R0 缺源时整帧仍 Full;不能按各 R Reuse 求和冒充 Fast。 +2. **连续 C 不是任意图层的免责通道。** 原宽 C 下 `unsupported-continuous-layer` 持续增加,说明当前 Fast 路径不支持直接把大量普通海图层划入 C 并期望跳过完整场景处理。需要以新窄 C 实测来确认适用边界。 +3. **配置高度依赖真实 Style 顺序。** 同一个 layerId 相对位置设错可能造成循环 C 混入静态层或天气落入 R;实际 PBF 63 层与 NavSea 113 层现场定义不同,不能按历史索引照搬。 +4. **缓存分段独立性有限。** 即使 R 分段有缓存,Source/Style/Tile/Symbol Placement/Atlas/Opacity/Annotation 的全局结果版本可能要求跨段失效;当前 13 项源级 C++ 测试尚未真正 GTest 运行,相关安全边界待进一步真机验证。 +5. **性能与显存不是必然收益。** 多个 R RenderTarget/Composite 会增大缓存资源与混合成本;当前没有同场景 RC/RCRR 同相机有效帧时间和 GPU 内存对照,不能声称多段比单 R 更快。 +6. **视觉与平台仍未关闭。** 尚无强制 Full/RCRR 同 Style 静态航标、Symbol、文字、层序的人工确认;Metal CMake 仍因既有 libuv 缺失未配置成功,13 GTest 与 iOS 真机没有新验证。 + +## 六、风险与下一步 + +建议以**窄 C、其余静态 R**作为 RCRR 正向候选,保留原 63 层 RCRR 和两个新负向变体。下一次由用户按既有 Mac 标准手工只进行一轮最新源码 APK 构建后,RDC 串行执行三场景各 90 秒真实诊断和同视角视觉对照;若窄 C 仍被 unsupported gate 拒绝,应保留 Full、检查准确原因和局限边界,而不是修改 Gate 放行。 + +状态为 `ANDROID_RCRR_FAIL_CLOSED_OBSERVED / MULTI_RCRR_SOURCE_READY / OVERALL_PARTIAL`。更详细命令、矩阵和预期证据位于 `docs/60_测试/STEP-119_RCRR多配置验收矩阵_v1.md`。本次不是 GSRL 正式发行,没有 SOURCE_LOCK、发布 Tag 或版本覆盖;未跟踪 STEP-110/111 文档禁止纳入提交。 + +## 七、Git 收尾 + +本段由 RDC 在完成 scoped commit/push 与远端回读后补写最终提交、Push 与远端 HEAD;在此之前不得声称 Git 收尾通过。 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 f4c37f69d..aae912f2e 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 @@ -28,7 +28,8 @@ internal class Step113PbfGateScene( private val map: MapLibreMap, private val display: (String) -> Unit ) { - enum class Scenario { PBF63, STATIC_RC, PARTICLE_OFF, PARTIAL_RCRR, SOURCE_CHANGE, STATIC_RC_NO_TIDE } + enum class Scenario { PBF63, STATIC_RC, PARTICLE_OFF, PARTIAL_RCRR, SOURCE_CHANGE, STATIC_RC_NO_TIDE, + RCRR_C_NARROW_NO_TIDE, RCRR_C_WIDE_NO_TIDE, RCRR_R_DYNAMIC_NO_TIDE } private val handler = Handler(Looper.getMainLooper()) private var generation = 0 @@ -126,7 +127,7 @@ internal class Step113PbfGateScene( // STEP-118: Keep the original 63-layer PBF baseline unchanged. // Only the explicitly named user-approved variant excludes one // orphaned Symbol layer whose tide Source is already absent. - val omitTideLayer = scenario == Scenario.STATIC_RC_NO_TIDE + val omitTideLayer = isNoTideVariant(scenario) val tideDependent = (0 until base.length()).filter { base.getJSONObject(it).optString("source") == TIDE_SOURCE } @@ -179,7 +180,7 @@ internal class Step113PbfGateScene( .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及STATIC_RC_NO_TIDE加载时移除空SDK辅助层;后续添加不再移除") + .put("internalAnnotationPolicy", "STATIC_RC及全部NO_TIDE场景加载时移除空SDK辅助层;后续添加不再移除") .put("styleAdjustmentScope", if (omitTideLayer) "TEST-APP-ONLY;原生产Style63层不变;测试副本排除唯一tide-spots→tide孤立Symbol层" else "NONE;生产Style基础63层原样") @@ -189,7 +190,7 @@ internal class Step113PbfGateScene( check(!sources.has(SOURCE)) { "天气 Source ID 冲突" } sources.put(SOURCE, JSONObject().put("type", "raster-array").put("host-field-submission", true)) val layers = JSONArray() - val prefix = scenario == Scenario.PARTIAL_RCRR + val prefix = isRcrrVariant(scenario) for (i in 0 until effectiveBase.length()) { layers.put(effectiveBase.getJSONObject(i)) if (prefix && effectiveBase.getJSONObject(i).getString("id") == "clip-outline") addFields(layers) @@ -222,16 +223,24 @@ internal class Step113PbfGateScene( .put("baseParityBasis", if (omitTideLayer) "同次SDK生产Style63层回读;测试副本仅排除tide-spots,其余62层定义逐项比较;不等同原63层像素验收" else "同次SDK生产Style回读;完整定义逐层比较,不是设备画面证明") - val plan = planJson(scenario, effectiveIds.last()) - parity.put("testPlanSelector", if (scenario == Scenario.PARTIAL_RCRR) - "结构RCRR:sea-area-fill/special-pattern-line-754/最后生产层ID;非现场113边界" else - if (hasField) "当前NavSea源码RC:rn-weather-color" else "基础63层全RETAINED") + val clipIndex = effectiveIds.indexOf("clip-outline") + check(clipIndex >= 0 && clipIndex + 1 < effectiveIds.size) { + "生产Style的clip-outline之后没有基础图层;不可推断RCRR天气位置" + } + val plan = planJson(scenario, effectiveIds.last(), effectiveIds[clipIndex + 1]) + parity.put("testPlanSelector", when (scenario) { + Scenario.PARTIAL_RCRR -> "旧RCRR缺源负对照:R0包含缺源tide,C1混合天气与大量静态PBF" + Scenario.RCRR_C_NARROW_NO_TIDE -> "RCRR窄C:仅天气;两段后续静态R" + Scenario.RCRR_C_WIDE_NO_TIDE -> "RCRR宽C:天气及部分PBF;后续静态R" + Scenario.RCRR_R_DYNAMIC_NO_TIDE -> "RCRR错误动态R:天气落R0;验证安全拒绝" + else -> 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@ { style -> if (!running || epoch != generation) return@testStyle try { - if (scenario == Scenario.STATIC_RC || scenario == Scenario.STATIC_RC_NO_TIDE) { + if (scenario == Scenario.STATIC_RC || isNoTideVariant(scenario)) { removeEmptyAnnotationLayer(style, ids) } map.cameraPosition = CameraPosition.Builder().target(LatLng(33.45, 129.97)) @@ -494,13 +503,27 @@ internal class Step113PbfGateScene( private const val ANNOTATION_LAYER = "org.maplibre.annotations.points" private const val FORECAST_MS = 1786060800000L - private fun planJson(s: Scenario, finalBaseId: String): String { + private fun isRcrrVariant(s: Scenario): Boolean = s == Scenario.PARTIAL_RCRR || + s == Scenario.RCRR_C_NARROW_NO_TIDE || s == Scenario.RCRR_C_WIDE_NO_TIDE || + s == Scenario.RCRR_R_DYNAMIC_NO_TIDE + + private fun isNoTideVariant(s: Scenario): Boolean = s == Scenario.STATIC_RC_NO_TIDE || + s == Scenario.RCRR_C_NARROW_NO_TIDE || s == Scenario.RCRR_C_WIDE_NO_TIDE || + s == Scenario.RCRR_R_DYNAMIC_NO_TIDE + + private fun planJson(s: Scenario, finalBaseId: String, firstAfterClipId: String): String { val segments = JSONArray().put(JSONObject().put("strategy", "RETAINED")) if (s != Scenario.PBF63) { - segments.put(JSONObject().put("strategy", "CONTINUOUS") - .put("startLayerId", if (s == Scenario.PARTIAL_RCRR) "sea-area-fill" else COLOR)) - if (s == Scenario.PARTIAL_RCRR) { - segments.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", "special-pattern-line-754")) + val continuousStart = when (s) { + Scenario.PARTIAL_RCRR, Scenario.RCRR_C_WIDE_NO_TIDE -> "sea-area-fill" + Scenario.RCRR_R_DYNAMIC_NO_TIDE -> firstAfterClipId + else -> COLOR + } + segments.put(JSONObject().put("strategy", "CONTINUOUS").put("startLayerId", continuousStart)) + if (isRcrrVariant(s)) { + val upperRetainedStart = if (s == Scenario.RCRR_C_NARROW_NO_TIDE) + firstAfterClipId else "special-pattern-line-754" + segments.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", upperRetainedStart)) segments.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", finalBaseId)) } } diff --git a/tools/particle_android_validation/test_step119_rcrr_scenarios.py b/tools/particle_android_validation/test_step119_rcrr_scenarios.py new file mode 100644 index 000000000..a3d71376a --- /dev/null +++ b/tools/particle_android_validation/test_step119_rcrr_scenarios.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""STEP-119 RCRR 验证器结构契约测试;合成样本不代表真机 GTest 或 GPU PASS。""" +import importlib.util +import unittest +from pathlib import Path + +MODULE = Path(__file__).with_name("verify_step113_fast_path.py") +spec = importlib.util.spec_from_file_location("step113", MODULE) +verifier = importlib.util.module_from_spec(spec) +spec.loader.exec_module(verifier) + + +def sample(retained): + return { + "planValid": True, + "basePreserved": False, + "tideLayerExcluded": True, + "originalBaseLayerCount": 63, + "effectiveBaseLayerCount": 62, + "excludedLayerId": "tide-spots", + "excludedLayerSource": "tide", + "excludedLayerCount": 1, + "actualLayers": 64, + "commands": {"emptyBuiltinAnnotationLayerRemoved": True, + "annotationSourcePreserved": True}, + "sources": [{"sourceId": "navsea_delivery"}, + {"sourceId": "rn-weather-fields"}], + "retained": retained, + } + + +def regions(r0_end, r2_start): + return [ + {"begin": 0, "end": r0_end}, + {"begin": r2_start, "end": 63}, + {"begin": 63, "end": 64}, + ] + + +class Step119RcrrVerifierTests(unittest.TestCase): + def test_three_rcrr_shapes_have_distinct_expected_boundaries(self): + shapes = { + "RCRR_C_NARROW_NO_TIDE": regions(31, 33), + "RCRR_C_WIDE_NO_TIDE": regions(2, 38), + "RCRR_R_DYNAMIC_NO_TIDE": regions(33, 38), + } + for scenario, retained in shapes.items(): + with self.subTest(scenario=scenario): + self.assertTrue(verifier.scene_parity_valid(sample(retained), scenario)) + for other in shapes: + if other != scenario: + self.assertFalse(verifier.scene_parity_valid(sample(retained), other)) + + def test_missing_source_or_annotation_policy_never_passes(self): + for scenario, retained in [ + ("RCRR_C_NARROW_NO_TIDE", regions(31, 33)), + ("RCRR_C_WIDE_NO_TIDE", regions(2, 38)), + ("RCRR_R_DYNAMIC_NO_TIDE", regions(33, 38)), + ]: + with self.subTest(scenario=scenario): + a = sample(retained) + a["sources"].append({"sourceId": "tide"}) + self.assertFalse(verifier.scene_parity_valid(a, scenario)) + b = sample(retained) + b["commands"]["emptyBuiltinAnnotationLayerRemoved"] = False + self.assertFalse(verifier.scene_parity_valid(b, scenario)) + + def test_wrong_final_extent_and_wrong_retained_count_rejected(self): + a = sample(regions(31, 33)) + a["retained"][-1]["end"] = 65 + self.assertFalse(verifier.scene_parity_valid(a, "RCRR_C_NARROW_NO_TIDE")) + b = sample(regions(31, 33)[:2]) + self.assertFalse(verifier.scene_parity_valid(b, "RCRR_C_NARROW_NO_TIDE")) + + def test_existing_static_no_tide_contract_preserved(self): + a = sample([{"begin": 0, "end": 62}]) + self.assertTrue(verifier.scene_parity_valid(a, "STATIC_RC_NO_TIDE")) + self.assertFalse(verifier.scene_parity_valid(a, "RCRR_C_NARROW_NO_TIDE")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tools/particle_android_validation/verify_step113_fast_path.py b/tools/particle_android_validation/verify_step113_fast_path.py index 5dcdfd400..3f0f98363 100755 --- a/tools/particle_android_validation/verify_step113_fast_path.py +++ b/tools/particle_android_validation/verify_step113_fast_path.py @@ -18,7 +18,10 @@ APK = ARTIFACTS / "MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk" BUILD_INFO = ARTIFACTS / "build-info.txt" PACKAGE = "org.maplibre.android.testapp" ACTIVITY = f"{PACKAGE}/{PACKAGE}.activity.particle.ParticleCoreLayerValidationActivity" -SCENARIOS = ("STATIC_RC", "PBF63", "PARTICLE_OFF", "PARTIAL_RCRR", "SOURCE_CHANGE", "STATIC_RC_NO_TIDE") +SCENARIOS = ("STATIC_RC", "PBF63", "PARTICLE_OFF", "PARTIAL_RCRR", "SOURCE_CHANGE", "STATIC_RC_NO_TIDE", + "RCRR_C_NARROW_NO_TIDE", "RCRR_C_WIDE_NO_TIDE", "RCRR_R_DYNAMIC_NO_TIDE") +NO_TIDE_SCENARIOS = {"STATIC_RC_NO_TIDE", "RCRR_C_NARROW_NO_TIDE", + "RCRR_C_WIDE_NO_TIDE", "RCRR_R_DYNAMIC_NO_TIDE"} LOG_ENTRY = re.compile(r"^[A-Z]/STEP113Gate\(\s*(\d+)\):\s*(\{.+\})\s*$") @@ -106,6 +109,24 @@ def extract(logcat, pid, expected): return [x[1] for x in sorted(result, key=lambda item: item[0])] +def rcrr_segments_valid(sample, scenario): + """Verify exact R/C boundaries in the new isolated, 64-layer PBF Test App cases.""" + retained = sample.get("retained", []) + if len(retained) != 3: + return False + r0, r2, r3 = retained + if (r0.get("begin") != 0 or r2.get("end") != r3.get("begin") + or r3.get("end") != 64 or r3.get("begin") != 63): + return False + if scenario == "RCRR_C_NARROW_NO_TIDE": + return r0.get("end") == 31 and r2.get("begin") == 33 + if scenario == "RCRR_C_WIDE_NO_TIDE": + return r0.get("end") == 2 and r2.get("begin") == 38 + if scenario == "RCRR_R_DYNAMIC_NO_TIDE": + return r0.get("end") == 33 and r2.get("begin") == 38 + return False + + def scene_parity_valid(sample, scenario): """Fail closed on the *declared* base style, not just a loose layer count. @@ -117,7 +138,7 @@ def scene_parity_valid(sample, scenario): """ if not sample.get("planValid"): return False - if scenario != "STATIC_RC_NO_TIDE": + if scenario not in NO_TIDE_SCENARIOS: return sample.get("basePreserved") is True and not sample.get("tideLayerExcluded", False) return ( sample.get("basePreserved") is False @@ -131,9 +152,16 @@ def scene_parity_valid(sample, scenario): and sample.get("commands", {}).get("emptyBuiltinAnnotationLayerRemoved") is True and sample.get("commands", {}).get("annotationSourcePreserved") is True and all(source.get("sourceId") != "tide" for source in sample.get("sources", [])) - and len(sample.get("retained", [])) == 1 - and sample["retained"][0].get("begin") == 0 - and sample["retained"][0].get("end") == 62 + and sample.get("actualLayers") == 64 + and ( + (scenario == "STATIC_RC_NO_TIDE" and + len(sample.get("retained", [])) == 1 and + sample["retained"][0].get("begin") == 0 and + sample["retained"][0].get("end") == 62) + or + (scenario != "STATIC_RC_NO_TIDE" and + rcrr_segments_valid(sample, scenario)) + ) ) @@ -222,7 +250,7 @@ def run(): "sourceReport": ( "STEP-118 user-approved TEST-APP-ONLY 62-layer derivative; " "original 63-layer PBF Style remains unmodified; not STEP-114 smoke" - if args.scenario == "STATIC_RC_NO_TIDE" + if args.scenario in NO_TIDE_SCENARIOS else "STEP-113 original 63-layer PBF test scene; not STEP-114 smoke" ), "durationSecondsRequested": args.seconds, diff --git a/粒子开发记录.md b/粒子开发记录.md index 37a0018a0..6856aaeb2 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1198,3 +1198,5 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 2026-10-11 STEP119 RDC Android实机:源码锁3d989a00a822989339a6d7cb4d23acc04396670e,新APK SHA 57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135、Mac现有Gradle/NDK整包编译PASS、FZG1-2安装设备SHA一致。新STATIC_RC_NO_TIDE真实90秒R Build=1/Reuse=3276(累计)、FastDelta=1819/FullDelta=0、120帧119Fast/0Full、24Gate mismatch0,证实Symbol缓存首次Build/后续Fast生效。原63 STATIC_RC 90秒因tide缺源保持Build/Reuse/Fast全0、FullDelta=305,缺源fail-closed有效。Mac原生GTest受旧libuv环境缺失阻断,未运行13项测试;Metal/iOS、Pan/Zoom/Rotate、Source/Style异步变更、像素/人工目视仍未验,整体PARTIAL。证据docs/60_测试/STEP-119_FZG1_原PBF真实Fast验收结果_20261011.md。 - 2026-10-11 STEP119实机首次FAST_OBSERVED:APK源3d989a00a822989339a6d7cb4d23acc04396670e SHA=57d68800770d431a2ce9247406f636e7a01cc19fccaf4c59f5eb3a7c99128135,FZG1-2安装/preflight PASS;STATIC_RC_NO_TIDE 90秒 FastDelta1819/FullDelta0、R Build1/Reuse3276、24Gate mismatch0、Native64,missingSourceIds=[]。仅正向目标首次通过,负例/视觉/GTest/iOS未验,STEP119 PARTIAL。详见 docs/报告/STEP-119_Android真机Fast首次通过_阶段验收_v1.md。 + +- 2026-10-11 STEP-119 RCRR续验:原四段 `PARTIAL_RCRR` 在已锁 APK 真机80秒 R0缺tide(Build0/Reuse0)、R2/R3各Build1/Reuse536;FastDelta0/FullDelta596、24Gate mismatch0,安全Full回退。真机原Style Manifest证实tide索引2、clip索引31、weather实际在宽C内。新增窄C、宽C、错误动态R三种Test App RCRR_NO_TIDE对照入口和精确验证器;Style结构静态核对PASS、非Jest Python 4 tests PASS。新APK编译/实机、13项GTest、Metal/iOS和视觉仍未验证,STEP119保持PARTIAL。见docs/60_测试/STEP-119_RCRR多配置验收矩阵_v1.md及docs/报告/STEP-119_RCRR多场景局限性_阶段验收_v1.md。