feat: add STEP-113 Android PBF fast gate diagnostics
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
41
docs/40_任务/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex任务_v1.md
Normal file
41
docs/40_任务/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex任务_v1.md
Normal 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 诊断。
|
||||||
85
docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md
Normal file
85
docs/50_Step记录/STEP-113_FZG1_PBF_Layer与FastGate诊断_v1.md
Normal 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 放行。
|
||||||
250
docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md
Normal file
250
docs/报告/STEP-113_FZG1_PBF_Layer与FastGate诊断_Codex完成报告_v1.md
Normal 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 放行。
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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与报告。
|
||||||
|
|||||||
Reference in New Issue
Block a user