test: add core particle render layer validation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-08-29 12:55:54 +08:00
parent 74be2a5052
commit d046be5e52
23 changed files with 1640 additions and 1 deletions

View File

@@ -246,6 +246,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/gfx/types.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/gfx/vertex_buffer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/background_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/particle_validation_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/circle_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/custom_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/fill_extrusion_layer_factory.hpp
@@ -357,6 +358,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/style/layer_properties.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/background_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/particle_validation_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/circle_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/custom_layer_host.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/custom_layer_init_parameters.hpp
@@ -501,6 +503,7 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/gfx/upload_pass.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/gfx/vertex_vector.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/background_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/particle_validation_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/circle_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_extrusion_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_layer_factory.cpp
@@ -578,6 +581,8 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/image_manager_observer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_background_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_background_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_particle_validation_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_particle_validation_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_circle_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_circle_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_fill_extrusion_layer.cpp
@@ -747,6 +752,8 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/style/layer_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layer_observer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/particle_validation_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/particle_validation_layer_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_impl.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_properties.cpp

View File

@@ -64,6 +64,7 @@ MLN_GENERATED_STYLE_SOURCE = [
"src/mbgl/style/layers/color_relief_layer.cpp",
"src/mbgl/style/layers/background_layer_properties.cpp",
"src/mbgl/style/layers/background_layer.cpp",
"src/mbgl/style/layers/particle_validation_layer.cpp",
"src/mbgl/style/layers/location_indicator_layer_properties.cpp",
"src/mbgl/style/layers/location_indicator_layer.cpp",
"src/mbgl/style/light.cpp",
@@ -162,6 +163,7 @@ MLN_CORE_SOURCE = [
"src/mbgl/gfx/upload_pass.hpp",
"src/mbgl/gfx/vertex_vector.hpp",
"src/mbgl/layermanager/background_layer_factory.cpp",
"src/mbgl/layermanager/particle_validation_layer_factory.cpp",
"src/mbgl/layermanager/circle_layer_factory.cpp",
"src/mbgl/layermanager/custom_layer_factory.cpp",
"src/mbgl/layermanager/fill_extrusion_layer_factory.cpp",
@@ -233,6 +235,8 @@ MLN_CORE_SOURCE = [
"src/mbgl/renderer/image_manager_observer.hpp",
"src/mbgl/renderer/layers/render_background_layer.cpp",
"src/mbgl/renderer/layers/render_background_layer.hpp",
"src/mbgl/renderer/layers/render_particle_validation_layer.cpp",
"src/mbgl/renderer/layers/render_particle_validation_layer.hpp",
"src/mbgl/renderer/layers/render_circle_layer.cpp",
"src/mbgl/renderer/layers/render_circle_layer.hpp",
"src/mbgl/renderer/layers/render_custom_layer.cpp",
@@ -406,6 +410,7 @@ MLN_CORE_SOURCE = [
"src/mbgl/style/layer_observer.hpp",
"src/mbgl/style/layers/background_layer_impl.cpp",
"src/mbgl/style/layers/background_layer_impl.hpp",
"src/mbgl/style/layers/particle_validation_layer_impl.hpp",
"src/mbgl/style/layers/circle_layer_impl.cpp",
"src/mbgl/style/layers/circle_layer_impl.hpp",
"src/mbgl/style/layers/custom_layer.cpp",
@@ -660,6 +665,7 @@ MLN_CORE_HEADERS = [
"include/mbgl/i18n/collator.hpp",
"include/mbgl/i18n/number_format.hpp",
"include/mbgl/layermanager/background_layer_factory.hpp",
"include/mbgl/layermanager/particle_validation_layer_factory.hpp",
"include/mbgl/layermanager/circle_layer_factory.hpp",
"include/mbgl/layermanager/custom_layer_factory.hpp",
"include/mbgl/layermanager/fill_extrusion_layer_factory.hpp",
@@ -673,6 +679,7 @@ MLN_CORE_HEADERS = [
"include/mbgl/layermanager/location_indicator_layer_factory.hpp",
"include/mbgl/layermanager/raster_layer_factory.hpp",
"include/mbgl/layermanager/symbol_layer_factory.hpp",
"include/mbgl/style/layers/particle_validation_layer.hpp",
"include/mbgl/map/bound_options.hpp",
"include/mbgl/map/camera.hpp",
"include/mbgl/map/change.hpp",

View File

@@ -0,0 +1,731 @@
# MapLibreParticleLab 30_最小技术验证-01_Core RenderLayer 接入验证指令书 v1
## 1. 任务基本信息
- 项目:MapLibreParticleLab
- 阶段:`30_最小技术验证`
- 子任务:`30-01 Core RenderLayer 接入验证`
- 当前主线:`particle/main`
- 前置阶段:
- `10_架构调查` 已完成
- `20_架构设计` 已完成
- 前置正式架构文档:
- `docs/10_架构/20_粒子系统总体架构设计_v1.md`
- 任务性质:最小运行时技术验证
- 本任务不是正式粒子功能实现
- 本任务允许最少量验证代码,但必须保持隔离、可识别、可移除
- 不允许顺手重构 MapLibre
- 不允许扩大到 GPU 粒子、真实场数据、Trail 或 Land/Ocean Mask 实现
---
## 2. 为什么本阶段先只做 30-01
当前总体架构已经选择独立 Core 粒子 Layer 作为正式主路线。
本阶段后续原计划还需要验证:
1. Core RenderLayer / Style order / 同帧 transform / repaint
2. GPU State A/B + Trail A/B ping-pong 与 context recreation
3. Camera / world wrap / Land-Ocean Mask
4. iOS Core Layer 薄接入
但第 1 项属于前置门槛。
如果第 1 项失败,说明当前正式主路线可能需要调整,后续 GPU、Mask 和平台验证的代码位置、生命周期和验证方式都会变化。
因此本任务只完成 `30-01`。
只有 `30-01` 得出 PASS 后,才进入下一项。
不得提前为后续验证大量写代码。
---
## 3. 必须先读取
Codex 开始前必须完整读取:
1. 根目录 `AGENTS.md`
2. `PARTICLE_BASELINE.md`
3. `docs/00_项目总则/` 下当前全部有效文档
4. `docs/00_项目规范/` 下当前全部有效文档
5. `docs/20_调查/10_架构调查报告_v1.md`
6. `docs/10_架构/20_粒子系统总体架构设计_v1.md`
7. `docs/报告/10_架构调查_Codex完成报告_v1.md`
8. `docs/报告/20_架构设计_Codex完成报告_v1.md`
9. `粒子开发记录.md`
10. 与下列现有实现直接相关的当前源码:
- Style Layer 注册链
- RenderLayer / RenderTree / RenderOrchestrator
- RasterLayer 或其他最接近的现有 Layer 示例
- CustomLayer / CustomDrawableLayer 示例
- Android MapRenderer / AndroidRendererFrontend
- `triggerRepaint` / `requestRender`
- `TransformState` / `TransformParameters` / `PaintParameters`
不得凭记忆或 MapLibre 通用知识直接开写。
---
## 4. 开始前 Git 检查
先执行只读检查并记录:
- 当前分支
- 当前 HEAD
- `git status`
- `origin` / 当前公开远端
- `particle/main` 是否与远端同步
如果存在与本任务无关的本地修改:
- 不得删除
- 不得覆盖
- 不得顺带提交
- 在完成报告中记录
- 本任务提交时只 stage 本任务相关文件
如果当前不是 `particle/main`,不要擅自切换覆盖用户工作。
先明确报告实际状态。
---
## 5. 验证目标
本任务只回答下面四个问题。
### V1. 独立 Core Layer 能否进入真实 MapLibre Style / RenderTree 顺序
必须验证一个最小实验 Layer:
- 由 Core Style/Layer 系统识别
- 能生成对应 RenderLayer
- 能进入真实 RenderTree
- 能按 Style Layer 顺序参与渲染
- 不依赖 MapView 外部 Overlay
- 不依赖 JS/RN 每帧逻辑
- 不把实现塞进现有 RasterLayer
### V2. 该 Layer 能否在同一 MapLibre render frame 获得当前 Transform
至少验证能够在真正绘制路径中读取或使用:
- viewport size
- zoom
- bearing
- pitch
- projection / transform matrix 中架构要求的必要部分
验证重点不是把数值打印出来,而是确认这些值来自:
> 当前 MapLibre render frame 的真实 `PaintParameters / TransformParameters / TransformState` 链
而不是另建独立 Camera 状态。
### V3. Layer 活动时能否驱动持续 repaint
验证:
- Layer 处于“动画活动”状态时,可以通过 MapLibre 自己的 render loop 持续获得下一帧
- 不建立独立于 MapLibre 的第二套动画循环
- 不依赖 Android UI Timer
- 不依赖 JS `requestAnimationFrame`
### V4. Layer 停止活动后能否停止额外 repaint
验证:
- Layer 切换为静止/disabled 后,不继续无意义请求下一帧
- 地图自身正常 pan/zoom/render 不受影响
- 不产生永久 continuous rendering
---
## 6. 本任务不是粒子实现
验证代码只能证明架构入口成立。
禁止在本任务实现:
- 速度场
- U/V Grid
- Field decode
- particle position
- age
- seed
- advection
- velocity texture
- particle state texture
- ping-pong
- Trail
- fade pass
- Land/Ocean Mask
- Wave/Tide/Current
- Wind fixture
- 真正 Particle Shader
- compute shader
如需要视觉确认,只允许使用最简单的验证图元或调试绘制。
例如:
- 单色矩形
- 三角形
- 简单全屏/局部 quad
- 简单随帧变化的 alpha / position
不得把验证图元包装成“粒子初版”。
---
## 7. 验证代码命名
验证代码必须明确表明其临时技术验证属性。
优先使用:
`ParticleValidation*`
或:
`RasterParticleValidation*`
不要直接把不成熟的验证代码命名成最终生产类并假装已经冻结。
如果必须使用正式架构类名才能验证注册链,例如:
- `RasterParticleLayer`
- `RenderRasterParticleLayer`
则必须:
1. 只实现验证所需最小骨架;
2. 在代码和文档中明确 `VALIDATION ONLY`;
3. 不提前加入未来未验证业务接口;
4. 不把临时调试字段伪装成正式公共 API。
---
## 8. 最小实现范围
Codex 应先调查当前最小实现需要触及哪些文件。
原则:
> 能用 5 个文件解决,不要改 15 个文件。
预计可能涉及,但不得机械照搬:
- Core style layer 声明
- Core style layer 实现
- 对应 RenderLayer
- LayerManager / 注册位置
- Android Test App 或现有测试入口
- 最小验证日志/状态
最终以当前真实代码为准。
不得为了“以后可能用到”提前创建整套 particle 目录树。
---
## 9. Style / RenderTree 验证要求
必须证明实验 Layer 是通过正式 MapLibre Layer 链进入渲染。
验证报告至少记录实际链:
```text
Style Layer
→ LayerManager / RenderLayer creation
→ RenderTree / RenderOrchestrator
→ 对应 RenderLayer
→ Render pass
```
必须指出:
- 实际插入哪个 pass
- 与前后普通 Style Layer 的 order 是否正确
- 是否需要 tile/source
- 当前验证 Layer 是否 source-less
- 如果 source-less 不可行,实际最小 Source 依赖是什么
不能只通过 CustomLayer 示例证明。
本任务要验证的是总体架构选择的“独立 Core Layer 路线”。
---
## 10. 同帧 Camera / Matrix 验证
验证代码必须从当前 render path 内取得 transform。
至少记录:
- 获取位置
- 对应源码对象
- 生命周期
- 是否每 frame 更新
- 是否与当前 RenderTree frame 同步
至少验证以下地图变化不会让实验 Layer 明显脱离地图:
- pan
- zoom
- bearing
- pitch
- resize
本任务不要求完成 world wrap / antimeridian 专项验证。
这些留给后续 Mask/Camera 技术验证。
---
## 11. Repaint 验证
实验 Layer 必须提供最小“活动/停止”活动模型。
例如内部验证状态:
```text
ACTIVE
STATIC
```
### ACTIVE
必须证明:
- 当前 frame 完成后仍会请求 MapLibre 下一帧
- 连续帧来自 MapLibre render/repaint 链
- 能观察到 frame counter 持续增长
### STATIC
必须证明:
- 不再由该 Layer 主动触发下一帧
- 地图静止时 frame counter 不继续无意义增长
- 用户操作地图时仍能正常重新绘制
不得使用平台 Timer 模拟。
---
## 12. 日志要求
验证期间允许增加最少量调试日志。
日志至少能识别:
- Layer create
- RenderLayer create
- first render
- frame counter
- current active/static
- repaint request
- relevant transform snapshot
- destroy/deinitialize
禁止每帧输出大量完整 matrix 导致日志爆炸。
建议:
- 首帧输出一次完整关键参数
- 后续每 N 帧输出 frame counter
- 状态变化时输出
- 销毁时输出
验证完成后是否保留日志,由报告明确说明。
---
## 13. Android 第一验证平台
本任务第一运行平台固定为:
**Android OpenGL**
原因:
- 当前架构设计将它作为第一共同验证入口;
- 本任务只验证 Core Layer + 同帧 transform + repaint;
- 不在本任务同时扩大到 Vulkan / iOS。
Vulkan 和 iOS 留到后续对应验证。
---
## 14. Android 编译规则:必须由用户手工执行
本项目 Android 编译必须由用户手工执行。
Codex 禁止运行:
- Gradle Android build
- `./gradlew assemble...`
- `./gradlew install...`
- Android npm build
- 任何等价 Android 编译命令
Codex 可以:
- 检查现有 Gradle 配置
- 准备代码
- 给出用户应执行的准确编译命令
- 分析用户返回的编译错误
- 在允许范围内修正本任务代码
Codex 不得代替用户执行 Android 编译。
---
## 15. 运行/设备验证规则
本任务不得默认替用户执行:
- ADB 安装
- ADB 启动
- ADB 日志抓取
- 真机操作
除非用户在后续明确授权。
因此第一轮 Codex 工作完成后,应输出:
1. 用户手工编译命令
2. 应打开的 Test App / 页面 / Activity
3. 需要执行的地图操作
4. 需要观察的日志
5. PASS/FAIL 判定方法
用户提供编译/运行结果后,再完成最终技术验证结论。
---
## 16. 两阶段执行方式
本任务允许同一份指令书分两次 Codex 执行。
### 阶段 A:验证代码准备
Codex:
1. 读取基线
2. 写最小验证代码
3. 做静态检查
4. 生成暂定验证报告
5. 给出用户手工编译/运行步骤
6. commit + push 到 `particle/main`
此时报告结论必须写:
`WAITING_MANUAL_VALIDATION`
不得写 PASS。
### 阶段 B:用户返回实际编译/运行结果后
Codex:
1. 读取用户提供的编译结果、日志和现象
2. 如果失败,只修复本验证范围内问题
3. 如果架构假设本身失败,停止,不强行修成另一套架构
4. 更新正式验证报告
5. 更新 Codex 完成报告
6. 更新 `粒子开发记录.md`
7. commit + push 到 `particle/main`
最终只能是:
- `PASS`
- `FAIL`
- `BLOCKED`
---
## 17. PASS 标准
只有同时满足以下条件,`30-01` 才能 PASS:
- [ ] 独立 Core Style Layer 被正式识别
- [ ] 对应 RenderLayer 被创建
- [ ] 真正进入 RenderTree / Render pass
- [ ] Layer order 正确
- [ ] 从当前 render frame 获得必要 Transform
- [ ] pan 正常
- [ ] zoom 正常
- [ ] bearing 正常
- [ ] pitch 正常
- [ ] resize 正常
- [ ] ACTIVE 时由 MapLibre render loop 持续 repaint
- [ ] STATIC 时停止该 Layer 的额外 repaint
- [ ] 地图普通渲染不受破坏
- [ ] 未使用外部 Overlay
- [ ] 未建立平台 Timer 动画循环
- [ ] 未实现超出本任务范围的粒子系统
任何一项关键门槛失败,不得标 PASS。
---
## 18. FAIL 与停止条件
出现以下任一情况时,应停止继续向后实现:
### F1
独立 Core Layer 无法在不破坏 MapLibre 现有主架构的情况下进入真实 RenderTree。
### F2
无法从该 Layer 的真实 render frame 获得足够 transform。
### F3
必须依赖外部 Overlay 或独立动画 loop 才能工作。
### F4
持续 repaint 必须通过明显侵入 Map 主循环才能实现。
### F5
验证要求迫使 Android/iOS 分别拥有不同 particle engine。
若触发 F1-F5:
- 停止
- 不继续 GPU ping-pong
- 不继续 Mask
- 不继续 iOS 实现
- 报告失败事实
- 回到架构设计重新决策
不得为了让验证“通过”扩大修改范围。
---
## 19. 禁止事项
本任务禁止:
- 正式实现 Particle Field
- 正式实现 RasterArraySource 数据链
- 正式实现 GPU Simulation
- 正式实现 Trail
- 正式实现 Mask
- 接真实天气数据
- 引入新第三方依赖
- 升级 MapLibre
- 升级 Android SDK / NDK / Gradle
- 修改无关 Layer
- 重构 Renderer
- 重构 Map
- 修改 iOS 代码
- 实现 Vulkan 版本
- 建立 RN/JS bridge
- 新增产品 API
- 清理与本任务无关的旧代码
---
## 20. 文档输出
### 20.1 技术验证报告
生成:
`docs/60_测试/30_01_CoreRenderLayer接入最小技术验证报告_v1.md`
如果同主题已有文件,按版本递增,不覆盖旧文件。
报告必须区分:
- 静态确认
- 用户手工编译结果
- 用户运行结果
- 实际日志证据
- 推论
没有用户运行结果时,只能写:
`WAITING_MANUAL_VALIDATION`
### 20.2 Codex 完成报告
生成:
`docs/报告/30_01_CoreRenderLayer接入验证_Codex完成报告_v1.md`
同主题已有报告时递增版本号。
### 20.3 开发记录
追加:
`粒子开发记录.md`
不得覆盖历史内容。
---
## 21. 不主动创建 Step
本任务不主动创建或修改 `docs/50_Step记录/`。
只有用户明确要求时才创建 Step。
---
## 22. Git 收尾规则
本任务凡产生文件修改,都必须提交并 push。
目标:
- 本地主线:`particle/main`
- 公开远端对应分支:`particle/main`
### 22.1 提交范围
只允许提交:
- 本任务验证代码
- 本任务验证文档
- 本任务完成报告
- 本任务对 `粒子开发记录.md` 的追加
不得把与本任务无关的本地修改一起提交。
### 22.2 阶段 A
验证代码准备完成后:
1. 检查 `git diff`
2. 明确列出本任务文件
3. commit
4. push 到公开 `particle/main`
5. 确认远端可见
推荐提交信息:
`test: add core particle render layer validation`
### 22.3 阶段 B
用户手工验证结果回填后:
1. 更新验证报告
2. 更新完成报告
3. 更新开发记录
4. 如有必要修复最小验证代码
5. commit
6. push 到公开 `particle/main`
推荐提交信息:
`test: finalize core particle render layer validation`
### 22.4 Push 失败
如果 push 失败:
- 不得声称任务完整完成
- 完成报告必须写明:
- push 失败
- 错误原因
- 本地 commit hash
- 目标远端
- 目标分支
---
## 23. Git 信息记录
Codex 完成报告必须记录:
- 任务开始 HEAD
- 本次主要变更 commit hash
- commit message
- push 是否成功
- 公开远端
- 远端分支
- push 后远端可见状态
- 是否存在未提交的无关本地改动
为避免“提交文件记录自己的 commit hash”产生自引用问题:
- 完成报告中的 `主要变更 commit hash` 记录包含验证代码和主要文档的实质性提交;
- 如果随后还有仅用于回填报告元数据的收尾提交,Codex 最终回复中必须另外报告最终远端 HEAD;
- 不得为了回填当前提交自己的 hash 无限 amend。
---
## 24. Codex 完成报告必须逐项说明
至少包括:
1. 前置文档读取
2. 当前 Git 基线
3. 实际修改文件
4. Style Layer 接入位置
5. RenderLayer 创建链
6. RenderTree / pass
7. Transform 获取链
8. Repaint 链
9. Android OpenGL 验证入口
10. 用户手工编译命令
11. 用户手工运行步骤
12. 当前状态:
- WAITING_MANUAL_VALIDATION
- PASS
- FAIL
- BLOCKED
13. 是否修改生产代码
14. 是否扩大架构范围
15. 是否安装/升级依赖
16. Git commit
17. push 结果
18. 是否可以进入下一个最小技术验证
---
## 25. 进入下一验证的条件
只有最终结果:
`PASS`
才允许进入下一个技术验证。
下一项预计验证:
> GPU State A/B + Trail A/B 的 gfx ping-pong、RenderTarget、同步/barrier 与 context recreation。
如果本任务是:
- FAIL
- BLOCKED
则不得提前执行下一项。
必须先回到架构层决定如何处理。
---
## 26. 本任务完成定义
`30-01` 真正完成必须满足:
> 用户手工 Android OpenGL 编译和运行后,已经通过实际运行证据确认:独立 Core Layer 可以进入 MapLibre 正式 Style/RenderTree 渲染链,在同一 render frame 使用 MapLibre 当前 Transform,并通过 MapLibre 自己的 repaint 机制在活动时持续渲染、停止时停止额外帧。
只有静态代码完成、没有实际运行结果,不叫验证完成。
只能标记:
`WAITING_MANUAL_VALIDATION`

View File

@@ -0,0 +1,193 @@
# 30-01 Core RenderLayer 接入最小技术验证报告 v1
日期:2026-08-29
当前状态:`WAITING_MANUAL_VALIDATION`
本报告只记录阶段 A 的代码准备与静态证据。Android OpenGL 编译、安装、运行、手势和日志结果尚未由用户回填,因此不得标记 PASS。
## 1. 验证范围
本验证只回答:独立 Core Style Layer/RenderLayer 是否能进入正式 RenderTree 顺序、读取同帧 transform、在 ACTIVE 时通过 MapLibre 自身 loop 请求下一帧,并在 STATIC 时停止额外 repaint。
未实现速度场、粒子状态、advection、GPU ping-pong、Trail、Mask、真实天气数据或 compute shader。
## 2. 最小验证实现
- Style type:`particle-validation`。
- Style class:`style::ParticleValidationLayer`,明确标记 `VALIDATION ONLY`。
- Render class:`RenderParticleValidationLayer`。
- Source:不要求 source;`LayerTypeInfo::Source::NotRequired`。
- Pass:`RenderPass::Translucent`。
- 状态:style paint 字段 `particle-validation-active=true/false`,只用于本验证。
- Drawable:一个不输出像素的验证 Drawable,用于进入真实 LayerGroup/pass 顺序并在 drawable tweaker 中取得当前 `PaintParameters`;不使用 `CustomLayerHost`,不构成外部 CustomLayer 路线。
- Android API:没有新增 Java/JNI 产品 API;Android 只把该类型注册为 JSON style 可用的 core-only layer。
## 3. Style / RenderTree / pass 静态确认
实际链路为:
```text
Android Test App inline Style JSON
→ style::conversion::Converter<std::unique_ptr<Layer>>
→ LayerManagerAndroid::getFactory("particle-validation")
→ ParticleValidationLayerFactory::createLayer
→ style::ParticleValidationLayer immutable Impl
→ RenderOrchestrator layer diff/order
→ LayerManagerAndroid::createRenderLayer
→ ParticleValidationLayerFactory::createRenderLayer
→ RenderParticleValidationLayer
→ source-less LayerRenderItem
→ RenderParticleValidationLayer::update
→ source-less LayerGroup(layerIndex)
→ validation Drawable(RenderPass::Translucent)
→ main translucent pass 的 LayerGroup 顺序
```
Test App style 顺序固定为:
```text
validation-background
→ validation-below(蓝色大圆)
→ particle-validation-core
→ validation-above(黄色小圆)
```
验证 Drawable 的 `layerIndex` 由 RenderOrchestrator 正式 style order 分配,LayerGroup 以同一 index 注册。运行日志同时记录 `layerIndex` 和 pass 内的 `currentLayer`。实际 Android 顺序仍需用户运行日志确认。
## 4. 同帧 Transform 静态确认
验证日志在 DrawableTweaker 的 `execute(gfx::Drawable&, PaintParameters&)` 中生成。该回调发生在 MapLibre main translucent pass 对应 LayerGroup/Drawable 的真实执行路径中,直接读取:
- `PaintParameters::state.getSize()`:viewport;
- `PaintParameters::state.getZoom()`:zoom;
- `PaintParameters::state.getBearing()`:bearing;
- `PaintParameters::state.getPitch()`:pitch;
- `PaintParameters::transformParams.projMatrix`:当前 frame projection matrix 摘要;
- `PaintParameters::frameCount`:MapLibre frame number。
未创建独立 Camera 状态,未从 Android UI 层复制或推算 transform。
## 5. Repaint 链静态确认
本任务增加一个薄 Core hook:
```text
RenderParticleValidationLayer::needsRepaint()
→ RenderOrchestrator::hasTransitions() 汇总
→ RenderTreeParameters::needsRepaint
→ RendererObserver::onDidFinishRenderingFrame(needsRepaint)
→ Map::Impl::onDidFinishRenderingFrame
→ needsRepaint=true 时 Map::Impl::onUpdate()
→ AndroidRendererFrontend::update()
→ Android MapRenderer.requestRender()
→ 下一 MapLibre render frame
```
所有其他 RenderLayer 默认返回 false,因此现有 Layer 行为不变。
- ACTIVE:验证 Layer 可见且 active 时返回 true。
- STATIC:active=false 时返回 false;地图自己的 transition、手势和 source 更新仍可请求 frame。
- 未使用 Android Timer、Handler 循环、JS `requestAnimationFrame` 或第二套动画 loop。
## 6. 日志格式
日志关键字统一为 `ParticleValidation`,包括:
- Style Layer create/destroy;
- RenderLayer create/destroy;
- ACTIVE/STATIC;
- validation Drawable create/destroy;
- 首帧及每 60 帧:frame counter、MapLibre frame、状态、repaint、pass、layer order、viewport、zoom、bearing、pitch、projection matrix 摘要。
预期 ACTIVE 样例:
```text
ParticleValidation: render frame=60 mapFrame=... state=ACTIVE repaint=requested pass=translucent layerIndex=... currentLayer=... viewport=... zoom=... bearing=... pitch=... proj=[...]
```
预期 STATIC 样例:
```text
ParticleValidation: state=STATIC ...
ParticleValidation: render frame=1 ... state=STATIC repaint=stopped ...
```
地图静止后不应继续出现该 STATIC 实例的周期性 `frame=60/120/...`。
## 7. Codex 已执行检查
### 7.1 通过
- `git diff --check`:PASS。
- 新增/修改 C++ 文件 `clang-format`:PASS。
- Android Manifest、layout、values XML 使用 Python 标准库只读解析:PASS。
- Android `MapLibreMap.getCameraPosition/setCameraPosition` API 静态确认:PASS。
- Linux OpenGL CMake 重新配置:PASS。
- 精确编译以下 Linux Core 对象:PASS:
- `particle_validation_layer_factory.cpp.o`
- `particle_validation_layer.cpp.o`
- `render_particle_validation_layer.cpp.o`
- `render_orchestrator.cpp.o`
- default `layer_manager.cpp.o`
### 7.2 未完成
- Linux OpenGL 全量增量编译:公共 `RenderLayer` 头变化触发 321 个对象重编译;在 `41/321` 无错误时主动停止,随后改为上述任务相关对象精确编译并通过。
- Android OpenGL Gradle/NDK/Test App 编译:按指令书禁止 Codex 执行,等待用户手工完成。
- Android 运行、日志、pan/zoom/bearing/pitch/resize、ACTIVE/STATIC:等待用户。
## 8. 用户手工编译
在项目 Linux 环境执行:
```bash
cd /mnt/sda1/MapLibreParticleLab
source /root/.maplibre-particle-env
cd platform/android
RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64
```
预期 APK:
```text
platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk
```
若目标设备不是 x86_64,请将 `android-x86-64` 换成对应现有 Make target;不要切换 Vulkan,本任务平台固定 Android OpenGL。
## 9. 用户手工运行步骤
1. 安装并打开 `MapLibre Native Android TestApp`。
2. 在 `Custom Layer` 分类打开 `Particle Core Layer Validation`。
3. 确认页面显示蓝色大圆和其上的黄色小圆,初始状态文字为 ACTIVE。
4. 过滤日志关键字:`adb logcat | grep ParticleValidation`;也可使用 Android Studio Logcat 搜索同一关键字。
5. ACTIVE 静置至少 3 秒,确认 `frame=60/120/...` 持续增长。
6. 分别执行 pan、双指 zoom、rotate、pitch,确认地图正常,并保存变化前后的 transform 日志。
7. 旋转设备或改变模拟器窗口尺寸,确认 `viewport` 更新且页面正常。
8. 点击 STATIC,静置至少 3 秒;确认不再周期性增长。
9. STATIC 下再次 pan/zoom,确认地图仍会按正常地图事件重绘;操作停止后不继续产生验证层额外帧。
10. 再点 ACTIVE,确认连续帧恢复。
## 10. PASS / FAIL 判定
### PASS 候选证据
- Style Layer、RenderLayer 和 validation Drawable 均创建;
- 日志显示 `pass=translucent` 且 layer order 与 style 顺序一致;
- viewport/zoom/bearing/pitch/projection 摘要随对应操作更新;
- ACTIVE frame counter 持续增长;
- STATIC 静止后停止周期增长,但地图手势仍正常;
- 无崩溃、黑屏、永久 continuous rendering 或普通地图破坏。
### FAIL/停止
若 layer 无法创建、无法进入 pass、transform 不是同帧、ACTIVE 无法持续、STATIC 无法停止,或必须改用 Overlay/Timer/大改 Map 主循环,则记录 FAIL 并停止后续 GPU/Mask/iOS 验证。
## 11. 当前结论
静态代码和 Linux 任务相关对象编译支持该路线具备可验证性,但缺少 Android OpenGL 实际编译与运行证据。
当前唯一合法结论:`WAITING_MANUAL_VALIDATION`。
不得进入下一项 GPU State A/B + Trail A/B 验证。

View File

@@ -0,0 +1,141 @@
# 30-01 Core RenderLayer 接入验证 Codex 完成报告 v1
日期:2026-08-29
任务:`docs/40_任务/30_01_CoreRenderLayer接入验证指令书_v1.md`
执行阶段:A(验证代码准备)
当前结论:`WAITING_MANUAL_VALIDATION`
## 1. 前置文档读取
已读取根 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目总则/` 全部当前有效文件、`docs/00_项目规范/` 全部当前有效文件、架构调查报告、总体架构设计、两份前置 Codex 报告、开发记录、当前 STEP-000 和本任务书。已按任务要求调查 Style 注册、RenderLayer/RenderTree/RenderOrchestrator、Background/Raster/Custom 示例、Android renderer/frontend、repaint 和 Transform/Paint 链。
## 2. Git 基线
- 分支:`particle/main`。
- 任务开始 HEAD:`74be2a5052428717d7c9e133c333da8bd783b958`。
- 开始时远端同步:`particle/main...origin/particle/main = 0 0`。
- 开始时无已跟踪修改;有用户提供的未跟踪当前任务书。
- submodule:已查询,无本任务修改。
- 主要变更 commit:阶段 A 实质提交后回填。
- commit message:`test: add core particle render layer validation`。
- push:阶段 A 实质提交后回填。
- 公开远端:`ssh://git@192.168.200.230:2222/tei/MapLibreParticleLab.git`。
- 远端分支:`particle/main`。
## 3. 实际修改文件
### Core 验证代码
- 新增 `ParticleValidationLayer`、Impl/properties、factory、`RenderParticleValidationLayer`。
- 修改 Core/default 与 Android LayerManager 注册。
- 修改 `RenderLayer` 增加默认 false 的 `needsRepaint()`。
- 修改 `RenderOrchestrator` 汇总 Layer repaint 请求。
- 修改 CMake/Bazel source lists。
### Android Test App
- 新增 `ParticleCoreLayerValidationActivity` 和 layout。
- 注册 Manifest。
- 新增页面标题、说明和 ACTIVE/STATIC 文案。
### 文档
- 纳入当前正式任务书。
- 新增技术验证报告和本完成报告。
- 追加 `粒子开发记录.md`。
- 未创建或修改 Step。
## 4. Style Layer 接入位置
JSON type `particle-validation` 经 Android `LayerManagerAndroid` 的 core-only factory 正式解析为 `style::ParticleValidationLayer`。其 `LayerTypeInfo` 为 source-less、无 layout、无 tile kind。它不是 RasterLayer 分支,也不是 MapView 外部 Overlay。
## 5. RenderLayer 创建链
```text
Style JSON → LayerManagerAndroid → ParticleValidationLayerFactory
→ ParticleValidationLayer immutable Impl
→ RenderOrchestrator layer diff
→ ParticleValidationLayerFactory::createRenderLayer
→ RenderParticleValidationLayer
```
## 6. RenderTree / pass
`RenderParticleValidationLayer` 进入 source-less `LayerRenderItem`,按 style index 创建 source-less LayerGroup,并注册一个无像素输出的 validation Drawable。Drawable 属于 `RenderPass::Translucent`,随正常 LayerGroup order 进入 main translucent pass。Test App 将它固定放在蓝色参考圆与黄色参考圆之间。
## 7. Transform 获取链
```text
UpdateParameters::TransformState
→ RenderTreeParameters::TransformParameters
→ 当前 frame PaintParameters
→ validation DrawableTweaker::execute
```
读取 viewport、zoom、bearing、pitch、projection matrix 摘要和 MapLibre frame count。没有独立 Camera 状态。
## 8. Repaint 链
ACTIVE 的 `RenderParticleValidationLayer::needsRepaint()` 返回 true,由 RenderOrchestrator 写入 RenderTree `needsRepaint`,再经 RendererObserver、Map::Impl::onDidFinishRenderingFrame/onUpdate 和 AndroidRendererFrontend.requestRender 请求下一 MapLibre frame。STATIC 返回 false。没有 Timer、Handler loop、JS/RN frame loop。
## 9. Android OpenGL 验证入口
Test App → `Custom Layer` 分类 → `Particle Core Layer Validation`。虽然位于现有 UI 分类中,但实现不是 CustomLayer/CustomLayerHost;分类只用于减少 Test App 导航改动。
## 10. 用户手工编译命令
```bash
cd /mnt/sda1/MapLibreParticleLab
source /root/.maplibre-particle-env
cd platform/android
RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64
```
APK 预期路径:`platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。
## 11. 用户手工运行步骤
详见技术验证报告第 9 节。核心操作是:打开验证页面;保存 `ParticleValidation` 日志;验证 pan/zoom/rotate/pitch/resize;ACTIVE 静置观察连续 frame;STATIC 静置观察停止;STATIC 下操作地图确认正常重绘。
## 12. 构建与检查记录
- Android/Gradle/npm/ADB:未执行,遵循任务书禁止项。
- Linux OpenGL CMake configure:PASS。
- Linux OpenGL 全量增量:在 `41/321` 无错误时主动停止,原因是公共头变化导致完整 Core 重编译;未宣称全量 PASS。
- 本任务新增/直接修改的 5 个 Linux Core 对象精确编译:PASS。
- C++ clang-format、`git diff --check`:PASS。
- Android XML 标准库解析:PASS。
- APK/AAR:本轮未生成新产物。
## 13. 当前状态
`WAITING_MANUAL_VALIDATION`。
用户尚未提供 Android OpenGL 编译、运行、手势、resize、ACTIVE/STATIC 和日志证据,不能写 PASS。
## 14. 是否修改生产代码
是,但只包含验证所需薄接入:新增隔离的 VALIDATION ONLY Core Layer/RenderLayer、注册/build list,以及通用默认 false repaint hook。没有粒子生产功能;没有改 RasterLayer、Map、MapView 主循环或 backend 算法。
## 15. 是否扩大架构范围
否。未实现 GPU particle、Field、RasterArraySource、Trail、Mask、Vulkan、iOS、RN/JS bridge 或真实数据。
## 16. 依赖状态
未安装、未升级、未修改任何依赖、lock、SDK、NDK、Gradle、Java、Node 或 MapLibre baseline。
## 17. 已知问题与未完成
- Android OpenGL 编译和运行待用户。
- 五项地图变化和 ACTIVE/STATIC 行为待实际证据。
- 全量 Linux OpenGL 增量未完成;任务相关对象已编译通过。
- 验证 Drawable故意不输出像素;以 pass/order/transform/repaint 日志为技术证据,蓝/黄参考圆用于确认地图与 style layer context。
- commit/push 元数据将在提交后回填一次,不循环 amend。
## 18. 是否可以进入下一验证
否。只有用户证据回填并将本任务最终结论更新为 PASS 后,才能进入 GPU State A/B + Trail A/B 验证。

View File

@@ -0,0 +1,16 @@
#pragma once
#include <mbgl/layermanager/layer_factory.hpp>
namespace mbgl {
// VALIDATION ONLY: factory for the 30-01 Core RenderLayer experiment.
class ParticleValidationLayerFactory final : public LayerFactory {
public:
const style::LayerTypeInfo* getTypeInfo() const noexcept override;
std::unique_ptr<style::Layer> createLayer(const std::string&,
const style::conversion::Convertible&) noexcept override;
std::unique_ptr<RenderLayer> createRenderLayer(Immutable<style::Layer::Impl>) noexcept override;
};
} // namespace mbgl

View File

@@ -0,0 +1,32 @@
#pragma once
#include <mbgl/style/layer.hpp>
namespace mbgl {
namespace style {
// VALIDATION ONLY: minimal source-less Core layer for 30-01 architecture validation.
class ParticleValidationLayer final : public Layer {
public:
explicit ParticleValidationLayer(const std::string& layerID);
~ParticleValidationLayer() override;
bool isActive() const;
void setActive(bool);
class Impl;
const Impl& impl() const;
Mutable<Impl> mutableImpl() const;
private:
explicit ParticleValidationLayer(Immutable<Impl>);
std::optional<conversion::Error> setPropertyInternal(const std::string&, const conversion::Convertible&) override;
StyleProperty getProperty(const std::string&) const override;
Value serialize() const override;
std::unique_ptr<Layer> cloneRef(const std::string& id) const override;
Mutable<Layer::Impl> mutableBaseImpl() const override;
};
} // namespace style
} // namespace mbgl

View File

@@ -19,12 +19,15 @@
#include "symbol_layer.hpp"
#include "fill_extrusion_layer.hpp"
#include "location_indicator_layer.hpp"
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
namespace mbgl {
namespace android {
LayerManagerAndroid::LayerManagerAndroid() {
// VALIDATION ONLY: JSON style access; intentionally no Android product API/JNI peer.
addLayerTypeCoreOnly(std::make_unique<ParticleValidationLayerFactory>());
#if defined(MBGL_LAYER_FILL_DISABLE_RUNTIME)
addLayerTypeCoreOnly(std::make_unique<FillLayerFactory>());
#elif !defined(MBGL_LAYER_FILL_DISABLE_ALL)

View File

@@ -286,6 +286,18 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.particle.ParticleCoreLayerValidationActivity"
android:description="@string/description_particle_core_layer_validation"
android:exported="true"
android:label="@string/activity_particle_core_layer_validation">
<meta-data
android:name="@string/category"
android:value="@string/category_custom_layer" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.annotation.PolygonActivity"
android:description="@string/description_polygon"

View File

@@ -0,0 +1,93 @@
package org.maplibre.android.testapp.activity.particle
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.testapp.R
/** VALIDATION ONLY: Android OpenGL host for the 30-01 Core RenderLayer experiment. */
class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var maplibreMap: MapLibreMap
private lateinit var status: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_particle_core_layer_validation)
mapView = findViewById(R.id.mapView)
status = findViewById(R.id.validationStatus)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map ->
maplibreMap = map
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(0.0, 0.0))
.zoom(2.0)
.build()
loadValidationStyle(active = true)
}
findViewById<Button>(R.id.validationActive).setOnClickListener {
if (this::maplibreMap.isInitialized) loadValidationStyle(active = true)
}
findViewById<Button>(R.id.validationStatic).setOnClickListener {
if (this::maplibreMap.isInitialized) loadValidationStyle(active = false)
}
}
private fun loadValidationStyle(active: Boolean) {
val state = if (active) "ACTIVE" else "STATIC"
Log.i(TAG, "ParticleValidation: Test App switching to $state")
maplibreMap.setStyle(Style.Builder().fromJson(validationStyle(active))) {
status.text = getString(
if (active) R.string.particle_validation_active_status
else R.string.particle_validation_static_status
)
}
}
private fun validationStyle(active: Boolean) = """
{
"version": 8,
"name": "Particle Core Layer Validation",
"sources": {
"validation-point": {
"type": "geojson",
"data": {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]}}
}
},
"layers": [
{"id":"validation-background","type":"background","paint":{"background-color":"#102030"}},
{"id":"validation-below","type":"circle","source":"validation-point",
"paint":{"circle-radius":46,"circle-color":"#247ba0"}},
{"id":"particle-validation-core","type":"particle-validation",
"paint":{"particle-validation-active":$active}},
{"id":"validation-above","type":"circle","source":"validation-point",
"paint":{"circle-radius":16,"circle-color":"#ffe066"}}
]
}
""".trimIndent()
override fun onStart() { super.onStart(); mapView.onStart() }
override fun onResume() { super.onResume(); mapView.onResume() }
override fun onPause() { mapView.onPause(); super.onPause() }
override fun onStop() { mapView.onStop(); super.onStop() }
override fun onDestroy() { mapView.onDestroy(); super.onDestroy() }
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
companion object {
private const val TAG = "ParticleValidation"
}
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.maplibre.android.maps.MapView
android:id="@id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#DD102030"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/validationStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/particle_validation_active_status"
android:textColor="@android:color/white" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/validationActive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/particle_validation_active" />
<Button
android:id="@+id/validationStatic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/particle_validation_static" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="description_custom_layer">Overlay a custom native layer on the map</string>
<string name="description_particle_core_layer_validation">Validate a source-less Core Style/RenderLayer, frame transform and repaint lifecycle</string>
<string name="description_info_window_adapter">Learn how to create a custom InfoWindow</string>
<string name="description_cameraposition">CameraPosition capabilities</string>
<string name="description_map_fragment">Showcase MapFragment</string>

View File

@@ -9,4 +9,8 @@
<string name="description_world_tour_benchmark">Writes out avg. fps and 1% fps after world tour with .flyTo()</string>
<string name="search">Search</string>
<string name="category_data">Data Sources</string>
<string name="particle_validation_active">ACTIVE</string>
<string name="particle_validation_static">STATIC</string>
<string name="particle_validation_active_status">ACTIVE: Core Layer requests MapLibre frames. Move, zoom, rotate and pitch the map.</string>
<string name="particle_validation_static_status">STATIC: Core Layer stopped requesting frames. Map gestures must still render normally.</string>
</resources>

View File

@@ -25,6 +25,7 @@
<string name="activity_snapshot">Snapshot Activity</string>
<string name="activity_snapshot_within">Snapshot with within expression</string>
<string name="activity_custom_layer">Custom Layer</string>
<string name="activity_particle_core_layer_validation">Particle Core Layer Validation</string>
<string name="activity_map_padding">Map Padding</string>
<string name="activity_debug_mode">Debug Mode</string>
<string name="activity_offline">Offline Map</string>

View File

@@ -10,6 +10,7 @@
#include <mbgl/layermanager/hillshade_layer_factory.hpp>
#include <mbgl/layermanager/line_layer_factory.hpp>
#include <mbgl/layermanager/location_indicator_layer_factory.hpp>
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
#include <mbgl/layermanager/raster_layer_factory.hpp>
#include <mbgl/layermanager/symbol_layer_factory.hpp>
#include <mbgl/util/logging.hpp>
@@ -49,6 +50,8 @@ void LayerManagerDefault::addLayerTypeCoreOnly(std::unique_ptr<mbgl::LayerFactor
}
LayerManagerDefault::LayerManagerDefault() {
// VALIDATION ONLY: 30-01 source-less Core RenderLayer experiment.
addLayerType(std::make_unique<ParticleValidationLayerFactory>());
#if !defined(MBGL_LAYER_FILL_DISABLE_ALL)
addLayerType(std::make_unique<FillLayerFactory>());
#endif

View File

@@ -0,0 +1,25 @@
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
#include <mbgl/renderer/layers/render_particle_validation_layer.hpp>
#include <mbgl/style/layers/particle_validation_layer.hpp>
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
namespace mbgl {
const style::LayerTypeInfo* ParticleValidationLayerFactory::getTypeInfo() const noexcept {
return style::ParticleValidationLayer::Impl::staticTypeInfo();
}
std::unique_ptr<style::Layer> ParticleValidationLayerFactory::createLayer(
const std::string& id, const style::conversion::Convertible&) noexcept {
return std::make_unique<style::ParticleValidationLayer>(id);
}
std::unique_ptr<RenderLayer> ParticleValidationLayerFactory::createRenderLayer(
Immutable<style::Layer::Impl> impl) noexcept {
assert(impl->getTypeInfo() == getTypeInfo());
return std::make_unique<RenderParticleValidationLayer>(
staticImmutableCast<style::ParticleValidationLayer::Impl>(impl));
}
} // namespace mbgl

View File

@@ -0,0 +1,132 @@
#include <mbgl/renderer/layers/render_particle_validation_layer.hpp>
#include <mbgl/gfx/context.hpp>
#include <mbgl/gfx/drawable.hpp>
#include <mbgl/gfx/drawable_builder.hpp>
#include <mbgl/gfx/drawable_tweaker.hpp>
#include <mbgl/map/transform_state.hpp>
#include <mbgl/renderer/layer_group.hpp>
#include <mbgl/renderer/paint_parameters.hpp>
#include <mbgl/renderer/render_pass.hpp>
#include <mbgl/style/types.hpp>
#include <mbgl/util/logging.hpp>
#include <sstream>
namespace mbgl {
namespace {
const style::ParticleValidationLayer::Impl& impl(const Immutable<style::Layer::Impl>& value) {
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
return static_cast<const style::ParticleValidationLayer::Impl&>(*value);
}
class ParticleValidationDrawableTweaker final : public gfx::DrawableTweaker {
public:
ParticleValidationDrawableTweaker(std::string id_, bool active_, int32_t layerIndex_)
: id(std::move(id_)),
active(active_),
layerIndex(layerIndex_) {}
~ParticleValidationDrawableTweaker() override {
Log::Info(Event::Render,
"ParticleValidation: validation drawable destroy id=" + id +
" validationFrames=" + std::to_string(validationFrameCount));
}
void init(gfx::Drawable&) override {}
void execute(gfx::Drawable&, PaintParameters& parameters) override {
if (parameters.pass != RenderPass::Translucent) {
return;
}
++validationFrameCount;
if (validationFrameCount != 1 && validationFrameCount % 60 != 0) {
return;
}
const auto size = parameters.state.getSize();
const auto& matrix = parameters.transformParams.projMatrix;
std::ostringstream message;
message << "ParticleValidation: render frame=" << validationFrameCount << " mapFrame=" << parameters.frameCount
<< " state=" << (active ? "ACTIVE" : "STATIC") << " repaint=" << (active ? "requested" : "stopped")
<< " pass=translucent" << " layerIndex=" << layerIndex << " currentLayer=" << parameters.currentLayer
<< " viewport=" << size.width << 'x' << size.height << " zoom=" << parameters.state.getZoom()
<< " bearing=" << parameters.state.getBearing() << " pitch=" << parameters.state.getPitch() << " proj=["
<< matrix[0] << ',' << matrix[5] << ',' << matrix[12] << ',' << matrix[13] << ']';
Log::Info(Event::Render, message.str());
}
private:
std::string id;
bool active;
int32_t layerIndex;
uint64_t validationFrameCount = 0;
};
} // namespace
RenderParticleValidationLayer::RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl> impl_)
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(impl_, impl_->active)) {
Log::Info(Event::Render,
"ParticleValidation: RenderLayer create id=" + getID() + " source-less=true pass=translucent");
}
RenderParticleValidationLayer::~RenderParticleValidationLayer() {
Log::Info(Event::Render, "ParticleValidation: RenderLayer destroy id=" + getID());
}
void RenderParticleValidationLayer::evaluate(const PropertyEvaluationParameters&) {
const bool active = impl(baseImpl).active;
auto properties = makeMutable<style::ParticleValidationLayerProperties>(
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active);
passes = RenderPass::Translucent;
properties->renderPasses = mbgl::underlying_type(passes);
evaluatedProperties = std::move(properties);
Log::Info(Event::Render,
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") + " id=" + getID() +
" layerIndex=" + std::to_string(getLayerIndex()));
}
bool RenderParticleValidationLayer::needsRepaint() const {
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
return properties.active && baseImpl->visibility != style::VisibilityType::None;
}
void RenderParticleValidationLayer::update(gfx::ShaderRegistry&,
gfx::Context& context,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&,
UniqueChangeRequestVec& changes) {
if (!layerGroup) {
if (auto group = context.createLayerGroup(layerIndex, 1, getID())) {
setLayerGroup(std::move(group), changes);
}
}
if (!layerGroup) {
return;
}
auto* group = static_cast<LayerGroup*>(layerGroup.get());
if (group->getDrawableCount() != 0) {
return;
}
const bool active = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties).active;
auto builder = context.createDrawableBuilder(getID());
auto& drawable = builder->getCurrentDrawable(true);
drawable->setIsCustom(true);
drawable->setRenderPass(RenderPass::Translucent);
drawable->addTweaker(std::make_shared<ParticleValidationDrawableTweaker>(getID(), active, getLayerIndex()));
auto drawables = builder->clearDrawables();
group->addDrawable(std::move(drawables.front()));
++stats.drawablesAdded;
Log::Info(Event::Render,
"ParticleValidation: validation drawable create id=" + getID() +
" layerIndex=" + std::to_string(getLayerIndex()) + " pass=translucent");
}
} // namespace mbgl

View File

@@ -0,0 +1,30 @@
#pragma once
#include <mbgl/renderer/render_layer.hpp>
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
namespace mbgl {
// VALIDATION ONLY: proves Core RenderLayer/frame-transform/repaint integration.
class RenderParticleValidationLayer final : public RenderLayer {
public:
explicit RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl>);
~RenderParticleValidationLayer() override;
void update(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&,
UniqueChangeRequestVec&) override;
private:
void transition(const TransitionParameters&) override {}
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override { return false; }
bool hasCrossfade() const override { return false; }
bool needsRepaint() const override;
void prepare(const LayerPrepareParameters&) override {}
};
} // namespace mbgl

View File

@@ -106,6 +106,9 @@ public:
// Returns true if the layer has a pattern property and is actively crossfading.
virtual bool hasCrossfade() const = 0;
// Returns true when an active animated layer needs another MapLibre frame.
virtual bool needsRepaint() const { return false; }
// Returns true if layer writes to depth buffer by drawing using PaintParameters::depthModeFor3D().
virtual bool is3D() const { return false; }

View File

@@ -818,7 +818,7 @@ bool RenderOrchestrator::hasTransitions(TimePoint timePoint) const {
}
for (const auto& entry : renderLayers) {
if (entry.second->hasTransition()) {
if (entry.second->hasTransition() || entry.second->needsRepaint()) {
return true;
}
}

View File

@@ -0,0 +1,110 @@
#include <mbgl/style/layers/particle_validation_layer.hpp>
#include <mbgl/style/conversion/constant.hpp>
#include <mbgl/style/conversion_impl.hpp>
#include <mbgl/style/layer_observer.hpp>
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
#include <mbgl/util/logging.hpp>
namespace mbgl {
namespace style {
namespace {
constexpr const char* ActiveProperty = "particle-validation-active";
const LayerTypeInfo typeInfo{.type = "particle-validation",
.source = LayerTypeInfo::Source::NotRequired,
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
.layout = LayerTypeInfo::Layout::NotRequired,
.fadingTiles = LayerTypeInfo::FadingTiles::NotRequired,
.crossTileIndex = LayerTypeInfo::CrossTileIndex::NotRequired,
.tileKind = LayerTypeInfo::TileKind::NotRequired};
} // namespace
ParticleValidationLayer::Impl::Impl(const std::string& id)
: Layer::Impl(id, std::string()) {}
bool ParticleValidationLayer::Impl::hasLayoutDifference(const Layer::Impl&) const {
return false;
}
void ParticleValidationLayer::Impl::stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const {}
const LayerTypeInfo* ParticleValidationLayer::Impl::staticTypeInfo() noexcept {
return &typeInfo;
}
ParticleValidationLayer::ParticleValidationLayer(const std::string& layerID)
: Layer(makeMutable<Impl>(layerID)) {
Log::Info(Event::Render, "ParticleValidation: Style Layer create id=" + layerID + " (VALIDATION ONLY)");
}
ParticleValidationLayer::ParticleValidationLayer(Immutable<Impl> impl_)
: Layer(std::move(impl_)) {}
ParticleValidationLayer::~ParticleValidationLayer() {
Log::Info(Event::Render, "ParticleValidation: Style Layer destroy id=" + getID());
weakFactory.invalidateWeakPtrs();
}
const ParticleValidationLayer::Impl& ParticleValidationLayer::impl() const {
return static_cast<const Impl&>(*baseImpl);
}
Mutable<ParticleValidationLayer::Impl> ParticleValidationLayer::mutableImpl() const {
return makeMutable<Impl>(impl());
}
bool ParticleValidationLayer::isActive() const {
return impl().active;
}
void ParticleValidationLayer::setActive(bool active) {
if (active == isActive()) {
return;
}
auto impl_ = mutableImpl();
impl_->active = active;
baseImpl = std::move(impl_);
observer->onLayerChanged(*this);
}
std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) {
if (name != ActiveProperty) {
return conversion::Error{"layer doesn't support this property"};
}
conversion::Error error;
auto converted = conversion::convert<bool>(value, error);
if (!converted) {
return error;
}
setActive(*converted);
return std::nullopt;
}
StyleProperty ParticleValidationLayer::getProperty(const std::string& name) const {
if (name == ActiveProperty) {
return StyleProperty(Value{isActive()}, StyleProperty::Kind::Constant);
}
return {};
}
Value ParticleValidationLayer::serialize() const {
auto result = Layer::serialize();
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
return result;
}
std::unique_ptr<Layer> ParticleValidationLayer::cloneRef(const std::string& id) const {
auto impl_ = mutableImpl();
impl_->id = id;
return std::unique_ptr<Layer>(new ParticleValidationLayer(std::move(impl_)));
}
Mutable<Layer::Impl> ParticleValidationLayer::mutableBaseImpl() const {
return staticMutableCast<Layer::Impl>(mutableImpl());
}
} // namespace style
} // namespace mbgl

View File

@@ -0,0 +1,36 @@
#pragma once
#include <mbgl/style/layer_impl.hpp>
#include <mbgl/style/layer_properties.hpp>
#include <mbgl/style/layers/particle_validation_layer.hpp>
namespace mbgl {
namespace style {
// VALIDATION ONLY: no particle data or production API belongs here.
class ParticleValidationLayer::Impl final : public Layer::Impl {
public:
explicit Impl(const std::string& id);
Impl(const Impl&) = default;
bool hasLayoutDifference(const Layer::Impl&) const override;
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override;
bool active = true;
DECLARE_LAYER_TYPE_INFO;
};
class ParticleValidationLayerProperties final : public LayerProperties {
public:
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl, bool active_)
: LayerProperties(std::move(impl)),
active(active_) {}
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
bool active;
};
} // namespace style
} // namespace mbgl

View File

@@ -34,3 +34,14 @@
- 下一阶段选择 `30_最小技术验证`,集中验证独立 RenderLayer、OpenGL/Vulkan ping-pong/trail/context、Camera/wrap/Mask 和 iOS 薄接入。
- 最终 FPS、最大粒子数、最佳 texture format、生产 Mask 来源、真实天气协议和生产 Host API 仍未确认。
- 本次未修改生产代码、依赖、构建配置、项目总则或 Step,未运行构建/测试。
## 2026-08-29 — 30-01 Core RenderLayer 接入验证(阶段 A)
- 按 `docs/40_任务/30_01_CoreRenderLayer接入验证指令书_v1.md` 完成最小验证代码准备。
- 新增 source-less `ParticleValidationLayer/RenderParticleValidationLayer`,经正式 Style/LayerManager/RenderTree/LayerGroup/translucent pass 链接入;代码明确标记 `VALIDATION ONLY`。
- 新增默认 false 的 RenderLayer repaint hook;ACTIVE 通过 MapLibre 自身 `needsRepaint → Map::onUpdate → Android requestRender` 链持续帧,STATIC 停止验证层额外请求。
- 验证 Drawable 在真实 pass 内读取当前 `PaintParameters/TransformParameters/TransformState` 的 viewport、zoom、bearing、pitch 和 projection matrix 摘要。
- Android Test App 新增 `Particle Core Layer Validation` 页面,提供 ACTIVE/STATIC 切换和蓝/黄普通 Style Layer 顺序参照。
- Linux OpenGL 重新配置通过;本任务新增/直接修改的 5 个 Core 对象精确编译通过。全量增量因公共头触发 321 个对象重编译,在 41/321 无错误时主动停止,未宣称全量 PASS。
- 遵循指令书未运行 Android/Gradle/npm/ADB;等待用户手工 Android OpenGL 编译、运行、手势、resize 与日志证据。
- 当前结论为 `WAITING_MANUAL_VALIDATION`;不得进入下一项 GPU ping-pong/Trail 验证。