diff --git a/docs/40_任务/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex任务_v1.md b/docs/40_任务/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex任务_v1.md new file mode 100644 index 000000000..4965f6367 --- /dev/null +++ b/docs/40_任务/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex任务_v1.md @@ -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 诊断。 \ No newline at end of file diff --git a/docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md b/docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md new file mode 100644 index 000000000..44cd26de2 --- /dev/null +++ b/docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md @@ -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 放行。 diff --git a/docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md b/docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md new file mode 100644 index 000000000..fcf6b185e --- /dev/null +++ b/docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md @@ -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--.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 放行。 diff --git a/include/mln/renderer/render_segment.hpp b/include/mln/renderer/render_segment.hpp index f8b61c86f..b5cc044ef 100644 --- a/include/mln/renderer/render_segment.hpp +++ b/include/mln/renderer/render_segment.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -237,6 +238,72 @@ struct RenderSegmentDiagnosticsEntry { std::optional 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 processSimulationAdvance; + std::vector 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 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 failureCounts{}; + std::array firstFailureCounts{}; + std::vector frames; +}; + struct RenderSegmentDiagnostics { uint32_t schemaVersion = 1; bool enabled = true; @@ -274,6 +341,7 @@ struct RenderSegmentDiagnostics { RenderMutationDiagnostics lastMutation; std::vector styleLayers; std::vector segments; + std::optional fastGateTrace; }; struct ResolvedRenderSegment { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index d2403b41a..07e3b6f3f 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -63,6 +63,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var step097Topology: Step097StencilFixture.Topology? = null private var step099Scenario: Step099ReadinessFixture.Scenario? = 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 step102Tick = 0L private val step102Handler = Handler(Looper.getMainLooper()) @@ -347,6 +351,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { step093Variant = savedInstanceState?.getString(STATE_STEP093_VARIANT) ?.let { value -> Step093Variant.values().firstOrNull { it.name == value } } ?: 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) Log.i( TAG, @@ -359,6 +368,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { status = findViewById(R.id.validationStatus) diagnostics = findViewById(R.id.validationDiagnostics) mapView.addOnDidFailLoadingMapListener { errorMessage -> + step113Scene?.onStyleLoadFailed(errorMessage) if (navSeaPbfMode || purePbfMode) { Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage") } else if (realWeatherMode) { @@ -379,7 +389,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { updateNavSeaPbfButtonState() updatePbfModeButtonState() updateRetainedCacheButtonState() - if (step102Scenario != null) loadStep102Style() + if (step113Scenario != null) startStep113Scene(step113Scenario!!) + else if (step102Scenario != null) loadStep102Style() else if (step101Scenario != null) loadStep101Style() else if (step097Topology != null) loadStep097StencilStyle() else if (step093Mode) loadStep093Style(configureBeforeStyle = false) @@ -688,6 +699,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { menu.add(0, 17, 17, "STEP-099 READINESS") menu.add(0, 18, 18, "STEP-101 TEARDOWN") menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY") + menu.add(0, 20, 20, "STEP-113 FZG1-PBF GATE") setOnMenuItemClickListener { item -> when (item.itemId) { in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId]) @@ -701,6 +713,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 17 -> showStep099Menu(anchor) 18 -> showStep101Menu(anchor) 19 -> showStep102Menu(anchor) + 20 -> showStep113Menu(anchor) } 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) { PopupMenu(this, anchor).apply { Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario -> @@ -778,6 +853,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadStep102Style() { + stopStep113Scene() val scenario = step102Scenario ?: return stopStep093DiagnosticsPolling() step093Mode = false @@ -810,6 +886,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadStep101Style() { + stopStep113Scene() val scenario = step101Scenario ?: return stopStep102Scenario() stopStep099Scenario() @@ -836,6 +913,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) { + stopStep113Scene() stopStep102Scenario() stopStep093DiagnosticsPolling() step093Mode = false @@ -971,6 +1049,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadStep097StencilStyle() { + stopStep113Scene() val topology = step097Topology ?: return styleReady = false val epoch = ++styleLoadEpoch @@ -1124,6 +1203,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadStep093Style(configureBeforeStyle: Boolean) { + stopStep113Scene() weatherRuntime?.dispose() weatherRuntime = null styleReady = false @@ -1621,6 +1701,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") { + stopStep113Scene() stopStep102Scenario() if (step099Scenario != null) stopStep099Scenario() step097V4GoldenReady = false @@ -1692,6 +1773,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { ) private fun loadPureParticleStyle(active: Boolean, enabled: Boolean, reason: String) { + stopStep113Scene() weatherRuntime?.dispose() weatherRuntime = null this.active = active @@ -1721,6 +1803,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadPurePbfStyle(reason: String) { + stopStep113Scene() weatherRuntime?.dispose() weatherRuntime = null styleReady = false @@ -1752,6 +1835,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadNavSeaPbfStyle(active: Boolean, enabled: Boolean, reason: String) { + stopStep113Scene() weatherRuntime?.dispose() weatherRuntime = null this.active = active @@ -2397,8 +2481,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } } else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() } - override fun onResume() { super.onResume(); mapView.onResume() } - override fun onPause() { mapView.onPause(); super.onPause() } + override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume() } + override fun onPause() { step113Scene?.pause(); mapView.onPause(); super.onPause() } override fun onStop() { step101Handler.removeCallbacksAndMessages(null) step093Handler.removeCallbacks(step093DiagnosticsPoll) @@ -2408,6 +2492,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { super.onStop() } override fun onDestroy() { + stopStep113Scene() step102Handler.removeCallbacksAndMessages(null) step101Handler.removeCallbacksAndMessages(null) 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 onSaveInstanceState(outState: Bundle) { + outState.putString("step113_scenario", step113Scenario?.name) outState.putBoolean(STATE_ACTIVE, active) outState.putBoolean(STATE_ENABLED, enabled) outState.putInt(STATE_MASK_MODE, maskMode) diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt new file mode 100644 index 000000000..68c24fac4 --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step113PbfGateScene.kt @@ -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? = 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(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) } + } +} diff --git a/src/mln/map/map.cpp b/src/mln/map/map.cpp index 1b345a5aa..6653efce8 100644 --- a/src/mln/map/map.cpp +++ b/src/mln/map/map.cpp @@ -115,7 +115,7 @@ bool Map::setRenderSegmentPlan(RenderSegmentPlan plan) { } RenderSegmentDiagnostics Map::getRenderSegmentDiagnostics() const { - return impl->retainedUnderlayState->renderSegmentSnapshot(); + return impl->retainedUnderlayState->renderSegmentSnapshot(true); } bool Map::isRetainedUnderlayEnabled() const { diff --git a/src/mln/renderer/particle_fast_path.hpp b/src/mln/renderer/particle_fast_path.hpp index 4339f9976..af20f79be 100644 --- a/src/mln/renderer/particle_fast_path.hpp +++ b/src/mln/renderer/particle_fast_path.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -126,6 +127,38 @@ inline ParticleFastPathDecision evaluateParticleFastPath(const ParticleFastPathG 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 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 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 ParticleFastPathPreparedTree { public: diff --git a/src/mln/renderer/render_segment_json.hpp b/src/mln/renderer/render_segment_json.hpp index 155ab0c28..2a69be6ff 100644 --- a/src/mln/renderer/render_segment_json.hpp +++ b/src/mln/renderer/render_segment_json.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -107,11 +108,141 @@ inline void writeStyleLayerDiagnostics(Writer& writer, const StyleLayerDiagnosti writer.EndObject(); } +template +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 +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(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 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(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) { rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); writer.StartObject(); 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("configured"); writer.Bool(snapshot.configured); writer.Key("valid"); writer.Bool(snapshot.valid); diff --git a/src/mln/renderer/renderer.cpp b/src/mln/renderer/renderer.cpp index a68552f85..4c817db94 100644 --- a/src/mln/renderer/renderer.cpp +++ b/src/mln/renderer/renderer.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ Renderer::~Renderer() { void Renderer::markContextLost() { impl->clearPreparedRenderTree(); + if (impl->retainedRuntimeState) impl->retainedRuntimeState->resetFastGateTrace(); impl->orchestrator.markContextLost(); } @@ -44,6 +46,7 @@ void Renderer::render(const std::shared_ptr& updateParameters) impl->synchronizeRetainedUnderlayState(updateParameters->retainedUnderlayState); const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded; + if (styleChanged && impl->retainedRuntimeState) impl->retainedRuntimeState->resetFastGateTrace(); impl->styleLoaded = updateParameters->styleLoaded; if (!impl->dynamicTextureAtlas || styleChanged) { if (styleChanged) impl->clearPreparedRenderTree(); @@ -61,6 +64,7 @@ void Renderer::render(const std::shared_ptr& updateParameters) impl->commitPreparedRenderTree(std::move(renderTree), updateParameters); } else { impl->clearPreparedRenderTree(); + if (impl->retainedRuntimeState) impl->retainedRuntimeState->completeFastGateAttempt("no-render-tree"); } } @@ -184,6 +188,7 @@ void Renderer::reduceMemoryUse() { } void Renderer::clearData() { + if (impl->retainedRuntimeState) impl->retainedRuntimeState->resetFastGateTrace(); impl->clearPreparedRenderTree(); impl->orchestrator.clearData(); } diff --git a/src/mln/renderer/renderer_impl.cpp b/src/mln/renderer/renderer_impl.cpp index 44b5c540f..235d672a1 100644 --- a/src/mln/renderer/renderer_impl.cpp +++ b/src/mln/renderer/renderer_impl.cpp @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include #if MLN_RENDER_BACKEND_METAL @@ -142,6 +144,7 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) { void Renderer::Impl::synchronizeRetainedUnderlayState( const std::shared_ptr& state) { + if (state != retainedRuntimeState && state) state->resetFastGateTrace(); retainedRuntimeState = state; for (auto& cache : retainedSegmentCaches) { if (cache) cache->synchronizeRuntimeState(state); @@ -217,11 +220,21 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptrcameraMatrix == signature.cameraMatrix; const bool updateParametersMatch = hasSignature && particleFastPathNonCameraInputsMatch(*particlePreparedSignature, signature); + std::vector cacheInputs; + cacheInputs.reserve(std::min(16, retainedSegmentCaches.size())); + bool cacheInputsTruncated = false; bool retainedRevisionsMatch = retainedSegmentCaches.size() == plan.segments.size(); bool retainedCachesValid = retainedRevisionsMatch; for (std::size_t i = 0; i < retainedSegmentCaches.size(); ++i) { if (plan.segments[i].strategy != RenderSegmentStrategy::Retained) continue; 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(i), static_cast(cache), + cache && cache->isValid(), cache ? cache->getBuildRevision() : 0, + cache ? cache->getStaticRevision() : 0}); + } else cacheInputsTruncated = true; retainedRevisionsMatch = retainedRevisionsMatch && cache && cache->getBuildRevision() == cache->getStaticRevision(); retainedCachesValid = retainedCachesValid && cache && cache->isValid(); @@ -274,6 +287,13 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptrrecordFastGateAttempt( + frameCount, + std::chrono::duration_cast(updateParameters->timePoint.time_since_epoch()).count(), + input, decision, updateParameters->transitionOptions.enablePlacementTransitions, + std::move(cacheInputs), cacheInputsTruncated); + } particleFastPathLastReason = decision.reason; if (retainedRuntimeState) retainedRuntimeState->recordFastPathDecision(decision.candidate, decision.reason); if (!decision.candidate) { @@ -1089,6 +1109,9 @@ void Renderer::Impl::render(const RenderTree& renderTree, } #endif + if (retainedRuntimeState) { + retainedRuntimeState->completeFastGateAttempt(particleFastFrame ? "fast-rendered" : "full-rendered"); + } frameCount += 1; MLN_END_FRAME(); } diff --git a/src/mln/renderer/retained_underlay_runtime_state.hpp b/src/mln/renderer/retained_underlay_runtime_state.hpp index 3d96e05b6..d246d217c 100644 --- a/src/mln/renderer/retained_underlay_runtime_state.hpp +++ b/src/mln/renderer/retained_underlay_runtime_state.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -291,9 +292,17 @@ public: renderSegmentDiagnostics.lastMutation = std::move(mutation); } - RenderSegmentDiagnostics renderSegmentSnapshot() const { + RenderSegmentDiagnostics renderSegmentSnapshot(bool includeFastGateTrace = false) const { std::lock_guard lock(rangeMutex); 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.fastPathEligible = fastPathEligible.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); 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 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 cacheInputs = {}, + bool cacheInputsTruncated = false) { + std::lock_guard 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 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: + // 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(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 fastGateRing{}; + std::size_t fastGateNext = 0; + std::size_t fastGateSize = 0; + FastGateTraceDiagnostics fastGateTrace; + template void updateSegmentCache(uint32_t index, Fn&& fn) { std::lock_guard lock(rangeMutex); diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index 26d93ad2d..f07ee90f5 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -1362,3 +1362,138 @@ TEST(RenderSegmentPlan, InvalidJSONClearsRendererPlanAndValidPlanRecovers) { ASSERT_TRUE(state.setRenderSegmentPlan(std::move(recovered))); EXPECT_TRUE(resolveRenderSegmentPlan(state.renderSegmentPlanConfig(), segmentLayerIds(), true).valid); } + +TEST(FastGateTrace, EveryGateBitMatchesProductionDecisionOrder) { + using Field = bool ParticleFastPathGateInput::*; + const std::array 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(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 ids; + std::vector 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(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); +} diff --git a/粒子开发记录.md b/粒子开发记录.md index 80f23a358..e7b120a99 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -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。 - 仅新增主报告/阶段记录/完成报告和本追加;未改源码/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修复。 + +### 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与报告。