test: add STEP-119 RCRR boundary variants and audit
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 17:42:26 +09:00
parent 46c88b0209
commit 1072816740
7 changed files with 296 additions and 20 deletions

View File

@@ -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/`。

View File

@@ -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 一致性。

View File

@@ -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 收尾通过。

View File

@@ -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))
}
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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。