feat: add STEP-113 Android PBF fast gate diagnostics
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 09:11:27 +09:00
parent c6d13e1059
commit 92efa36f4c
14 changed files with 1368 additions and 5 deletions

View File

@@ -0,0 +1,41 @@
# STEP-113 — NavSea FZG1 PBF/Layer 等价测试 App 与逐帧 Fast Gate 诊断
## 授权与范围
用户明确授权:在 Particle Android 测试 App 接入与 NavSea 相同的正式 PBF 与图层分段,找到每帧阻止 Particle Fast Path 的所有 Gate;RDC 可以编译 APK、使用已连接 FZG1-2 的 ADB 测试。使用 Codex **GPT-6.1-Sol High** 实施源码,RDC 独立编译/设备验收。不得把调研中的假设冒充真机结果。
## 项目与协作
- Particle repo: /Users/tei/dev/MapLibreParticleLab, `particle/main`; NavSea **只读**: /Users/tei/dev/NavSea-Build。Integration **只读**: /Users/tei/dev/navsea-particle-integration;已 fetch 核实 Integration main 80667d6c。
- 先完整阅读 Particle `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、`docs/00_项目总则/README.md` 和其四份有效总则,以及 STEP-112 源码调查、上一步报告。
- Native/Shared Core 仅增加诊断,Android Test App 增加测试 UI/场景。**禁止**改变现有 Fast Path Gate 准入逻辑、Placement/Fade、安全回退、公开 RN API、NavSea 源码、Cache 等价安全判定。Common Core 诊断数据不能含 Android 专有语义。
- 仓库中两个已有未跟踪 STEP-110/111 任务 MD 必须原样保留,不得 add/reset/clean;禁止新分支或工作树。
- Codex 不编译、不执行 Android Gradle/ADB、不执行 Jest(用户没有要求),不做正式发行、Git commit/push;由 RDC 负责这些步骤。
## 目标 A:准确复制生产 Style、场景与真实 R/C 分层
现有测试 App 已支持 `NAVSEA PBF`,使用生产 URL `https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json`,但仅有 **63** 个基础图层;它与 NavSea FZG1 实测 **113** 个最终渲染图层尚未等价。NavSea 源码 `src/config/builtinRuntimeUrlConfig.ts` 是 URL 事实源;`src/map/particle/RenderSegmentHost.tsx` 是宿主分段控制。STEP-112 用户 FZG1 的 RCRR_PREFIX 诊断:R0 [0,13), C1 [13,33), R2 [33,107), R3 [107,113)。注意 NavSea 当前源码 R/C 初始 plan 可能与运行诊断不一致,必须记录版本/选择器,不得当作同一事实。
- 基于现有 `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`/fixture 增加独立 **STEP-113 FZG1-PBF Gate** 测试场景。真正加载生产 Style、Sprite、Glyph、VectorTile URI;原有场景全部保留,且不得改为假矢量地图。
- 与 NavSea 对齐层顺序、类型、Source ID 和显隐规则:优先从 NavSea 实际源码、已有 FZG1 快照/资料得到完整清单,至少能证明原始63层一致;对于 RN 额外动态层,若没有真实 Source/图层定义,不得将同名假图层宣称为“同等画面”,应分别标注原样/结构仿真/缺失。严格实现全113层和所述四分段如能力允许;若其余层无法准确复制,仍完成可运行的 **真实 PBF 63 层模式 + 明示的部分等价模式**,并记录阻塞原因,不要卡死整个任务。
- 每个场景导出/Logcat:实际 styleLayers (index/id/type/source/visibility)、总层数、每段 [begin,end)/类型与层名、分段配置接受/拒绝原因、当前 Style URL/版本/fingerprint、预报时间和 Particle Tick 状态;严格用实际 Layer ID 定义分段,不假定第13/33/107的 ID 永远不变。
- 支持至少静态30fps粒子且 camera/forecast fixed、粒子关闭、Source 变化、场景对照。静态场景不可每500ms自行修改 Symbol;原 STEP-102 的动态 fixture 保持原样。
## 目标 B:逐帧 24 Gate 完整诊断
- 阅读 `src/mln/renderer/particle_fast_path.hpp` 的 `ParticleFastPathGateInput` (24 条固定顺序) 与 `src/mln/renderer/renderer_impl.cpp::tryRenderParticleFastFrame`;每次真实 Fast 尝试,记录统一 frame/sequence、所有 24 条 Gate 的 **PASS/FAIL** 或明确不可观测的 UNKNOWN,并保存首个实际短路 Gate 与同帧失败全集。不得通过二次调用有副作用的函数来获取字段;优先使用现成的已计算 `ParticleFastPathGateInput` 在单次调用无副作用完成独立掩码计算,并确保首错与 `evaluateParticleFastPath` 决定一致。
- 对于 CacheInvalid 等早期阻断,也能看出后面的 `unsupported-continuous-layer` / `unsupported-scene` 是否同帧成立。记录 per-R cache state/build/reuse/invalidation/fallback, Placement changed/transition/fade、原因分类、FullFrame count。不要把 Full 绘制导致的 post frame Placement 数据冒充 pre-fast 原子时刻;必要时为 pre/post 两种 phase 分离记录。
- 诊断必须为有界环形缓冲/低开销低频导出(例如保留最近120或256帧,另聚合失败计数),不得每 33ms 从 JS/Android UI Logcat 输出大 JSON。沿用已存在的 Android `getRenderSegmentDiagnostics` Native JSON 出口,新增 `fastGateTrace` 可选诊断字段,避免新 RN API/ABI;必要时增加手动/低频截图/Logcat 导出入口。控制每分钟几条摘要而非每帧 log。
- 同一渲染生命周期 reset 要有清晰逻辑;不引入 data race,遵循现有 shared snapshot/atomic/锁语义,避免在 map UI 线程操作 Native mutable renderer state。
## 目标 C:可验收性
- Test App UI 能切换 STEP-113 场景、查看 Gate 首错/失败排名与各段状态;可用 Logcat 定向标签稳定抽取机器可读 JSON (包含 schema/version, style parity summary, frame window计数, gate-failure聚合, R状态摘要)。不要让 UI 实时多次 Native 查询产生额外帧,使用约2至5秒刷新。
- 提供实际验证步骤(Mac PowerShell不要,macOS shell可用),基于标准 `tools/particle_android_validation/build_validation_apk.sh` 构建,确认设备 ABI 与脚本默认 ABI一致后交 RDC 运行。ADB 必须明确 `-s 192.168.200.139:5555`(这个设备本轮是 FZG1-2),不可无参数误操作其他设备。
- 文字报告区分:SOURCE-PROVEN / TEST-APP-STRUCTURAL / DEVICE-MEASURED / NOT-VERIFIED。成功构建 APK 不表示真机性能验证通过。
## 必做输出与收尾
1. 所需 Shared Core / Android Test App 源码实现(尽量只局部变更)。
2. `docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md`:实际状态、设备验收填写栏、剩余风险。
3. `docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md`:改动文件列表、与 NavSea 样式/层等价度证据、24 Gate 定义、精确构建/ADB/场景命令、产物/日志预期、静态检查事实、未做项。
4. `粒子开发记录.md` 中文末尾追加少量本次进展(append-only)。
5. Codex 最终回报真实 model=GPT-6.1-Sol / high,以及剩余需要 RDC 独立验证事项。
6. 文件变更仅 scoped;`git diff --check`,不提交不 push;避免 Jest 和 Native/Android 长时编译。任何公共跨项目 API 变更先停、写明需要 Integration RFC。
## 优先次序
优先先完成 **B 24Gate真正可靠同帧采集**,再实现已有 PBF Mode 的专项布局与 UI;不允许因为“113层不能精确复刻”就省略实际可交付的完整 Gate 诊断。

View File

@@ -0,0 +1,85 @@
# STEP-113 FZG1 PBF Layer 与 Fast Gate 诊断 v1
日期:2026-10-11(JST)。状态:**进行中;源码交付PARTIAL,等待RDC独立编译/设备验收,113层完整等价缺证据。**
## 目标、基线与边界
优先B:每次真实Fast尝试可靠保存24 Gate、首错及失败全集;再交付真PBF专项场景、低频UI和机器可读导出。
- Mac `particle/main@c6d13e1059d464d312ac33acac87967300fa5dc4`,仓库 `/Users/tei/dev/MapLibreParticleLab`。
- 已实际核实模型 `gpt-6.1-sol/high`;NavSea只读 `ad9aa043`,Integration本地 `80667d6c`。
- 当前源码版本 `13.6.1-particle.23` 未改;本次修改未发行。
- 只修改Shared Core诊断、Android Test App、相关C++测试源码、本Step/报告及开发记录追加。禁止改变Gate/Placement/Fade/Cache安全判定、公开RN方法、NavSea/Integration、构建基线、正式发行或Git提交推送。
- 既有STEP-110/111/113未跟踪任务书原样保留,无branch/worktree变更。
## 必做项与当前结果
- [x] 同一次已计算input保存24项状态掩码与生产首错,一致性计数明确,不重复副作用函数。
- [x] 120帧有界环形数据、生命周期及窗口聚合,所有Gate记录;pre/post明确分离。
- [x] per-R实际pre cache validity/revision和两phase的cache build/reuse/invalidation/fallback/原因/执行状态;每phase最多16R,超限显式。
- [x] 使用现有Native JSON出口追加可选fastGateTrace;共享锁快照,UI不访问Renderer mutable state。
- [x] 明确reset位置;普通Full miss不清历史。新增4个C++回归用例,尚未运行。
- [x] 正式URL专项PBF63/STATIC_RC/PARTICLE_OFF/PARTIAL_RCRR/SOURCE_CHANGE场景源码;保留原场景和STEP-102动态fixture。
- [x] 运行时原63层完整定义/顺序保存检查,真实Sprite/Glyph/Tile URI检查、Style fingerprint/版本/Layer表/plan接受及最终解析原因导出。
- [x] 固定camera/forecast请求、30fps目标;禁用手势;静态轮询不改Symbol/Source/forecast。
- [x] UI每3秒单次query、30秒摘要、手动最近snapshot导出;Logcat标签STEP113Gate,Base64分块防截断。
- [x] 中文完成报告、精确RDC命令、开发记录末尾追加与静态范围检查。
- [ ] 113层全量生产Layer/Source/显隐/业务数据与现场四段数字边界:缺同版本完整快照,未复制。
- [ ] Native/Kotlin编译、定向测试、APK、FZG1-2资源/层/逐帧/性能/开销验收:本轮Codex未执行。
## 证据等级与剩余风险
SOURCE-PROVEN:NavSea URL配置、当前源码RC选择器、shared天气片段ID/type/source。TEST-APP-STRUCTURAL:保序63层和部分RCRR检查代码、额外Source探针、既有canonical风场。DEVICE-MEASURED:本轮无结果。NOT-VERIFIED:正式服务回读(本机域名无法解析)、设备实际Layer表/113层全等价、构建与性能。
原始JSON63层不包含SDK可能追加的Annotation等内部层;必须使用实际Native styleLayers总数与Layer ID解析边界。部分RCRR锚点不同于现场[0,13)/[13,33)/[33,107)/[107,113),不能把结构测试说成同等画面。NavSea当前RenderSegmentHost源码RC与该历史现场RCRR_PREFIX分别记录。
pre里的changed/fade是上一完成RenderTree快照;当前Controller transition另存preFastPlacementTransition。Fast post不是新Placement结果。Debug simulationAdvance是进程级计数;实际forecast时间未新增Native回读接口,导出null并保存accepted请求。环形覆盖会丢弃最早详情但保留聚合;内存字节/CPU开销未实测。不能从零Fast提升或APK编译成功直接判性能PASS。
## 独立验收填写栏(由RDC追加真实结果)
| 项目 | 本轮状态 / RDC证据待填 |
|---|---|
| 源码diff/hash、分支/HEAD/dirty | Codex交付未提交;RDC填写制品对应源码 |
| 设备serial/主ABI/ABI list | 目标192.168.200.139:5555;未读取 |
| Native配置/编译/4个新用例与相关回归 | NOT RUN;填写命令/exit/耗时/日志/首错 |
| APK build-info/ZIP/ABI/字节/SHA/签名/安装 | NOT RUN;标准Validation脚本,禁止复用失败旧APK |
| PBF63完整资源、实际Layer manifest | NOT VERIFIED;填写SDK回读fingerprint和原/实际层数 |
| STATIC_RC首错/24Gate/window/mismatch | NOT VERIFIED;mismatch必须0,保存120帧详情 |
| PARTICLE_OFF/固定预报/camera | NOT VERIFIED;保存对照窗口和请求接受结果 |
| PARTIAL_RCRR实际四段/Layer ID/缓存状态 | NOT VERIFIED;明确部分等价,不能补写113层PASS |
| SOURCE_CHANGE基线/单次变化/pre-post | NOT VERIFIED;记录Source mutation和两次导出 |
| reset/前后台/退出/旧STEP-102回归 | NOT VERIFIED;不能以进程正常退出替代场景检查 |
| 内存/trace开销/CPU/FPS/截图/crash | NOT VERIFIED;不能用单制品构建成功替代 |
| 完整113层及原始现场selector | 缺证据;另需真实同版本快照 |
## 构建、检查与必做输出
Codex仅执行只读调查、文本一致性/范围检查、git diff --check与新增文件空白检查;无编译、C++运行、Jest、Gradle/APK、ADB、设备、发行或Git提交推送。
精确macOS Shell构建/ADB/场景/Logcat/私有JSON回读及Native定向命令见:`docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md` 第5节。Android ABI先匹配标准脚本x86_64,所有设备命令固定 `-s 192.168.200.139:5555`,由RDC执行。
交付源码/测试、本记录、上述完成报告、粒子开发记录追加已落盘。下一步仅由RDC独立验收;失败保留首个真实error及完整日志,再按本任务范围修复。不得借此放宽Fast/Placement/Cache安全判定或进入正式发行。
## RDC 独立验收补记(2026-10-11,设备证据)
以下是 Codex 退出后 RDC 新执行的证据,不修改上文 Codex 未构建/未实测的历史结论。本次阶段判定:**PARTIAL;Android 构建 PASS;五场景稳定基线采样 PASS;113 层等价与性能 NOT VERIFIED**。
- 环境:Mac、particle/main;构建源码基于 c6d13e1059d464d312ac33acac87967300fa5dc4 加 STEP-113 未提交改动,build-info 明确 git_dirty=true。此 APK 不是 Source Lock / 正式发行。
- 标准 build_validation_apk.sh 于 2026-10-11 08:35:08 JST 退出码 0;Gradle BUILD SUCCESSFUL in 19m 22s;Java 17.0.20.1;OpenGL/x86_64;ZIP/ABI/体积/SHA 自检通过。
- APK:artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk;66,587,454 bytes;libmaplibre.so 40,033,976 bytes;SHA-256 0054248bc3872e8670df4e9a10d11f1d65c0912a0482586cd7a247e880a6eaef。记录:同目录 build-info.txt、build.log。
- 设备:FZG1-2,ADB 192.168.200.139:5555,主 ABI x86_64;明确 -s 的 adb install -r 返回 Success,未卸载数据;设备安装后版本 13.6.1-particle.23。正式 PBF Style 模式运行并加载。启动前数秒 sceneReady=true 但 Native Layer=0 / plan not-configured 是加载暂态;只接受之后的 Native Layers 非零、planValid=true 稳定记录。
- 五个场景均由 ADB 明确 serial 启动;STEP113Gate Logcat Base64 summary 成功还原。每场景实际设备稳定状态的 decisionMismatch 都是 **0**。Full/Fast/Attempts 为生命周期累计,并非精确 60 秒窗口。
| 场景 | Native Layers | Attempts | Full | Fast | Retained 及关键证据 |
|---|---:|---:|---:|---:|---|
| PBF63 | 64(63+内部层) | 106 | 106 | 0 | R[0,64) Build0,unknown-dependency;无粒子持续驱动 |
| STATIC_RC | 66 | 876 | 875 | 0 | R[0,63) Build0,unknown-dependency;unsupported-continuous-layer、unsupported-scene 各876 |
| PARTICLE_OFF | 66 | 70 | 70 | 0 | no-particle-repaint=70;非相等时长性能对照 |
| PARTIAL_RCRR | 66 | 862 | 861 | 0 | R0[0,3) unknown-dependency;R2[39,64) 与 R3[64,66) Build0/static-placement;两个 unsupported Gate 各862 |
| SOURCE_CHANGE | 67 | 888 | 888 | 0 | 基线已记录;**没有执行菜单手动 Source mutation** |
- STATIC_RC 同帧失败全集汇总 CacheInvalid=870、revision-mismatch=871、placement-or-fade=780;PARTIAL_RCRR CacheInvalid=856。这些失败条件可能在**同一帧**并存,不能累加为帧数。即使修复缓存 Build,也还有 #22/#23 与其它独立 Gate。
- 设备摘要 JSON(不纳入 Git):artifacts/maplibre-particle/android-validation/step113/ 下的 pbf63-summary-ready.json、static-rc-summary.json、particle-off-summary.json、partial-rcrr-summary.json、source-change-baseline-summary.json。它们只是低频 summary,不是导出的完整 120 帧。
- 仍 NOT VERIFIED:原 NavSea 113 层完整 Layer/Source/显隐及真实四段边界;实际 PBF/Sprite/Glyph 画面截图;Source 单次变更后前后对照;120帧手动详情导出;C++ GTest 运行;reset/后台、旧 STEP-102 场景回归;CPU/GPU/诊断开销;生产性能提升、正式发行。不得把部分结构等价当成现场复测或将 STEP-113 判为 CLOSED/PASS。
- 下一阶段必须分别处理 retained first-build 的安全准入、unsupported continuous layer、unsupported scene;不能直接删除 Gate 放行。

View File

@@ -0,0 +1,250 @@
# STEP-113 FZG1 PBF Layer 与 Fast Gate 诊断 Codex 完成报告 v1
日期:2026-10-11(JST)。结论:**PARTIAL:诊断和专项测试场景源码已交付;构建、设备及生产113层完整等价未验收。**
## 1. 基线、模型与授权
- Mac.lan / Darwin 24.6.0 / x86_64;仓库 `/Users/tei/dev/MapLibreParticleLab`。
- 起点及交付 HEAD:`particle/main@c6d13e1059d464d312ac33acac87967300fa5dc4`;无 commit/push、无新 branch/worktree、无发行。
- 实际模型 **GPT-6.1-Sol(`gpt-6.1-sol`)/ high**,从本会话 `01a12808-4436-7d33-bc56-9135722348b1` rollout 的 `turn_context.model/effort` 核实。未切换模型、未启动其他 Codex/子代理。
- Native/Test App 的源码版本 `platform/android/VERSION=13.6.1-particle.23`,RN package 同为 `.23`;没有改版本或覆盖正式制品。新诊断只存在于本次未提交源码,不能把现有正式 V23 APK 当本次制品。
- NavSea 只读 HEAD `ad9aa043fde37f87462f31de3fbd8ce008f8424b`,读取时 tracked/untracked status 均无输出;Integration 本地 HEAD `80667d6c343411d03a760425438e3d292f452118`。没有 fetch/pull、跨项目写入或消息发送;Integration 远端核验采用用户提供的 RDC 事实。
- 起点 tracked clean;已有 STEP-110/111/113 三份未跟踪任务书均保留。子模块状态均为空格前缀,没有操作子模块。
- 已读根 AGENTS、PARTICLE_BASELINE、项目规范全部文件、总则 README 和四份有效总则、STEP-113 任务书、STEP-112 调查/记录/报告,以及 STEP-102/106 的相关实现与记录。
- 本轮不执行 CMake/Ninja/Bazel、Native 测试、Gradle/APK、ADB/安装/Logcat、Jest、发布、提交或 push。以下命令仅交 RDC 执行。
## 2. 实际修改与新增文件
| 文件 | 变更目的 |
|---|---|
| `src/mln/renderer/particle_fast_path.hpp` | 24项固定顺序表、无副作用失败掩码;生产 evaluator 原样保留 |
| `include/mln/renderer/render_segment.hpp` | 平台无关帧/phase/cache诊断值结构,现有 snapshot 可选 `fastGateTrace` |
| `src/mln/renderer/retained_underlay_runtime_state.hpp` | 120帧环形缓冲、生命周期聚合、受锁保护的快照/reset/完成记录 |
| `src/mln/renderer/render_segment_json.hpp` | 现有 JSON 出口增加可选 trace,24项状态、窗口与生命周期计数、pre/post |
| `src/mln/renderer/renderer_impl.cpp` | 原始 input/decision 采集点、实际 pre cache validity/revision、渲染后完成记录 |
| `src/mln/renderer/renderer.cpp` | 生命周期 reset 与没有 RenderTree 时的明确 outcome |
| `src/mln/map/map.cpp` | 外部诊断读取携带 trace;renderer 内部读不复制历史环形数据 |
| `test/renderer/retained_underlay_cache.test.cpp` | 新增4个 `FastGateTrace.*` C++ 回归用例;未运行 |
| `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt`(新增) | 真 PBF、五个场景、parity证明/限制、固定预报、低频UI与分块导出 |
| 同目录 `ParticleCoreLayerValidationActivity.kt` | 菜单/Intent、scene生命周期与旧场景隔离,诊断区域临时扩展 |
| `docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md`(新增) | 当前状态、RDC验收填写栏 |
| 本完成报告(新增) | 源码交付事实、命令、证据与限制 |
| `粒子开发记录.md` | 仅末尾追加本次短记录 |
没有修改 Shader、Placement、Retained Cache 的准入/等价/回退算法、RN 类型或方法、JNI 方法签名、NavSea、Integration、构建流水线和发行版本。扩展的是任务明确授权的可选诊断 JSON 与其内部值结构,不新增公共控制 API/Host 契约。若后续需要新的公共控制面,应先走 Integration RFC。本任务不为未来接口变更提供授权。
## 3. B:24 Gate 同帧记录与真实性边界
每次真实 `tryRenderParticleFastFrame` 用**已经计算好的同一个 input**调用原 evaluator 一次,再独立计算掩码。没有再次调用 `particleFastPathDynamicState`、needsRepaint、Placement 计算或 cadence 推进。新增 Cache getter 是 `const noexcept` 的纯字段读取;用于观察已被 aggregate 短路掩盖的 R 状态。
`knownMask=0x00ffffff`:当前24条 evaluator 布尔输入全部已知;`gateStatus` 逐项给出 PASS/FAIL。若以后缺少输入,可通过 knownMask 显式输出 UNKNOWN。**这里观测的是实际 Gate 输入;如没有 prepared scene,其已有 false 标志导致 Gate FAIL,不表示调查过一个尚不存在的 scene。**
`firstFailure` 保存生产 evaluator 真正返回的首错;`failMask` 保存同一 input 的失败全集。`decisionConsistent` 比较首位失败与生产决定,`decisionMismatch` 累计不一致;任何非零值均需要保留现场,不能接受归因结果,也不会放宽生产 Gate。位0对应顺序1;enum 的 camera/viewport 数值顺序不能用作掩码顺序。
| 顺序 | 失败名 | FAIL 谓词 |
|---:|---|---|
| 1 | no-prepared-tree | `!hasPreparedTree` |
| 2 | retained-disabled | `!retainedEnabled` |
| 3 | cache-invalid | `!cacheValid` |
| 4 | revision-mismatch | `!revisionMatches` |
| 5 | content-mutation | `!contentRevisionMatches` |
| 6 | viewport-changed | `!viewportMatches` |
| 7 | camera-changed | `!cameraMatches` |
| 8 | update-parameters-changed | `!updateParametersMatch` |
| 9 | style-not-loaded | `!styleLoaded` |
| 10 | boundary-changed | `!boundaryMatches` |
| 11 | topology-changed | `!topologyMatches` |
| 12 | static-unstable | `!staticStable` |
| 13 | placement-or-fade | `hasPlacementTransition` |
| 14 | pending-changes | `hasPendingChanges` |
| 15 | non-particle-transition | `hasNonParticleTransition` |
| 16 | non-particle-repaint | `hasNonParticleRepaint` |
| 17 | fading-tiles | `hasFadingTiles` |
| 18 | no-particle-repaint | `!particleNeedsRepaint` |
| 19 | still-image-request | `stillImageRequest` |
| 20 | unsupported-mode | `!continuousMode` |
| 21 | debug-options | `!debugDisabled` |
| 22 | unsupported-continuous-layer | `unsupportedContinuousLayer` |
| 23 | unsupported-scene | `!supportedScene` |
| 24 | annotation-mutation | `annotationMutation` |
因此 CacheInvalid 同帧也会保留 #22、#23 的真假,不再通过后续Full快照推测此前Gate。
每帧有 lifecycle 内唯一 `sequence`、renderer `frame`、steady-clock 的 `updateTimeNs`;没有树时多个尝试可以共用一个尚未完成的 renderer frame,**关联主键是 lifecycle+sequence**。不把 updateTimeNs 当 UTC/forecast。外层原有fastPathHit/Miss/Full atomics沿用原快照语义,可能与正在写入的trace末帧相差一个调用阶段;逐帧归因以frame记录为准,不把外层字段拼成额外原子帧。
- `cacheInputs` 是实际 pre-fast cache exists/valid/buildRevision/staticRevision;最多16个 R,超限明确 `cacheInputsTruncated=true`,所有 aggregate Gate 仍完整。
- `pre` 保存判定阶段的缓存计数/失效/回退原因与上一已完成 RenderTree 诊断快照。`preFastPlacementTransition` 才是本次 Gate #13 的当前 Controller 值;`placementTransitionsEnabled` 来自本次 UpdateParameters。
- `post` 在渲染末尾取得同帧 R build/reuse/invalidation/fallback、sourceReady/stable、decision、actualExecution、Placement changed/transition/fade与Full计数。Full、Fast、无树分别有明确 outcome;中途异常或尚未结束时 `completed=false/post=null`。
- `placementBasis` 区分历史 RenderTree、当前 Full tree、当前 Fast tree(未重新计算Placement);没有完成过的诊断快照标 UNKNOWN。没有将 post Full 数据回填 pre Gate。
- 每phase最多16个R;`retainedSamplesTruncated` 明示超限。无同帧每Symbol输出签名、旧Placement options/duration/commitTime/fadeStartTime、CPU/GPU阶段计时,这些不在本次实现中伪造。
- Debug 的 `processSimulationAdvance` 复用现有进程级 atomic 计数;Release为null。它不是单Map的计数,也不等于Fast尝试或FPS。
120帧全部Gate每次尝试都记录;容量满时覆盖最早帧,`overwritten` 累计。生命周期 `gates[].failures/firstFailures` 保留聚合;`windowFailures`、窗口首末序号、窗口 Full/Fast/无树计数另行导出。默认30fps时120帧约4秒,真实渲染频率可能更高,不能承诺完整60秒逐帧档案。
环形记录在 render thread 更新;写入、完成、reset和外部copy沿用 `rangeMutex`。Android UI只读现有共享snapshot,不操作Renderer可变对象。内部render snapshot默认不复制trace;只有外部 `Map::getRenderSegmentDiagnostics()` 包含历史。reset发生于绑定新的 runtime state、观测到 styleLoaded true→false、context lost、Renderer.clearData;每次普通 Full miss清prepared tree**不会**清trace。sceneGeneration/configRevision另用于区分UI场景切换;同步加载没有暴露false帧时,不能假定trace epoch必然随UI场景切换。
新增CPU工作为O(24+R)、固定容量历史;没有额外GPU pass。每帧仍有小型vector/string副本与短mutex临界区,外部读取序列化120帧也有成本。**内存字节数和CPU成本没有实测,不声明低于某一百分比**;RDC需量测。没有诊断开关的新RN API;如需成本对照,应使用同环境基线源码制品与本次制品并记录差异。
## 4. A:Style、Layer 与场景等价度证据
| 项目 | 证据等级与当前结论 |
|---|---|
| 正式Style/Sprite/Glyph/VectorTile URI | **SOURCE-PROVEN**:NavSea `src/config/builtinRuntimeUrlConfig.ts` 与新场景常量相同 |
| NavSea当前分段选择器 | **SOURCE-PROVEN**:`RenderSegmentHost.tsx` 为R→C,C从 `rn-weather-color` 开始;不是现场四段计划 |
| 天气片段 ID/type/source | **SOURCE-PROVEN**:`particleStyle.ts` 调用shared fragment,追加到顶;shared `packages/maplibre-particle-rn/src/index.ts` 定义 `rn-weather-fields`、`rn-weather-color`、`rn-weather-particles` |
| 正式63层保序/显隐/完整定义 | **TEST-APP-STRUCTURAL**:运行时从正式URL加载,要求63层和实际锚点ID;完整原始定义逐层不变、顺序不变,并导出生产manifest与实际Native styleLayers。**尚未在设备执行该证明** |
| Sprite/Glyph/VectorTile访问成功 | **NOT-VERIFIED**:本机web open失败;urllib回读失败于域名无法解析,没有服务器JSON/SHA或瓦片访问成功证据 |
| 113层及[0,13)/[13,33)/[33,107)/[107,113) | 用户STEP-112历史现场输入;本轮没有同版本完整Layer/Source/visibility/业务数据快照,**NOT-VERIFIED,未完整复制** |
| 旧NavSea FZG采样 | 只读找到V15报告与129层采样;年代/版本/层数均不同,不用于补造当前113层 |
| 本轮真机/性能 | **DEVICE-MEASURED:无本轮结果** |
生产URI分别为:
- Style:`https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json`
- Sprite:`https://pbf.jpcn.ccwu.cc/sprite/sprite`
- Glyph:`https://pbf.jpcn.ccwu.cc/fonts/{fontstack}/{range}.pbf`
- VectorTile:`https://pbf.jpcn.ccwu.cc/pbf/pbf-delivery-full-20260418-rebuild/{z}/{x}/{y}.pbf`,source=`navsea_delivery`。
场景会检查真实回读的资源URI;变更或缺失则显示BLOCKED并导出原因,不切换假地图。`productionFingerprintSha256` 是SDK回读序列化JSON的UTF-8 SHA-256,**不是HTTP原始字节SHA**,且URL本身可变。导出styleVersion/name/metadata(styleVersion是Style规格版本,包版本未知时为null)、生产/增强fingerprint、basemanifest、请求plan、最终Native所有Layer的index/id/type/source/visibility,以及段边界与层名。Native可能追加Annotation等内部层,不能把JSON基础63层等同实际总层数。
| 场景 | 行为与等价限制 |
|---|---|
| PBF63 | 正式63层原定义;无测试天气片段;全RETAINED,实际总层数以Native为准 |
| STATIC_RC | 原63层+正式类型的天气/粒子片段(JSON65层);保序append;当前NavSea源码RC选择器;不等价NavSea全部动态业务层 |
| PARTICLE_OFF | 与STATIC_RC同样层序,粒子visibility=none;WeatherColor/forecast/camera保持对照 |
| PARTIAL_RCRR | 原63层+天气片段,片段放在实际 `clip-outline` 后;四段锚点为 `sea-area-fill`、`special-pattern-line-754`、**实际回读的最后生产层ID**;明示结构RCRR,与现场113层/数字边界不同 |
| SOURCE_CHANGE | STATIC_RC加一个独立GeoJSON/circle测试Source;显式菜单单次更新;结构探针,不冒充AIS/Radar/航线等生产数据 |
所有带天气场景使用既有ECMWF canonical两个时间片(2026080700/06),forecast请求 `1786060800000` ms,PAUSE,targetFps=30,粒子1024。与NavSea当前QBIN/预报业务输入不等价。命令接受结果逐项保存;实际Native预报时间没有新增查询API,`actualForecastTimeObservedMs=null`,不把Host请求值当Native独立回读结果。camera请求33.45/129.97、zoom10.5、bearing/pitch0;场景禁用手势,退出恢复原手势设置。静态轮询不改Source/Symbol/forecast/camera。
Native准入限制保持原样;RCRR结构模式预期可以暴露不支持的C层和Symbol R阻塞,但这只是源码预期,不是已经在FZG测到的结果。
## 5. C:UI、导出及精确RDC命令
入口:Test App `STEP 093 → STEP-113 FZG1-PBF GATE`。五个场景、`EXPORT LAST SNAPSHOT`、`MUTATE PROBE SOURCE ONCE`、退出。加载中或失败时也保留低频诊断读取,sceneReady=false/blockedReason明确标注,不能把可能仍在加载的旧Native层表当已完成场景。每3秒只有一次Native诊断查询;UI首错来自trace末帧,失败排名来自同copy聚合,各段显示范围/state/build/reuse/invalidation。仅STEP-113期间增大诊断区域,退出恢复。
每30秒一条逻辑summary(通常少量物理Logcat分块);不是每33ms输出JSON。完整120帧和Layer表仅手动导出最近一次copy,写App私有 `files/step113-<SCENARIO>-<elapsed>.json` 并使用标签 **STEP113Gate** 输出Base64分块。每块payload≤2400 ASCII字符,携带schemaVersion/kind/exportId/chunkIndex/chunkCount/encoding;按exportId和chunkIndex完整拼接后Base64解码,缺块不能当完整证据。手动完整导出会产生较多块,优先直接回读私有JSON文件。
以下仅RDC执行,本轮Codex没有执行。先在macOS普通Terminal核对设备主ABI与标准脚本的x86_64一致;不同则停止本构建流程并记录,不自行改脚本/环境:
```bash
cd /Users/tei/dev/MapLibreParticleLab
step113_abi="$(adb -s 192.168.200.139:5555 shell getprop ro.product.cpu.abi | tr -d '\r')"
step113_abilist="$(adb -s 192.168.200.139:5555 shell getprop ro.product.cpu.abilist | tr -d '\r')"
printf 'FZG1-2 primary ABI=%s; ABI list=%s\n' "$step113_abi" "$step113_abilist"
test "$step113_abi" = x86_64 || { echo 'ABI不一致,停止'; return 1 2>/dev/null || exit 1; }
./tools/particle_android_validation/build_validation_apk.sh
```
标准产物:`artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk`、`build.log`、`build-info.txt`;脚本记录commit/branch/dirty/SHA/体积并自验ZIP与Native ABI,失败不得复用旧APK。本轮源码未提交,build-info里的dirty必须真实,另保留任务文件diff/hash以证明制品来自本次修改。
安装前RDC核对现存同包签名,签名不匹配不得自动卸载用户数据;保留INSTALL_FAILED错误。安装与静态场景启动:
```bash
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 step113_scenario STATIC_RC
adb -s 192.168.200.139:5555 logcat -v raw -s STEP113Gate:I '*:S' > artifacts/maplibre-particle/android-validation/step113-static-rc.log
```
最后一条在RDC普通前台Terminal采集,窗口完成后Ctrl-C。其他场景各用一条明确启动命令,并独立保留日志:
```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 step113_scenario PBF63
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step113_scenario PARTICLE_OFF
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step113_scenario PARTIAL_RCRR
adb -s 192.168.200.139:5555 shell am start -S -n org.maplibre.android.testapp/org.maplibre.android.testapp.activity.particle.ParticleCoreLayerValidationActivity --es step113_scenario SOURCE_CHANGE
```
不要把这些命令连续执行当场景测试;每个场景至少预热10秒、观察60秒、手动导出窗口,再切下一场景。SOURCE_CHANGE先导出baseline,单击手动探针变化,等一次3秒刷新后再次导出;不添加周期Symbol更新。
点击 `EXPORT LAST SNAPSHOT` 后回读最近文件(run-as仅适用于该Debug App):
```bash
step113_file="$(adb -s 192.168.200.139:5555 shell run-as org.maplibre.android.testapp ls -t files | tr -d '\r' | rg '^step113-.*[.]json$' | head -n 1)"
test -n "$step113_file" || { echo '未找到手动导出文件'; return 1 2>/dev/null || exit 1; }
adb -s 192.168.200.139:5555 exec-out run-as org.maplibre.android.testapp cat "files/$step113_file" > "artifacts/maplibre-particle/android-validation/$step113_file"
shasum -a 256 "artifacts/maplibre-particle/android-validation/$step113_file"
adb -s 192.168.200.139:5555 exec-out screencap -p > artifacts/maplibre-particle/android-validation/step113-screen.png
adb -s 192.168.200.139:5555 logcat -b crash -d > artifacts/maplibre-particle/android-validation/step113-crash.log
```
如果只保留Logcat,使用下面的离线重组命令;每个导出必须收齐chunkCount,否则直接报错:
```bash
python3 - <<'PY'
import base64, json
from pathlib import Path
root = Path('artifacts/maplibre-particle/android-validation')
groups = {}
for line in (root / 'step113-static-rc.log').read_text().splitlines():
try:
c = json.loads(line)
except json.JSONDecodeError:
continue
if c.get('encoding') != 'base64-utf8':
continue
key = (c['exportId'], c['kind'])
count, parts = groups.setdefault(key, (c['chunkCount'], {}))
assert count == c['chunkCount'], key
parts[c['chunkIndex']] = c['payload']
for (export_id, kind), (count, parts) in groups.items():
assert set(parts) == set(range(count)), ('缺块', export_id)
raw = base64.b64decode(''.join(parts[i] for i in range(count)), validate=True)
json.loads(raw)
(root / f'step113-{export_id}-{kind}.json').write_bytes(raw)
PY
```
RDC的macOS Native定向检查可按当前preset执行(历史缺libuv阻塞不代表当前环境仍相同;若失败保留首错,不扩大改依赖):
```bash
cmake --preset macos-metal
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
build-macos-metal/mbgl-test-runner --gtest_filter='FastGateTrace.*:ParticleFastPath.*:RenderSegmentDiagnostics.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*' --gtest_color=no
```
配置失败就停止该链;不运行旧runner冒充新源码通过。Metal host测试不能替代Android OpenGL/实体机/iOS验收。
## 6. 已执行静态检查与未做项
已执行:只读Git/源码/文档调查、diff范围检查、`git diff --check`(退出0);新增文件另用no-index whitespace检查。静态文本核对24条谓词及极性/顺序一致;生产 evaluator 与HEAD逐字相同;STEP-102 fixture与500ms poll逐字未变。最终hash与append-only核对见第7节。
4个测试源码覆盖:全部24独立失败位/生产首错顺序;CacheInvalid与#22/#23同帧并存、137次记录后120条有序窗口与reset;pending/null post/错误shadow显式计数和完成幂等;18个R截断及pre/post独立缓存计数。**没有运行这些测试,没有C++/Kotlin编译或语法编译PASS结论。**
本轮无APK/AAR、体积或SHA,无设备运行日志、截图、FPS、CPU、trace开销或性能改善数字。没有已观察到的构建/测试失败;未执行不表示会通过。
剩余RDC事项:Native/Kotlin编译和上述定向测试;设备ABI/制品SHA/版本证据;正式资源加载、原63层manifest/实际层序;plan接受与最终valid原因;所有24状态与首错一致性;cache-invalid同帧后续失败;Full/post和pre分离、Source变更前后、粒子关闭对照;reset/退出/恢复与旧场景回归;开销/CPU和截图。**APK成功仅是构建证据。**
113层完整等价仍需同版本真实Layer/Source/业务数据/显隐/选择器快照后才能扩展;本任务交付的部分模式不为生产Full循环提供完整因果或性能验收结论。
## 7. 收尾证据
- STEP-110任务书SHA-256:`33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`。
- STEP-111任务书SHA-256:`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`。
- STEP-113任务书原样SHA-256:`4e3fef391dc3addef2702b3d588c1fcc8f79b898b5d9004bfc1410070533e0b4`。
- HEAD保持起点,暂存区为空;仅任务源码/测试、两个新交付文档和开发记录追加,既有三任务书未暂存。没有远端HEAD回读、commit/push或制品发布行为。
- 开发记录的HEAD原始160442字节前缀完全保留,原SHA-256=`070aaeaf85aa8fdbe8b15a899f74fd492a0e3eb2b6442b37cb3d5ee9724dbd24`;本次仅末尾追加1120字节(7行)。静态检查是范围与一致性证据,不是全任务PASS。
## RDC 独立验收补记(2026-10-11,设备证据)
以下是 Codex 退出后 RDC 新执行的证据,不修改上文 Codex 未构建/未实测的历史结论。本次阶段判定:**PARTIAL;Android 构建 PASS;五场景稳定基线采样 PASS;113 层等价与性能 NOT VERIFIED**。
- 环境:Mac、particle/main;构建源码基于 c6d13e1059d464d312ac33acac87967300fa5dc4 加 STEP-113 未提交改动,build-info 明确 git_dirty=true。此 APK 不是 Source Lock / 正式发行。
- 标准 build_validation_apk.sh 于 2026-10-11 08:35:08 JST 退出码 0;Gradle BUILD SUCCESSFUL in 19m 22s;Java 17.0.20.1;OpenGL/x86_64;ZIP/ABI/体积/SHA 自检通过。
- APK:artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk;66,587,454 bytes;libmaplibre.so 40,033,976 bytes;SHA-256 0054248bc3872e8670df4e9a10d11f1d65c0912a0482586cd7a247e880a6eaef。记录:同目录 build-info.txt、build.log。
- 设备:FZG1-2,ADB 192.168.200.139:5555,主 ABI x86_64;明确 -s 的 adb install -r 返回 Success,未卸载数据;设备安装后版本 13.6.1-particle.23。正式 PBF Style 模式运行并加载。启动前数秒 sceneReady=true 但 Native Layer=0 / plan not-configured 是加载暂态;只接受之后的 Native Layers 非零、planValid=true 稳定记录。
- 五个场景均由 ADB 明确 serial 启动;STEP113Gate Logcat Base64 summary 成功还原。每场景实际设备稳定状态的 decisionMismatch 都是 **0**。Full/Fast/Attempts 为生命周期累计,并非精确 60 秒窗口。
| 场景 | Native Layers | Attempts | Full | Fast | Retained 及关键证据 |
|---|---:|---:|---:|---:|---|
| PBF63 | 64(63+内部层) | 106 | 106 | 0 | R[0,64) Build0,unknown-dependency;无粒子持续驱动 |
| STATIC_RC | 66 | 876 | 875 | 0 | R[0,63) Build0,unknown-dependency;unsupported-continuous-layer、unsupported-scene 各876 |
| PARTICLE_OFF | 66 | 70 | 70 | 0 | no-particle-repaint=70;非相等时长性能对照 |
| PARTIAL_RCRR | 66 | 862 | 861 | 0 | R0[0,3) unknown-dependency;R2[39,64) 与 R3[64,66) Build0/static-placement;两个 unsupported Gate 各862 |
| SOURCE_CHANGE | 67 | 888 | 888 | 0 | 基线已记录;**没有执行菜单手动 Source mutation** |
- STATIC_RC 同帧失败全集汇总 CacheInvalid=870、revision-mismatch=871、placement-or-fade=780;PARTIAL_RCRR CacheInvalid=856。这些失败条件可能在**同一帧**并存,不能累加为帧数。即使修复缓存 Build,也还有 #22/#23 与其它独立 Gate。
- 设备摘要 JSON(不纳入 Git):artifacts/maplibre-particle/android-validation/step113/ 下的 pbf63-summary-ready.json、static-rc-summary.json、particle-off-summary.json、partial-rcrr-summary.json、source-change-baseline-summary.json。它们只是低频 summary,不是导出的完整 120 帧。
- 仍 NOT VERIFIED:原 NavSea 113 层完整 Layer/Source/显隐及真实四段边界;实际 PBF/Sprite/Glyph 画面截图;Source 单次变更后前后对照;120帧手动详情导出;C++ GTest 运行;reset/后台、旧 STEP-102 场景回归;CPU/GPU/诊断开销;生产性能提升、正式发行。不得把部分结构等价当成现场复测或将 STEP-113 判为 CLOSED/PASS。
- 下一阶段必须分别处理 retained first-build 的安全准入、unsupported continuous layer、unsupported scene;不能直接删除 Gate 放行。

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
#include <set> #include <set>
@@ -237,6 +238,72 @@ struct RenderSegmentDiagnosticsEntry {
std::optional<RenderSegmentCacheDiagnostics> cache; std::optional<RenderSegmentCacheDiagnostics> cache;
}; };
// Platform-neutral, bounded diagnostic samples. No pointers to mutable renderer objects.
struct FastGateRetainedSample {
uint32_t segmentIndex = 0;
RenderSegmentCacheDiagnostics cache;
bool placementGatePassed = false;
std::string actualExecution;
};
struct FastGateCacheInputSample {
uint32_t segmentIndex = 0;
bool exists = false;
bool valid = false;
uint64_t buildRevision = 0;
uint64_t staticRevision = 0;
};
struct FastGatePhaseSample {
// Placement changed/fade come from the last completed render-tree snapshot
// in pre-fast. Fast tree flags do not imply a fresh Placement calculation.
// They are never substituted for the current controller gate #13.
bool hasPlacementSnapshot = false;
bool placementChanged = false;
float symbolFadeChange = 0;
bool placementTransitionActive = false;
bool retainedSamplesTruncated = false;
uint64_t contentRevision = 0;
uint64_t topologyRevision = 0;
uint64_t configRevision = 0;
uint64_t fullCpuFrame = 0;
std::optional<uint64_t> processSimulationAdvance;
std::vector<FastGateRetainedSample> retained;
};
struct FastGateFrameSample {
uint64_t sequence = 0;
uint64_t frame = 0;
int64_t updateTimeNs = 0;
uint32_t failMask = 0;
uint32_t knownMask = 0x00ffffff;
std::string firstFailure;
bool decisionConsistent = true;
bool candidate = false;
bool preFastPlacementTransition = false;
bool placementTransitionsEnabled = true;
bool completed = false;
std::string outcome{"pending"};
bool cacheInputsTruncated = false;
std::vector<FastGateCacheInputSample> cacheInputs;
FastGatePhaseSample pre;
FastGatePhaseSample post;
};
struct FastGateTraceDiagnostics {
uint32_t schemaVersion = 1;
uint64_t lifecycle = 0;
uint64_t attempts = 0;
uint64_t overwritten = 0;
uint64_t decisionMismatch = 0;
uint64_t fullRendered = 0;
uint64_t fastRendered = 0;
uint64_t noRender = 0;
std::array<uint64_t, 24> failureCounts{};
std::array<uint64_t, 24> firstFailureCounts{};
std::vector<FastGateFrameSample> frames;
};
struct RenderSegmentDiagnostics { struct RenderSegmentDiagnostics {
uint32_t schemaVersion = 1; uint32_t schemaVersion = 1;
bool enabled = true; bool enabled = true;
@@ -274,6 +341,7 @@ struct RenderSegmentDiagnostics {
RenderMutationDiagnostics lastMutation; RenderMutationDiagnostics lastMutation;
std::vector<StyleLayerDiagnostics> styleLayers; std::vector<StyleLayerDiagnostics> styleLayers;
std::vector<RenderSegmentDiagnosticsEntry> segments; std::vector<RenderSegmentDiagnosticsEntry> segments;
std::optional<FastGateTraceDiagnostics> fastGateTrace;
}; };
struct ResolvedRenderSegment { struct ResolvedRenderSegment {

View File

@@ -63,6 +63,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var step097Topology: Step097StencilFixture.Topology? = null private var step097Topology: Step097StencilFixture.Topology? = null
private var step099Scenario: Step099ReadinessFixture.Scenario? = null private var step099Scenario: Step099ReadinessFixture.Scenario? = null
private var step101Scenario: String? = null private var step101Scenario: String? = null
private var step113DiagnosticsHeight: Int? = null
private var step113DiagnosticsMaxLines = 6
private var step113Scene: Step113PbfGateScene? = null
private var step113Scenario: Step113PbfGateScene.Scenario? = null
private var step102Scenario: Step102PlacementFixture.Scenario? = null private var step102Scenario: Step102PlacementFixture.Scenario? = null
private var step102Tick = 0L private var step102Tick = 0L
private val step102Handler = Handler(Looper.getMainLooper()) private val step102Handler = Handler(Looper.getMainLooper())
@@ -347,6 +351,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
step093Variant = savedInstanceState?.getString(STATE_STEP093_VARIANT) step093Variant = savedInstanceState?.getString(STATE_STEP093_VARIANT)
?.let { value -> Step093Variant.values().firstOrNull { it.name == value } } ?.let { value -> Step093Variant.values().firstOrNull { it.name == value } }
?: Step093Variant.DEFAULT ?: Step093Variant.DEFAULT
val savedStep113 = if (savedInstanceState?.containsKey("step113_scenario") == true) {
savedInstanceState.getString("step113_scenario")
} else intent.getStringExtra("step113_scenario")
step113Scenario = savedStep113
?.let { value -> Step113PbfGateScene.Scenario.values().firstOrNull { it.name == value } }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.i( Log.i(
TAG, TAG,
@@ -359,6 +368,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
status = findViewById(R.id.validationStatus) status = findViewById(R.id.validationStatus)
diagnostics = findViewById(R.id.validationDiagnostics) diagnostics = findViewById(R.id.validationDiagnostics)
mapView.addOnDidFailLoadingMapListener { errorMessage -> mapView.addOnDidFailLoadingMapListener { errorMessage ->
step113Scene?.onStyleLoadFailed(errorMessage)
if (navSeaPbfMode || purePbfMode) { if (navSeaPbfMode || purePbfMode) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage") Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage")
} else if (realWeatherMode) { } else if (realWeatherMode) {
@@ -379,7 +389,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
updateNavSeaPbfButtonState() updateNavSeaPbfButtonState()
updatePbfModeButtonState() updatePbfModeButtonState()
updateRetainedCacheButtonState() updateRetainedCacheButtonState()
if (step102Scenario != null) loadStep102Style() if (step113Scenario != null) startStep113Scene(step113Scenario!!)
else if (step102Scenario != null) loadStep102Style()
else if (step101Scenario != null) loadStep101Style() else if (step101Scenario != null) loadStep101Style()
else if (step097Topology != null) loadStep097StencilStyle() else if (step097Topology != null) loadStep097StencilStyle()
else if (step093Mode) loadStep093Style(configureBeforeStyle = false) else if (step093Mode) loadStep093Style(configureBeforeStyle = false)
@@ -688,6 +699,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
menu.add(0, 17, 17, "STEP-099 READINESS") menu.add(0, 17, 17, "STEP-099 READINESS")
menu.add(0, 18, 18, "STEP-101 TEARDOWN") menu.add(0, 18, 18, "STEP-101 TEARDOWN")
menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY") menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY")
menu.add(0, 20, 20, "STEP-113 FZG1-PBF GATE")
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId]) in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
@@ -701,6 +713,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
17 -> showStep099Menu(anchor) 17 -> showStep099Menu(anchor)
18 -> showStep101Menu(anchor) 18 -> showStep101Menu(anchor)
19 -> showStep102Menu(anchor) 19 -> showStep102Menu(anchor)
20 -> showStep113Menu(anchor)
} }
true true
} }
@@ -761,6 +774,68 @@ 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, 10, 10, "EXPORT LAST SNAPSHOT")
menu.add(0, 11, 11, "MUTATE PROBE SOURCE ONCE")
menu.add(0, 12, 12, "EXIT STEP-113")
setOnMenuItemClickListener { item ->
when (item.itemId) {
in Step113PbfGateScene.Scenario.values().indices ->
startStep113Scene(Step113PbfGateScene.Scenario.values()[item.itemId])
10 -> step113Scene?.export()
11 -> step113Scene?.mutateSource()
12 -> { stopStep113Scene(); loadValidationStyle(active, enabled, "STEP113_EXIT") }
}
true
}
show()
}
}
private fun startStep113Scene(scenario: Step113PbfGateScene.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 // Cancel previous asynchronous style callbacks.
step113Scenario = scenario
retainedCacheEnabled = true
updateRetainedCacheButtonState()
step113DiagnosticsHeight = diagnostics.layoutParams.height
step113DiagnosticsMaxLines = diagnostics.maxLines
diagnostics.maxLines = 10
diagnostics.layoutParams = diagnostics.layoutParams.apply { height = (156 * resources.displayMetrics.density).toInt() }
status.text = "STEP-113 FZG1-PBF Gate · ${scenario.name} · PARTIAL"
step113Scene = Step113PbfGateScene(this, maplibreMap) { diagnostics.text = it }
.also { it.start(scenario) }
}
private fun stopStep113Scene() {
step113Scene?.stop()
step113DiagnosticsHeight?.let { originalHeight ->
diagnostics.maxLines = step113DiagnosticsMaxLines
diagnostics.layoutParams = diagnostics.layoutParams.apply { height = originalHeight }
}
step113DiagnosticsHeight = null
step113Scene = null
step113Scenario = null
}
private fun showStep102Menu(anchor: android.view.View) { private fun showStep102Menu(anchor: android.view.View) {
PopupMenu(this, anchor).apply { PopupMenu(this, anchor).apply {
Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario -> Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario ->
@@ -778,6 +853,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadStep102Style() { private fun loadStep102Style() {
stopStep113Scene()
val scenario = step102Scenario ?: return val scenario = step102Scenario ?: return
stopStep093DiagnosticsPolling() stopStep093DiagnosticsPolling()
step093Mode = false step093Mode = false
@@ -810,6 +886,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadStep101Style() { private fun loadStep101Style() {
stopStep113Scene()
val scenario = step101Scenario ?: return val scenario = step101Scenario ?: return
stopStep102Scenario() stopStep102Scenario()
stopStep099Scenario() stopStep099Scenario()
@@ -836,6 +913,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) { private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) {
stopStep113Scene()
stopStep102Scenario() stopStep102Scenario()
stopStep093DiagnosticsPolling() stopStep093DiagnosticsPolling()
step093Mode = false step093Mode = false
@@ -971,6 +1049,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadStep097StencilStyle() { private fun loadStep097StencilStyle() {
stopStep113Scene()
val topology = step097Topology ?: return val topology = step097Topology ?: return
styleReady = false styleReady = false
val epoch = ++styleLoadEpoch val epoch = ++styleLoadEpoch
@@ -1124,6 +1203,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadStep093Style(configureBeforeStyle: Boolean) { private fun loadStep093Style(configureBeforeStyle: Boolean) {
stopStep113Scene()
weatherRuntime?.dispose() weatherRuntime?.dispose()
weatherRuntime = null weatherRuntime = null
styleReady = false styleReady = false
@@ -1621,6 +1701,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") { private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
stopStep113Scene()
stopStep102Scenario() stopStep102Scenario()
if (step099Scenario != null) stopStep099Scenario() if (step099Scenario != null) stopStep099Scenario()
step097V4GoldenReady = false step097V4GoldenReady = false
@@ -1692,6 +1773,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
) )
private fun loadPureParticleStyle(active: Boolean, enabled: Boolean, reason: String) { private fun loadPureParticleStyle(active: Boolean, enabled: Boolean, reason: String) {
stopStep113Scene()
weatherRuntime?.dispose() weatherRuntime?.dispose()
weatherRuntime = null weatherRuntime = null
this.active = active this.active = active
@@ -1721,6 +1803,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadPurePbfStyle(reason: String) { private fun loadPurePbfStyle(reason: String) {
stopStep113Scene()
weatherRuntime?.dispose() weatherRuntime?.dispose()
weatherRuntime = null weatherRuntime = null
styleReady = false styleReady = false
@@ -1752,6 +1835,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
private fun loadNavSeaPbfStyle(active: Boolean, enabled: Boolean, reason: String) { private fun loadNavSeaPbfStyle(active: Boolean, enabled: Boolean, reason: String) {
stopStep113Scene()
weatherRuntime?.dispose() weatherRuntime?.dispose()
weatherRuntime = null weatherRuntime = null
this.active = active this.active = active
@@ -2397,8 +2481,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} }
} else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() } else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling()
} }
override fun onResume() { super.onResume(); mapView.onResume() } override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume() }
override fun onPause() { mapView.onPause(); super.onPause() } override fun onPause() { step113Scene?.pause(); mapView.onPause(); super.onPause() }
override fun onStop() { override fun onStop() {
step101Handler.removeCallbacksAndMessages(null) step101Handler.removeCallbacksAndMessages(null)
step093Handler.removeCallbacks(step093DiagnosticsPoll) step093Handler.removeCallbacks(step093DiagnosticsPoll)
@@ -2408,6 +2492,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
super.onStop() super.onStop()
} }
override fun onDestroy() { override fun onDestroy() {
stopStep113Scene()
step102Handler.removeCallbacksAndMessages(null) step102Handler.removeCallbacksAndMessages(null)
step101Handler.removeCallbacksAndMessages(null) step101Handler.removeCallbacksAndMessages(null)
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_ENTER scenario=$step101Scenario completed=$step101Completed") if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_ENTER scenario=$step101Scenario completed=$step101Completed")
@@ -2422,6 +2507,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() } override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putString("step113_scenario", step113Scenario?.name)
outState.putBoolean(STATE_ACTIVE, active) outState.putBoolean(STATE_ACTIVE, active)
outState.putBoolean(STATE_ENABLED, enabled) outState.putBoolean(STATE_ENABLED, enabled)
outState.putInt(STATE_MASK_MODE, maskMode) outState.putInt(STATE_MASK_MODE, maskMode)

View File

@@ -0,0 +1,394 @@
package org.maplibre.android.testapp.activity.particle
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Base64
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
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.sources.GeoJsonSource
import org.maplibre.android.testapp.BuildConfig
import org.maplibre.geojson.Feature
import org.maplibre.geojson.Point
import java.io.File
import java.security.MessageDigest
/** STEP-113: real remote chart, explicit partial parity, fixed forecast and low-frequency pull. */
internal class Step113PbfGateScene(
private val context: Context,
private val map: MapLibreMap,
private val display: (String) -> Unit
) {
enum class Scenario { PBF63, STATIC_RC, PARTICLE_OFF, PARTIAL_RCRR, SOURCE_CHANGE }
private val handler = Handler(Looper.getMainLooper())
private var generation = 0
private var running = false
private var paused = false
private var ready = false
private var failureReason: String? = null
private var runtime: WeatherRuntime? = null
private var scenario = Scenario.PBF63
private var parity = JSONObject()
private var latest: JSONObject? = null
private var lastLogMs = 0L
private var mutation = 0
private var exportSequence = 0L
private var commandResults = JSONObject()
private var savedGestures: List<Boolean>? = null
private val poll = object : Runnable {
override fun run() {
if (!running || paused) return
if (running) {
try {
// One shared snapshot query per refresh; all UI/log/export uses this copy.
val native = JSONObject(map.getRenderSegmentDiagnostics())
latest = envelope(native)
val trace = native.optJSONObject("fastGateTrace")
val frames = trace?.optJSONArray("frames")
val lastFrame = if (frames != null && frames.length() > 0) frames.optJSONObject(frames.length() - 1) else null
val firstFailure = lastFrame?.optString("firstFailure") ?: native.optString("fastPathFallbackReason")
val gates = trace?.optJSONArray("gates") ?: JSONArray()
val rank = (0 until gates.length()).map { gates.getJSONObject(it) }
.sortedByDescending { it.optLong("failures") }.take(4)
val segments = native.optJSONArray("segments") ?: JSONArray()
display(buildString {
append("STEP-113 $scenario · ${if (ready) "READY/PARTIAL" else failureReason ?: "LOADING"} · actual=${native.optInt("styleLayerCount")}\n")
append("first=$firstFailure seq=${lastFrame?.optLong("sequence")} full=${trace?.optLong("fullRendered")} ")
append("window=${trace?.optInt("windowCount")} mismatch=${trace?.optLong("decisionMismatch")}\n")
append(rank.joinToString(" / ") { "${it.optString("reason")}=${it.optLong("failures")}" })
for (i in 0 until segments.length()) {
val s = segments.getJSONObject(i)
val c = s.optJSONObject("cache")
append("\n${s.optString("strategy")}[${s.optInt("beginIndex")},${s.optInt("endExclusive")}) ")
append("${c?.optString("cacheState") ?: "live"} B=${c?.optLong("build") ?: 0} ")
append("R=${c?.optLong("reuse") ?: 0} I=${c?.optLong("invalidation") ?: 0}")
}
})
val now = SystemClock.elapsedRealtime()
if (now - lastLogMs >= 30000L) {
lastLogMs = now
logRecord("summary", summary(native))
}
} catch (error: Exception) {
display("STEP-113 诊断读取失败: ${error.message}")
Log.e(TAG, "diagnostic-read-failed", error)
}
}
handler.postDelayed(this, 3000L)
}
}
fun start(next: Scenario) {
stop()
running = true
scenario = next
ready = false
failureReason = null
latest = null
parity = JSONObject()
lastLogMs = 0L
mutation = 0
commandResults = JSONObject()
val epoch = ++generation
display("STEP-113 $scenario · 加载正式 PBF · 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)
if (!paused) handler.post(poll)
map.setStyle(Style.Builder().fromUri(STYLE_URL)) production@ { loaded ->
if (!running || epoch != generation) return@production
try {
val root = JSONObject(loaded.json)
val baseLayers = root.getJSONArray("layers")
val base = JSONArray(baseLayers.toString())
check(base.length() == 63) { "正式基础层数已变化: ${base.length()},保留现场" }
val ids = (0 until base.length()).map { base.getJSONObject(it).getString("id") }
check(ids.distinct().size == ids.size && "clip-outline" in ids && "sea-area-fill" in ids &&
"special-pattern-line-754" in ids) { "正式 Style 边界 ID 缺失或重复" }
val source = root.getJSONObject("sources").getJSONObject("navsea_delivery")
check(root.optString("sprite") == SPRITE && root.optString("glyphs") == GLYPHS &&
source.optString("type") == "vector" && source.optJSONArray("tiles")?.optString(0) == TILES) {
"正式 Sprite/Glyph/VectorTile URI 与 NavSea URL 事实源不一致"
}
parity = JSONObject().put("evidence", "TEST-APP-STRUCTURAL")
.put("styleUrl", STYLE_URL).put("styleVersion", root.optInt("version"))
.put("styleVersionBasis", "Style specification version; 包版本未知")
.put("stylePackageVersionObserved", JSONObject.NULL)
.put("styleMetadata", root.opt("metadata") ?: JSONObject.NULL)
.put("styleName", root.optString("name"))
.put("fingerprintBasis", "SDK-returned-production-json-utf8")
.put("productionFingerprintSha256", sha256(loaded.json))
.put("baseLayerCount", base.length()).put("baseLayersPreserved", true)
.put("full113Equivalent", false).put("missingProductionDynamicLayerDefinitions", true)
.put("dynamicParity", "测试场结构模式;缺少同版本113层完整Source/Layer/visibility/数据快照")
.put("navSeaSourceHead", "ad9aa043fde37f87462f31de3fbd8ce008f8424b")
.put("navSeaSourceSelector", "RenderSegmentHost.tsx:RC@rn-weather-color")
.put("deviceReferenceSelector", "用户STEP-112:RCRR_PREFIX[0,13)/[13,33)/[33,107)/[107,113)")
.put("baseLayerManifest", layerManifest(base)).put("sprite", root.optString("sprite"))
.put("glyphs", root.optString("glyphs")).put("vectorSource", JSONObject(source.toString()))
val hasField = scenario != Scenario.PBF63
if (hasField) {
val sources = root.getJSONObject("sources")
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
for (i in 0 until base.length()) {
layers.put(base.getJSONObject(i))
if (prefix && base.getJSONObject(i).getString("id") == "clip-outline") addFields(layers)
}
if (!prefix) addFields(layers) // NavSea current source fragment appends to top.
if (scenario == Scenario.SOURCE_CHANGE) {
sources.put(PROBE_SOURCE, JSONObject().put("type", "geojson")
.put("data", JSONObject().put("type", "FeatureCollection").put("features", JSONArray())))
layers.put(JSONObject().put("id", PROBE_LAYER).put("type", "circle")
.put("source", PROBE_SOURCE).put("paint", JSONObject()
.put("circle-radius", 6).put("circle-color", "#ff8800")))
}
root.put("layers", layers)
}
// Prove every original definition, including visibility, survives unchanged and in order.
val finalLayers = root.getJSONArray("layers")
val retainedBase = (0 until finalLayers.length()).map { finalLayers.getJSONObject(it) }
.filter { it.getString("id") in ids }
check(retainedBase.size == base.length() && retainedBase.map { it.getString("id") } == ids &&
retainedBase.indices.all { retainedBase[it].toString() == base.getJSONObject(it).toString() }) {
"基础图层等价检查失败"
}
parity.put("testStyleFingerprintSha256", sha256(root.toString()))
.put("testJsonLayerCount", finalLayers.length())
.put("baseParityBasis", "同次SDK生产Style回读;完整定义逐层比较,不是设备画面证明")
val plan = planJson(scenario, ids.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")
.put("requestedPlan", JSONObject(plan))
commandResults.put("planAccepted", map.setRenderSegmentPlan(plan))
map.setStyle(Style.Builder().fromJson(root.toString())) testStyle@ {
if (!running || epoch != generation) return@testStyle
try {
map.cameraPosition = CameraPosition.Builder().target(LatLng(33.45, 129.97))
.zoom(10.5).bearing(0.0).tilt(0.0).build()
if (hasField) initializeFields()
ready = true
lastLogMs = 0L
handler.removeCallbacksAndMessages(null)
if (!paused) handler.post(poll)
} catch (error: Exception) { fail(error) }
}
} catch (error: Exception) { fail(error) }
}
}
private fun addFields(layers: JSONArray) {
layers.put(JSONObject().put("id", COLOR).put("type", "weather-color").put("source", SOURCE)
.put("paint", JSONObject().put("weather-color-opacity", 0.62)))
layers.put(JSONObject().put("id", PARTICLE).put("type", "raster-particle").put("source", SOURCE)
.put("layout", JSONObject().put("visibility", if (scenario == Scenario.PARTICLE_OFF) "none" else "visible"))
.put("paint", JSONObject().put("raster-particle-active", true).put("raster-particle-particle-count", 1024)))
}
private fun initializeFields() {
val host = WeatherRuntime(map, WeatherRuntime.Config(SOURCE, COLOR, PARTICLE))
runtime = host
// Existing real-weather canonical assets. Explicitly not the current NavSea forecast/QBIN feed.
val results = 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)
results.put(result.name)
check(result.accepted()) { "Field提交拒绝: $cycle $result" }
}
commandResults.put("fieldSubmit", results)
.put("forecastSeek", host.setForecastTime(FORECAST_MS).name)
.put("forecastPause", host.setPlaybackState(WeatherRuntime.PlaybackState.PAUSE).name)
.put("particleVisibility", host.setParticleVisible(scenario != Scenario.PARTICLE_OFF).name)
.put("targetFps", host.setTargetFps(WeatherRuntime.TargetFps.FPS_30).name)
check(host.state == WeatherRuntime.State.READY &&
commandResults.getString("forecastSeek") == "ACCEPTED" &&
commandResults.getString("forecastPause") == "ACCEPTED" &&
commandResults.getString("particleVisibility") == "ACCEPTED" &&
commandResults.getString("targetFps") == "ACCEPTED") { "固定预报/30fps命令未全部接受: $commandResults" }
}
fun mutateSource() {
if (!running || !ready || scenario != Scenario.SOURCE_CHANGE) return
val source = map.style?.getSourceAs<GeoJsonSource>(PROBE_SOURCE) ?: return
source.setGeoJson(Feature.fromGeometry(Point.fromLngLat(129.97 + (++mutation % 2) * 0.01, 33.45)))
// Single explicit mutation; polling never changes Source, Symbol, forecast or camera.
commandResults.put("sourceMutationCount", mutation)
logRecord("source-command", JSONObject().put("schemaVersion", 1).put("scenario", scenario.name)
.put("mutation", mutation).put("source", PROBE_SOURCE).put("evidence", "TEST-APP-STRUCTURAL"))
}
private fun envelope(native: JSONObject): JSONObject = JSONObject().put("schema", "step113-pbf-fast-gate")
.put("schemaVersion", 1).put("appVersion", BuildConfig.VERSION_NAME)
.put("scenario", scenario.name).put("sceneGeneration", generation)
.put("sceneReady", ready).put("blockedReason", failureReason ?: JSONObject.NULL)
.put("sampleElapsedMs", SystemClock.elapsedRealtime()).put("styleParity", parity)
.put("cameraRequested", JSONObject().put("latitude", 33.45).put("longitude", 129.97)
.put("zoom", 10.5).put("bearing", 0.0).put("pitch", 0.0).put("gesturesEnabled", false))
.put("commands", JSONObject(commandResults.toString()))
.put("forecastRequestedMs", if (runtime != null) FORECAST_MS else JSONObject.NULL)
.put("actualForecastTimeObservedMs", JSONObject.NULL)
.put("forecastStatus", runtime?.playbackState?.name ?: "NO_FIELD")
.put("fieldParity", "既有ECMWF canonical双帧测试数据;非NavSea当前业务预报")
.put("particleTick", JSONObject().put("targetFps", runtime?.targetFps?.name ?: "NONE")
.put("state", runtime?.state?.name ?: "NO_FIELD").put("particleVisible", scenario != Scenario.PARTICLE_OFF && runtime != null)
.put("simulationCounterScope", "process-debug-counter; 非本Map独占计数")
.put("processSimulationAdvance", lastPost(native)?.opt("processSimulationAdvance") ?: JSONObject.NULL))
.put("native", native)
private fun lastPost(native: JSONObject): JSONObject? {
val frames = native.optJSONObject("fastGateTrace")?.optJSONArray("frames") ?: return null
return if (frames.length() > 0) frames.optJSONObject(frames.length() - 1)?.optJSONObject("post") else null
}
private fun summary(native: JSONObject): JSONObject {
val t = native.optJSONObject("fastGateTrace") ?: JSONObject()
val gates = t.optJSONArray("gates") ?: JSONArray()
val counts = JSONArray()
for (i in 0 until gates.length()) {
val g = gates.getJSONObject(i)
counts.put(JSONArray().put(g.optString("reason")).put(g.optLong("failures")).put(g.optLong("firstFailures")))
}
val rs = JSONArray()
val segments = native.optJSONArray("segments") ?: JSONArray()
for (i in 0 until segments.length()) {
val s = segments.getJSONObject(i)
val c = s.optJSONObject("cache") ?: continue
rs.put(JSONObject().put("segment", s.optInt("segmentIndex")).put("begin", s.optInt("beginIndex"))
.put("end", s.optInt("endExclusive")).put("state", c.optString("cacheState"))
.put("build", c.optLong("build")).put("reuse", c.optLong("reuse"))
.put("invalidation", c.optLong("invalidation")).put("fallback", c.optLong("fallbackCount"))
.put("reason", c.optString("lastFallbackReason")))
}
return JSONObject().put("schema", "step113-summary").put("schemaVersion", 1).put("scenario", scenario.name)
.put("sceneReady", ready).put("blockedReason", failureReason ?: JSONObject.NULL)
.put("sceneGeneration", generation).put("full113Equivalent", false).put("basePreserved", parity.optBoolean("baseLayersPreserved"))
.put("fingerprint", parity.optString("productionFingerprintSha256")).put("styleUrl", STYLE_URL)
.put("actualLayers", native.optInt("styleLayerCount")).put("planValid", native.optBoolean("valid"))
.put("planReason", native.optString("lastReason")).put("lifecycle", t.optLong("lifecycle"))
.put("attempts", t.optLong("attempts")).put("windowCount", t.optInt("windowCount"))
.put("windowBegin", t.optLong("windowBeginSequence")).put("windowEnd", t.optLong("windowEndSequence"))
.put("windowFullRendered", t.optLong("windowFullRendered"))
.put("windowFastRendered", t.optLong("windowFastRendered"))
.put("processSimulationAdvance", lastPost(native)?.opt("processSimulationAdvance") ?: JSONObject.NULL)
.put("overwritten", t.optLong("overwritten")).put("decisionMismatch", t.optLong("decisionMismatch"))
.put("fullRendered", t.optLong("fullRendered")).put("fastRendered", t.optLong("fastRendered"))
.put("gateFailureCounts", counts).put("retained", rs)
}
fun export() {
// Export exactly the last low-frequency shared snapshot; do not request an extra render/query.
val record = latest ?: return
val file = File(context.filesDir, "step113-${scenario.name}-${SystemClock.elapsedRealtime()}.json")
try {
file.writeText(record.toString(), Charsets.UTF_8)
logRecord("snapshot", record)
display("STEP-113 已导出 ${file.name};Logcat标签 $TAG")
} catch (error: Exception) { Log.e(TAG, "export-failed", error) }
}
private fun logRecord(kind: String, record: JSONObject) {
// Logcat has a per-entry limit. UTF-8 -> Base64 ensures chunks never split Unicode.
val data = Base64.encodeToString(record.toString().toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
val chunks = data.chunked(2400)
val id = "${SystemClock.elapsedRealtime()}-${generation}-${++exportSequence}"
chunks.forEachIndexed { index, chunk ->
Log.i(TAG, JSONObject().put("schemaVersion", 1).put("kind", kind).put("exportId", id)
.put("chunkIndex", index).put("chunkCount", chunks.size).put("encoding", "base64-utf8")
.put("payload", chunk).toString())
}
}
fun onStyleLoadFailed(message: String) {
if (running) fail(IllegalStateException(message))
}
private fun fail(error: Exception) {
ready = false
failureReason = "BLOCKED: ${error.message}"
display("STEP-113 BLOCKED: ${error.message}")
Log.e(TAG, "scene-blocked", error)
logRecord("blocked", JSONObject().put("schemaVersion", 1).put("scenario", scenario.name)
.put("reason", error.message).put("styleUrl", STYLE_URL).put("evidence", "NOT-VERIFIED"))
}
fun pause() { paused = true; handler.removeCallbacksAndMessages(null) }
fun resume() {
paused = false
handler.removeCallbacksAndMessages(null)
if (running) handler.post(poll)
}
fun stop() {
running = false
++generation
handler.removeCallbacksAndMessages(null)
runtime?.dispose()
runtime = 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
}
companion object {
const val TAG = "STEP113Gate"
const val STYLE_URL = "https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json"
private const val SPRITE = "https://pbf.jpcn.ccwu.cc/sprite/sprite"
private const val GLYPHS = "https://pbf.jpcn.ccwu.cc/fonts/{fontstack}/{range}.pbf"
private const val TILES = "https://pbf.jpcn.ccwu.cc/pbf/pbf-delivery-full-20260418-rebuild/{z}/{x}/{y}.pbf"
private const val SOURCE = "rn-weather-fields"
private const val COLOR = "rn-weather-color"
private const val PARTICLE = "rn-weather-particles"
private const val PROBE_SOURCE = "step113-source-probe"
private const val PROBE_LAYER = "step113-source-probe-circle"
private const val FORECAST_MS = 1786060800000L
private fun planJson(s: Scenario, finalBaseId: 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"))
segments.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", finalBaseId))
}
}
return JSONObject().put("schemaVersion", 1).put("segments", segments).toString()
}
private fun layerManifest(layers: JSONArray): JSONArray = JSONArray().apply {
for (i in 0 until layers.length()) {
val l = layers.getJSONObject(i)
put(JSONObject().put("index", i).put("id", l.getString("id")).put("type", l.getString("type"))
.put("source", l.opt("source") ?: JSONObject.NULL)
.put("visibility", l.optJSONObject("layout")?.optString("visibility", "visible") ?: "visible"))
}
}
private fun sha256(text: String): String = MessageDigest.getInstance("SHA-256")
.digest(text.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it.toInt() and 0xff) }
}
}

View File

@@ -115,7 +115,7 @@ bool Map::setRenderSegmentPlan(RenderSegmentPlan plan) {
} }
RenderSegmentDiagnostics Map::getRenderSegmentDiagnostics() const { RenderSegmentDiagnostics Map::getRenderSegmentDiagnostics() const {
return impl->retainedUnderlayState->renderSegmentSnapshot(); return impl->retainedUnderlayState->renderSegmentSnapshot(true);
} }
bool Map::isRetainedUnderlayEnabled() const { bool Map::isRetainedUnderlayEnabled() const {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@@ -126,6 +127,38 @@ inline ParticleFastPathDecision evaluateParticleFastPath(const ParticleFastPathG
return {true, ParticleFastPathMissReason::None}; return {true, ParticleFastPathMissReason::None};
} }
// Diagnostic order follows evaluateParticleFastPath, including viewport before camera.
// Keep the production decision above independent: this is a side-effect-free shadow.
inline constexpr std::array<ParticleFastPathMissReason, 24> particleFastGateReasons{{
ParticleFastPathMissReason::NoPreparedTree, ParticleFastPathMissReason::RetainedDisabled,
ParticleFastPathMissReason::CacheInvalid, ParticleFastPathMissReason::RevisionMismatch,
ParticleFastPathMissReason::ContentMutation, ParticleFastPathMissReason::ViewportChanged,
ParticleFastPathMissReason::CameraChanged, ParticleFastPathMissReason::UpdateParametersChanged,
ParticleFastPathMissReason::StyleNotLoaded, ParticleFastPathMissReason::BoundaryChanged,
ParticleFastPathMissReason::TopologyChanged, ParticleFastPathMissReason::StaticUnstable,
ParticleFastPathMissReason::PlacementOrFade, ParticleFastPathMissReason::PendingChanges,
ParticleFastPathMissReason::NonParticleTransition, ParticleFastPathMissReason::NonParticleRepaint,
ParticleFastPathMissReason::FadingTiles, ParticleFastPathMissReason::NoParticleRepaint,
ParticleFastPathMissReason::StillImageRequest, ParticleFastPathMissReason::UnsupportedMode,
ParticleFastPathMissReason::DebugOptions, ParticleFastPathMissReason::UnsupportedContinuousLayer,
ParticleFastPathMissReason::UnsupportedScene, ParticleFastPathMissReason::AnnotationMutation,
}};
inline uint32_t particleFastGateFailureMask(const ParticleFastPathGateInput& i) noexcept {
const std::array<bool, 24> failures{{
!i.hasPreparedTree, !i.retainedEnabled, !i.cacheValid, !i.revisionMatches,
!i.contentRevisionMatches, !i.viewportMatches, !i.cameraMatches, !i.updateParametersMatch,
!i.styleLoaded, !i.boundaryMatches, !i.topologyMatches, !i.staticStable,
i.hasPlacementTransition, i.hasPendingChanges, i.hasNonParticleTransition,
i.hasNonParticleRepaint, i.hasFadingTiles, !i.particleNeedsRepaint, i.stillImageRequest,
!i.continuousMode, !i.debugDisabled, i.unsupportedContinuousLayer, !i.supportedScene,
i.annotationMutation,
}};
uint32_t mask = 0;
for (std::size_t n = 0; n < failures.size(); ++n) if (failures[n]) mask |= uint32_t{1} << n;
return mask;
}
template <class T> template <class T>
class ParticleFastPathPreparedTree { class ParticleFastPathPreparedTree {
public: public:

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <mln/renderer/render_segment.hpp> #include <mln/renderer/render_segment.hpp>
#include <mln/renderer/particle_fast_path.hpp>
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h> #include <rapidjson/stringbuffer.h>
@@ -107,11 +108,141 @@ inline void writeStyleLayerDiagnostics(Writer& writer, const StyleLayerDiagnosti
writer.EndObject(); writer.EndObject();
} }
template <typename Writer>
inline void writeFastGatePhase(Writer& w, const FastGatePhaseSample& p, const char* phase,
const char* placementBasis = "previous-completed-render-tree") {
w.StartObject();
w.Key("phase"); w.String(phase);
w.Key("placementBasis"); w.String(!p.hasPlacementSnapshot ? "UNKNOWN/no-completed-snapshot"
: placementBasis);
w.Key("placementChanged"); w.Bool(p.placementChanged);
w.Key("symbolFadeChange"); w.Double(p.symbolFadeChange);
w.Key("placementTransitionActive"); w.Bool(p.placementTransitionActive);
w.Key("contentRevision"); w.Uint64(p.contentRevision);
w.Key("topologyRevision"); w.Uint64(p.topologyRevision);
w.Key("configRevision"); w.Uint64(p.configRevision);
w.Key("fullCpuFrame"); w.Uint64(p.fullCpuFrame);
w.Key("processSimulationAdvance");
if (p.processSimulationAdvance) w.Uint64(*p.processSimulationAdvance); else w.Null();
w.Key("retainedSamplesTruncated"); w.Bool(p.retainedSamplesTruncated);
w.Key("retained"); w.StartArray();
for (const auto& r : p.retained) {
const auto& c = r.cache;
w.StartObject();
w.Key("segmentIndex"); w.Uint(r.segmentIndex);
w.Key("cacheState"); w.String(c.cacheState.c_str());
w.Key("build"); w.Uint64(c.build);
w.Key("reuse"); w.Uint64(c.reuse);
w.Key("invalidation"); w.Uint64(c.invalidation);
w.Key("fallbackCount"); w.Uint64(c.fallbackCount);
w.Key("contentRevision"); w.Uint64(c.contentRevision);
w.Key("sourceRevision"); w.Uint64(c.sourceRevision);
w.Key("sourceReady"); w.Bool(c.sourceReady);
w.Key("sourceStable"); w.Bool(c.sourceStable);
w.Key("supported"); w.Bool(c.supported);
w.Key("finalDecision"); w.String(c.finalDecision.c_str());
w.Key("lastInvalidationReason"); w.String(c.lastInvalidationReason.c_str());
w.Key("lastFallbackReason"); w.String(c.lastFallbackReason.c_str());
w.Key("placementGatePassed"); w.Bool(r.placementGatePassed);
w.Key("actualExecution"); w.String(r.actualExecution.c_str());
w.EndObject();
}
w.EndArray();
w.EndObject();
}
template <typename Writer>
inline void writeFastGateTrace(Writer& w, const FastGateTraceDiagnostics& t) {
w.StartObject();
w.Key("schemaVersion"); w.Uint(t.schemaVersion);
w.Key("capacity"); w.Uint(120);
w.Key("retainedLimitPerPhase"); w.Uint(16);
w.Key("lifecycle"); w.Uint64(t.lifecycle);
w.Key("attempts"); w.Uint64(t.attempts);
w.Key("overwritten"); w.Uint64(t.overwritten);
w.Key("decisionMismatch"); w.Uint64(t.decisionMismatch);
w.Key("fullRendered"); w.Uint64(t.fullRendered);
w.Key("fastRendered"); w.Uint64(t.fastRendered);
w.Key("noRender"); w.Uint64(t.noRender);
w.Key("windowCount"); w.Uint(static_cast<unsigned>(t.frames.size()));
w.Key("windowBeginSequence"); w.Uint64(t.frames.empty() ? 0 : t.frames.front().sequence);
w.Key("windowEndSequence"); w.Uint64(t.frames.empty() ? 0 : t.frames.back().sequence);
uint64_t windowFull = 0, windowFast = 0, windowNoRender = 0;
std::array<uint64_t, 24> windowFailures{};
for (const auto& f : t.frames) {
if (f.completed && f.outcome == "full-rendered") ++windowFull;
else if (f.completed && f.outcome == "fast-rendered") ++windowFast;
else if (f.completed) ++windowNoRender;
for (std::size_t i = 0; i < 24; ++i) if (f.failMask & (uint32_t{1} << i)) ++windowFailures[i];
}
w.Key("windowFullRendered"); w.Uint64(windowFull);
w.Key("windowFastRendered"); w.Uint64(windowFast);
w.Key("windowNoRender"); w.Uint64(windowNoRender);
w.Key("gates"); w.StartArray();
for (std::size_t i = 0; i < particleFastGateReasons.size(); ++i) {
w.StartObject();
w.Key("order"); w.Uint(static_cast<unsigned>(i + 1));
w.Key("reason"); w.String(particleFastPathMissReasonText(particleFastGateReasons[i]));
w.Key("failures"); w.Uint64(t.failureCounts[i]);
w.Key("firstFailures"); w.Uint64(t.firstFailureCounts[i]);
w.Key("windowFailures"); w.Uint64(windowFailures[i]);
w.EndObject();
}
w.EndArray();
w.Key("frames"); w.StartArray();
for (const auto& f : t.frames) {
w.StartObject();
w.Key("sequence"); w.Uint64(f.sequence);
w.Key("frame"); w.Uint64(f.frame);
w.Key("updateTimeNs"); w.Int64(f.updateTimeNs);
w.Key("knownMask"); w.Uint(f.knownMask);
w.Key("failMask"); w.Uint(f.failMask);
w.Key("firstFailure"); w.String(f.firstFailure.c_str());
w.Key("decisionConsistent"); w.Bool(f.decisionConsistent);
w.Key("candidate"); w.Bool(f.candidate);
w.Key("preFastPlacementTransition"); w.Bool(f.preFastPlacementTransition);
w.Key("placementTransitionsEnabled"); w.Bool(f.placementTransitionsEnabled);
w.Key("completed"); w.Bool(f.completed);
w.Key("outcome"); w.String(f.outcome.c_str());
w.Key("gateStatus"); w.StartArray();
for (std::size_t i = 0; i < 24; ++i) {
const auto bit = uint32_t{1} << i;
w.String(!(f.knownMask & bit) ? "UNKNOWN" : (f.failMask & bit) ? "FAIL" : "PASS");
}
w.EndArray();
w.Key("cacheInputsTruncated"); w.Bool(f.cacheInputsTruncated);
w.Key("cacheInputs"); w.StartArray();
for (const auto& c : f.cacheInputs) {
w.StartObject();
w.Key("segmentIndex"); w.Uint(c.segmentIndex);
w.Key("exists"); w.Bool(c.exists);
w.Key("valid"); w.Bool(c.valid);
w.Key("buildRevision"); w.Uint64(c.buildRevision);
w.Key("staticRevision"); w.Uint64(c.staticRevision);
w.EndObject();
}
w.EndArray();
w.Key("pre"); writeFastGatePhase(w, f.pre, "pre-fast");
w.Key("post");
if (f.completed) {
writeFastGatePhase(w, f.post, "post-render", f.outcome == "full-rendered"
? "current-full-render-tree" : f.outcome == "fast-rendered"
? "current-fast-render-tree/no-new-placement" : "previous-completed-render-tree");
} else w.Null();
w.EndObject();
}
w.EndArray();
w.EndObject();
}
inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics& snapshot) { inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics& snapshot) {
rapidjson::StringBuffer buffer; rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
writer.StartObject(); writer.StartObject();
writer.Key("schemaVersion"); writer.Uint(snapshot.schemaVersion); writer.Key("schemaVersion"); writer.Uint(snapshot.schemaVersion);
if (snapshot.fastGateTrace) {
writer.Key("fastGateTrace"); writeFastGateTrace(writer, *snapshot.fastGateTrace);
}
writer.Key("enabled"); writer.Bool(snapshot.enabled); writer.Key("enabled"); writer.Bool(snapshot.enabled);
writer.Key("configured"); writer.Bool(snapshot.configured); writer.Key("configured"); writer.Bool(snapshot.configured);
writer.Key("valid"); writer.Bool(snapshot.valid); writer.Key("valid"); writer.Bool(snapshot.valid);

View File

@@ -8,6 +8,7 @@
#include <mln/renderer/renderer_impl.hpp> #include <mln/renderer/renderer_impl.hpp>
#include <mln/renderer/render_static_data.hpp> #include <mln/renderer/render_static_data.hpp>
#include <mln/renderer/render_tree.hpp> #include <mln/renderer/render_tree.hpp>
#include <mln/renderer/retained_underlay_runtime_state.hpp>
#include <mln/renderer/update_parameters.hpp> #include <mln/renderer/update_parameters.hpp>
#include <mln/util/error_sink.hpp> #include <mln/util/error_sink.hpp>
#include <mln/util/instrumentation.hpp> #include <mln/util/instrumentation.hpp>
@@ -26,6 +27,7 @@ Renderer::~Renderer() {
void Renderer::markContextLost() { void Renderer::markContextLost() {
impl->clearPreparedRenderTree(); impl->clearPreparedRenderTree();
if (impl->retainedRuntimeState) impl->retainedRuntimeState->resetFastGateTrace();
impl->orchestrator.markContextLost(); impl->orchestrator.markContextLost();
} }
@@ -44,6 +46,7 @@ void Renderer::render(const std::shared_ptr<UpdateParameters>& updateParameters)
impl->synchronizeRetainedUnderlayState(updateParameters->retainedUnderlayState); impl->synchronizeRetainedUnderlayState(updateParameters->retainedUnderlayState);
const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded; const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded;
if (styleChanged && impl->retainedRuntimeState) impl->retainedRuntimeState->resetFastGateTrace();
impl->styleLoaded = updateParameters->styleLoaded; impl->styleLoaded = updateParameters->styleLoaded;
if (!impl->dynamicTextureAtlas || styleChanged) { if (!impl->dynamicTextureAtlas || styleChanged) {
if (styleChanged) impl->clearPreparedRenderTree(); if (styleChanged) impl->clearPreparedRenderTree();
@@ -61,6 +64,7 @@ void Renderer::render(const std::shared_ptr<UpdateParameters>& updateParameters)
impl->commitPreparedRenderTree(std::move(renderTree), updateParameters); impl->commitPreparedRenderTree(std::move(renderTree), updateParameters);
} else { } else {
impl->clearPreparedRenderTree(); impl->clearPreparedRenderTree();
if (impl->retainedRuntimeState) impl->retainedRuntimeState->completeFastGateAttempt("no-render-tree");
} }
} }
@@ -184,6 +188,7 @@ void Renderer::reduceMemoryUse() {
} }
void Renderer::clearData() { void Renderer::clearData() {
if (impl->retainedRuntimeState) impl->retainedRuntimeState->resetFastGateTrace();
impl->clearPreparedRenderTree(); impl->clearPreparedRenderTree();
impl->orchestrator.clearData(); impl->orchestrator.clearData();
} }

View File

@@ -30,6 +30,8 @@
#include <mln/renderer/render_target.hpp> #include <mln/renderer/render_target.hpp>
#include <mln/renderer/retained_underlay_cache.hpp> #include <mln/renderer/retained_underlay_cache.hpp>
#include <mln/particle/renderer_debug_counters.hpp> #include <mln/particle/renderer_debug_counters.hpp>
#include <algorithm>
#include <chrono>
#include <string_view> #include <string_view>
#if MLN_RENDER_BACKEND_METAL #if MLN_RENDER_BACKEND_METAL
@@ -142,6 +144,7 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) {
void Renderer::Impl::synchronizeRetainedUnderlayState( void Renderer::Impl::synchronizeRetainedUnderlayState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& state) { const std::shared_ptr<RetainedUnderlayRuntimeState>& state) {
if (state != retainedRuntimeState && state) state->resetFastGateTrace();
retainedRuntimeState = state; retainedRuntimeState = state;
for (auto& cache : retainedSegmentCaches) { for (auto& cache : retainedSegmentCaches) {
if (cache) cache->synchronizeRuntimeState(state); if (cache) cache->synchronizeRuntimeState(state);
@@ -217,11 +220,21 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
const bool cameraMatches = hasSignature && particlePreparedSignature->cameraMatrix == signature.cameraMatrix; const bool cameraMatches = hasSignature && particlePreparedSignature->cameraMatrix == signature.cameraMatrix;
const bool updateParametersMatch = const bool updateParametersMatch =
hasSignature && particleFastPathNonCameraInputsMatch(*particlePreparedSignature, signature); hasSignature && particleFastPathNonCameraInputsMatch(*particlePreparedSignature, signature);
std::vector<FastGateCacheInputSample> cacheInputs;
cacheInputs.reserve(std::min<std::size_t>(16, retainedSegmentCaches.size()));
bool cacheInputsTruncated = false;
bool retainedRevisionsMatch = retainedSegmentCaches.size() == plan.segments.size(); bool retainedRevisionsMatch = retainedSegmentCaches.size() == plan.segments.size();
bool retainedCachesValid = retainedRevisionsMatch; bool retainedCachesValid = retainedRevisionsMatch;
for (std::size_t i = 0; i < retainedSegmentCaches.size(); ++i) { for (std::size_t i = 0; i < retainedSegmentCaches.size(); ++i) {
if (plan.segments[i].strategy != RenderSegmentStrategy::Retained) continue; if (plan.segments[i].strategy != RenderSegmentStrategy::Retained) continue;
const auto& cache = retainedSegmentCaches[i]; const auto& cache = retainedSegmentCaches[i];
// These accessors are pure; capture actual pre-fast validity/revisions
// even when the aggregate gate already failed on an earlier R.
if (cacheInputs.size() < 16) {
cacheInputs.push_back({static_cast<uint32_t>(i), static_cast<bool>(cache),
cache && cache->isValid(), cache ? cache->getBuildRevision() : 0,
cache ? cache->getStaticRevision() : 0});
} else cacheInputsTruncated = true;
retainedRevisionsMatch = retainedRevisionsMatch && cache && retainedRevisionsMatch = retainedRevisionsMatch && cache &&
cache->getBuildRevision() == cache->getStaticRevision(); cache->getBuildRevision() == cache->getStaticRevision();
retainedCachesValid = retainedCachesValid && cache && cache->isValid(); retainedCachesValid = retainedCachesValid && cache && cache->isValid();
@@ -274,6 +287,13 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdatePara
.annotationMutation = annotationMutation, .annotationMutation = annotationMutation,
}; };
const auto decision = evaluateParticleFastPath(input); const auto decision = evaluateParticleFastPath(input);
if (retainedRuntimeState) {
retainedRuntimeState->recordFastGateAttempt(
frameCount,
std::chrono::duration_cast<std::chrono::nanoseconds>(updateParameters->timePoint.time_since_epoch()).count(),
input, decision, updateParameters->transitionOptions.enablePlacementTransitions,
std::move(cacheInputs), cacheInputsTruncated);
}
particleFastPathLastReason = decision.reason; particleFastPathLastReason = decision.reason;
if (retainedRuntimeState) retainedRuntimeState->recordFastPathDecision(decision.candidate, decision.reason); if (retainedRuntimeState) retainedRuntimeState->recordFastPathDecision(decision.candidate, decision.reason);
if (!decision.candidate) { if (!decision.candidate) {
@@ -1089,6 +1109,9 @@ void Renderer::Impl::render(const RenderTree& renderTree,
} }
#endif #endif
if (retainedRuntimeState) {
retainedRuntimeState->completeFastGateAttempt(particleFastFrame ? "fast-rendered" : "full-rendered");
}
frameCount += 1; frameCount += 1;
MLN_END_FRAME(); MLN_END_FRAME();
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <mln/renderer/particle_fast_path.hpp> #include <mln/renderer/particle_fast_path.hpp>
#include <mln/particle/renderer_debug_counters.hpp>
#include <mln/renderer/retained_underlay.hpp> #include <mln/renderer/retained_underlay.hpp>
#include <mln/renderer/render_segment.hpp> #include <mln/renderer/render_segment.hpp>
#include <mln/renderer/render_segment_plan.hpp> #include <mln/renderer/render_segment_plan.hpp>
@@ -291,9 +292,17 @@ public:
renderSegmentDiagnostics.lastMutation = std::move(mutation); renderSegmentDiagnostics.lastMutation = std::move(mutation);
} }
RenderSegmentDiagnostics renderSegmentSnapshot() const { RenderSegmentDiagnostics renderSegmentSnapshot(bool includeFastGateTrace = false) const {
std::lock_guard<std::mutex> lock(rangeMutex); std::lock_guard<std::mutex> lock(rangeMutex);
auto result = renderSegmentDiagnostics; auto result = renderSegmentDiagnostics;
if (includeFastGateTrace) {
result.fastGateTrace = fastGateTrace;
auto& frames = result.fastGateTrace->frames;
frames.reserve(fastGateSize);
for (std::size_t i = 0; i < fastGateSize; ++i) {
frames.push_back(fastGateRing[(fastGateNext + FastGateCapacity - fastGateSize + i) % FastGateCapacity]);
}
}
result.enabled = isEnabled(); result.enabled = isEnabled();
result.fastPathEligible = fastPathEligible.load(std::memory_order_acquire); result.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
result.fastPathActive = fastPathActive.load(std::memory_order_acquire); result.fastPathActive = fastPathActive.load(std::memory_order_acquire);
@@ -486,7 +495,101 @@ public:
fullCpuFrame.fetch_add(1, std::memory_order_relaxed); fullCpuFrame.fetch_add(1, std::memory_order_relaxed);
fastPathActive.store(false, std::memory_order_release); fastPathActive.store(false, std::memory_order_release);
} }
// Called only by the render thread. UI readers copy under rangeMutex.
// Reset is explicit at renderer/style lifecycle boundaries, never on each
// prepared-tree clear (which happens on every Full miss).
void resetFastGateTrace() {
std::lock_guard<std::mutex> lock(rangeMutex);
const auto lifecycle = fastGateTrace.lifecycle + 1;
fastGateTrace = {};
fastGateTrace.lifecycle = lifecycle;
fastGateNext = fastGateSize = 0;
for (auto& frame : fastGateRing) frame = {};
}
void recordFastGateAttempt(uint64_t frame, int64_t updateTimeNs,
const ParticleFastPathGateInput& input,
const ParticleFastPathDecision& decision,
bool placementTransitionsEnabled,
std::vector<FastGateCacheInputSample> cacheInputs = {},
bool cacheInputsTruncated = false) {
std::lock_guard<std::mutex> lock(rangeMutex);
FastGateFrameSample sample;
sample.sequence = ++fastGateTrace.attempts;
sample.frame = frame;
sample.updateTimeNs = updateTimeNs;
sample.failMask = particleFastGateFailureMask(input);
sample.firstFailure = particleFastPathMissReasonText(decision.reason);
sample.candidate = decision.candidate;
sample.preFastPlacementTransition = input.hasPlacementTransition;
sample.placementTransitionsEnabled = placementTransitionsEnabled;
sample.cacheInputs = std::move(cacheInputs);
sample.cacheInputsTruncated = cacheInputsTruncated;
auto shadowReason = ParticleFastPathMissReason::None;
for (std::size_t i = 0; i < 24; ++i) {
if (!(sample.failMask & (uint32_t{1} << i))) continue;
++fastGateTrace.failureCounts[i];
if (shadowReason == ParticleFastPathMissReason::None) {
shadowReason = particleFastGateReasons[i];
}
}
for (std::size_t i = 0; i < 24; ++i) {
if (particleFastGateReasons[i] == decision.reason) ++fastGateTrace.firstFailureCounts[i];
}
sample.decisionConsistent = shadowReason == decision.reason && decision.candidate == (sample.failMask == 0);
if (!sample.decisionConsistent) ++fastGateTrace.decisionMismatch;
sample.pre = fastGatePhaseSample();
if (fastGateSize == FastGateCapacity) ++fastGateTrace.overwritten;
else ++fastGateSize;
fastGateRing[fastGateNext] = std::move(sample);
fastGateNext = (fastGateNext + 1) % FastGateCapacity;
}
void completeFastGateAttempt(const char* outcome) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (!fastGateSize) return;
auto& frame = fastGateRing[(fastGateNext + FastGateCapacity - 1) % FastGateCapacity];
if (frame.completed) return;
frame.post = fastGatePhaseSample();
frame.completed = true;
frame.outcome = outcome;
if (std::string_view(outcome) == "full-rendered") ++fastGateTrace.fullRendered;
else if (std::string_view(outcome) == "fast-rendered") ++fastGateTrace.fastRendered;
else ++fastGateTrace.noRender;
}
private: private:
// rangeMutex must already be held. Fixed R limit keeps memory bounded even
// for arbitrary host plans; truncation is explicit, aggregate gates are complete.
FastGatePhaseSample fastGatePhaseSample() const {
FastGatePhaseSample result;
result.hasPlacementSnapshot = renderSegmentDiagnostics.mainBufferExecutionMode != "not-rendered";
result.placementChanged = renderSegmentDiagnostics.placementChanged;
result.symbolFadeChange = renderSegmentDiagnostics.symbolFadeChange;
result.placementTransitionActive = renderSegmentDiagnostics.placementTransitionActive;
result.contentRevision = renderSegmentDiagnostics.contentRevision;
result.topologyRevision = renderSegmentDiagnostics.topologyRevision;
result.configRevision = renderSegmentDiagnostics.configRevision;
result.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
#ifndef NDEBUG
result.processSimulationAdvance = particle::rendererDebugCounterStorage()[
static_cast<std::size_t>(particle::RendererDebugCounter::ParticleSimulationAdvance)]
.load(std::memory_order_relaxed);
#endif
for (const auto& segment : renderSegmentDiagnostics.segments) {
if (!segment.cache) continue;
if (result.retained.size() == 16) { result.retainedSamplesTruncated = true; break; }
result.retained.push_back({segment.segmentIndex, *segment.cache,
segment.placementGatePassed, segment.actualExecution});
}
return result;
}
static constexpr std::size_t FastGateCapacity = 120;
std::array<FastGateFrameSample, FastGateCapacity> fastGateRing{};
std::size_t fastGateNext = 0;
std::size_t fastGateSize = 0;
FastGateTraceDiagnostics fastGateTrace;
template <typename Fn> template <typename Fn>
void updateSegmentCache(uint32_t index, Fn&& fn) { void updateSegmentCache(uint32_t index, Fn&& fn) {
std::lock_guard<std::mutex> lock(rangeMutex); std::lock_guard<std::mutex> lock(rangeMutex);

View File

@@ -1362,3 +1362,138 @@ TEST(RenderSegmentPlan, InvalidJSONClearsRendererPlanAndValidPlanRecovers) {
ASSERT_TRUE(state.setRenderSegmentPlan(std::move(recovered))); ASSERT_TRUE(state.setRenderSegmentPlan(std::move(recovered)));
EXPECT_TRUE(resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true).valid); EXPECT_TRUE(resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true).valid);
} }
TEST(FastGateTrace, EveryGateBitMatchesProductionDecisionOrder) {
using Field = bool ParticleFastPathGateInput::*;
const std::array<Field, 24> fields{{
&ParticleFastPathGateInput::hasPreparedTree, &ParticleFastPathGateInput::retainedEnabled,
&ParticleFastPathGateInput::cacheValid, &ParticleFastPathGateInput::revisionMatches,
&ParticleFastPathGateInput::contentRevisionMatches, &ParticleFastPathGateInput::viewportMatches,
&ParticleFastPathGateInput::cameraMatches, &ParticleFastPathGateInput::updateParametersMatch,
&ParticleFastPathGateInput::styleLoaded, &ParticleFastPathGateInput::boundaryMatches,
&ParticleFastPathGateInput::topologyMatches, &ParticleFastPathGateInput::staticStable,
&ParticleFastPathGateInput::hasPlacementTransition, &ParticleFastPathGateInput::hasPendingChanges,
&ParticleFastPathGateInput::hasNonParticleTransition, &ParticleFastPathGateInput::hasNonParticleRepaint,
&ParticleFastPathGateInput::hasFadingTiles, &ParticleFastPathGateInput::particleNeedsRepaint,
&ParticleFastPathGateInput::stillImageRequest, &ParticleFastPathGateInput::continuousMode,
&ParticleFastPathGateInput::debugDisabled, &ParticleFastPathGateInput::unsupportedContinuousLayer,
&ParticleFastPathGateInput::supportedScene, &ParticleFastPathGateInput::annotationMutation,
}};
EXPECT_EQ(particleFastGateFailureMask(fastPathInput()), 0u);
auto allFail = fastPathInput();
for (std::size_t i = 0; i < fields.size(); ++i) {
auto input = fastPathInput();
input.*fields[i] = !(input.*fields[i]);
allFail.*fields[i] = input.*fields[i];
EXPECT_EQ(particleFastGateFailureMask(input), uint32_t{1} << i);
EXPECT_EQ(evaluateParticleFastPath(input).reason, particleFastGateReasons[i]);
EXPECT_FALSE(evaluateParticleFastPath(input).candidate);
}
EXPECT_EQ(particleFastGateFailureMask(allFail), 0x00ffffffu);
}
TEST(FastGateTrace, CacheMissRetainsLaterFailuresAndBoundedOrderedWindow) {
RetainedUnderlayRuntimeState state;
state.resetFastGateTrace();
auto input = fastPathInput();
input.cacheValid = false;
input.unsupportedContinuousLayer = true;
input.supportedScene = false;
const auto decision = evaluateParticleFastPath(input);
for (uint64_t i = 0; i < 137; ++i) {
state.recordFastGateAttempt(i, static_cast<int64_t>(i * 33333333), input, decision, true);
if (i == 0) state.completeFastGateAttempt("no-render-tree");
else {
state.recordFullCpuFrame();
state.completeFastGateAttempt("full-rendered");
}
}
EXPECT_FALSE(state.renderSegmentSnapshot().fastGateTrace); // No ring copy on internal render hot path.
const auto snapshot = state.renderSegmentSnapshot(true);
ASSERT_TRUE(snapshot.fastGateTrace);
const auto& t = *snapshot.fastGateTrace;
EXPECT_EQ(t.attempts, 137u);
EXPECT_EQ(t.overwritten, 17u);
EXPECT_EQ(t.decisionMismatch, 0u);
EXPECT_EQ(t.fullRendered, 136u);
EXPECT_EQ(t.noRender, 1u);
ASSERT_EQ(t.frames.size(), 120u);
EXPECT_EQ(t.frames.front().sequence, 18u);
EXPECT_EQ(t.frames.back().sequence, 137u);
EXPECT_EQ(t.frames.back().firstFailure, "cache-invalid");
EXPECT_EQ(t.frames.back().failMask, (1u << 2) | (1u << 21) | (1u << 22));
EXPECT_EQ(t.frames.back().pre.fullCpuFrame, 135u);
EXPECT_EQ(t.frames.back().post.fullCpuFrame, 136u);
EXPECT_EQ(t.failureCounts[2], 137u);
EXPECT_EQ(t.failureCounts[21], 137u);
EXPECT_EQ(t.failureCounts[22], 137u);
EXPECT_EQ(t.firstFailureCounts[2], 137u);
const auto json = renderSegmentDiagnosticsToJSON(snapshot);
rapidjson::Document document;
ASSERT_FALSE(document.Parse(json.c_str()).HasParseError());
const auto& last = document["fastGateTrace"]["frames"][119];
ASSERT_EQ(last["gateStatus"].Size(), 24u);
EXPECT_STREQ(last["gateStatus"][2].GetString(), "FAIL");
EXPECT_STREQ(last["gateStatus"][21].GetString(), "FAIL");
EXPECT_STREQ(last["gateStatus"][22].GetString(), "FAIL");
EXPECT_STREQ(last["pre"]["phase"].GetString(), "pre-fast");
EXPECT_STREQ(last["post"]["phase"].GetString(), "post-render");
state.resetFastGateTrace();
const auto reset = state.renderSegmentSnapshot(true);
ASSERT_TRUE(reset.fastGateTrace);
EXPECT_EQ(reset.fastGateTrace->lifecycle, t.lifecycle + 1);
EXPECT_TRUE(reset.fastGateTrace->frames.empty());
EXPECT_EQ(reset.fastGateTrace->attempts, 0u);
EXPECT_EQ(reset.fastGateTrace->failureCounts[2], 0u);
}
TEST(FastGateTrace, PendingAttemptAndInconsistentShadowAreExplicit) {
RetainedUnderlayRuntimeState state;
auto input = fastPathInput();
state.recordFastGateAttempt(8, 123, input, {false, ParticleFastPathMissReason::CacheInvalid}, false);
auto snapshot = state.renderSegmentSnapshot(true);
ASSERT_TRUE(snapshot.fastGateTrace);
ASSERT_EQ(snapshot.fastGateTrace->frames.size(), 1u);
EXPECT_EQ(snapshot.fastGateTrace->decisionMismatch, 1u);
EXPECT_FALSE(snapshot.fastGateTrace->frames[0].completed);
EXPECT_FALSE(snapshot.fastGateTrace->frames[0].placementTransitionsEnabled);
const auto json = renderSegmentDiagnosticsToJSON(snapshot);
rapidjson::Document document;
ASSERT_FALSE(document.Parse(json.c_str()).HasParseError());
EXPECT_TRUE(document["fastGateTrace"]["frames"][rapidjson::SizeType{0}]["post"].IsNull());
state.completeFastGateAttempt("fast-rendered");
state.completeFastGateAttempt("fast-rendered"); // Completion must be idempotent.
snapshot = state.renderSegmentSnapshot(true);
EXPECT_EQ(snapshot.fastGateTrace->fastRendered, 1u);
}
TEST(FastGateTrace, RetainedSamplesHaveDistinctPhasesAndExplicitLimit) {
RetainedUnderlayRuntimeState state;
RenderSegmentPlan configured;
std::vector<std::string> ids;
std::vector<StyleLayerDiagnostics> layers;
for (int32_t i = 0; i < 18; ++i) {
ids.push_back("r-" + std::to_string(i));
configured.segments.push_back({RenderSegmentStrategy::Retained,
i ? std::optional<std::string>(ids.back()) : std::nullopt});
layers.push_back({i, ids.back(), "background", true, std::nullopt, std::nullopt});
}
ASSERT_TRUE(state.setRenderSegmentPlan(configured));
const auto resolved = resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), ids, true);
ASSERT_TRUE(resolved.valid);
state.recordRenderSegmentResolution(resolved, layers);
const auto input = fastPathInput();
state.recordFastGateAttempt(0, 0, input, evaluateParticleFastPath(input), true);
state.recordSegmentBuild(0);
state.recordSegmentExecution(0, true, true, true, 7, "build", nullptr);
state.completeFastGateAttempt("full-rendered");
const auto snapshot = state.renderSegmentSnapshot(true);
const auto& frame = snapshot.fastGateTrace->frames.front();
ASSERT_EQ(frame.pre.retained.size(), 16u);
ASSERT_EQ(frame.post.retained.size(), 16u);
EXPECT_TRUE(frame.pre.retainedSamplesTruncated);
EXPECT_TRUE(frame.post.retainedSamplesTruncated);
EXPECT_EQ(frame.pre.retained[0].cache.build, 0u);
EXPECT_EQ(frame.post.retained[0].cache.build, 1u);
EXPECT_EQ(frame.post.retained[0].cache.sourceRevision, 7u);
}

View File

@@ -1133,3 +1133,12 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
- 已交付24项Gate矩阵、R2状态机、开关/Fade真值表、S1—S5和A—D提案。FZG1逐帧配置/来源/CPU、原RCRR完整布局及安全新准入仍待验证,不能称Native已修复或性能PASS。 - 已交付24项Gate矩阵、R2状态机、开关/Fade真值表、S1—S5和A—D提案。FZG1逐帧配置/来源/CPU、原RCRR完整布局及安全新准入仍待验证,不能称Native已修复或性能PASS。
- 仅新增主报告/阶段记录/完成报告和本追加;未改源码/Style/脚本/测试/跨项目文件,未编译/测试/Jest/设备操作,未commit/push,两份未跟踪STEP-110/111任务书保留。 - 仅新增主报告/阶段记录/完成报告和本追加;未改源码/Style/脚本/测试/跨项目文件,未编译/测试/Jest/设备操作,未commit/push,两份未跟踪STEP-110/111任务书保留。
- 主报告:`docs/20_调查/STEP-112_V23粒子FullRender循环与Symbol缓存准入全面源码调查_v1.md`;阶段记录及完成报告见 `docs/50_Step记录/STEP-112_V23粒子FullRender与Symbol缓存调查_v1.md`、`docs/报告/STEP-112_V23粒子FullRender与Symbol缓存调查_Codex完成报告_v1.md`。后续RDC独立审查和Git收尾,未授权Native修复。 - 主报告:`docs/20_调查/STEP-112_V23粒子FullRender循环与Symbol缓存准入全面源码调查_v1.md`;阶段记录及完成报告见 `docs/50_Step记录/STEP-112_V23粒子FullRender与Symbol缓存调查_v1.md`、`docs/报告/STEP-112_V23粒子FullRender与Symbol缓存调查_Codex完成报告_v1.md`。后续RDC独立审查和Git收尾,未授权Native修复。
### 2026-10-11 STEP-113 FZG1 PBF Layer 与 Fast Gate 诊断(源码交付PARTIAL)
- 实际模型已核实 `gpt-6.1-sol/high`;在Mac `particle/main@c6d13e1059d464d312ac33acac87967300fa5dc4` 实现同次input的24Gate首错/失败全集,120帧环形诊断、聚合、pre/post与per-R状态;保留原生产Gate/Placement/Cache准入及安全回退。
- Test App增加正式PBF63、静态RC、粒子关闭、明示部分RCRR、手动Source变化;3秒查询/30秒摘要与STEP113Gate手动导出。没有伪造113层动态业务图层;本机生产域名回读失败,完整等价需同版本快照及设备证据。
- 新增4个C++回归用例源码;仅静态核对与diff空白检查,未编译/运行测试/Jest/Gradle/APK/ADB,未commit/push/发行。STEP-102动态poll/fixture与既有未跟踪任务书保持原样。
- Step与中文交付报告见 `docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md`、`docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md`;RDC独立完成编译、FZG1-2逐帧/资源/场景与开销验收,APK成功不等于性能PASS。
- 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与报告。