diff --git a/docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md b/docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md new file mode 100644 index 000000000..5bdf7d1c4 --- /dev/null +++ b/docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md @@ -0,0 +1,85 @@ +# 30-03 Camera / World Wrap 与 Land/Ocean Mask 最小技术验证报告 v1 + +日期:2026-08-30 + +当前状态:`WAITING_MANUAL_VALIDATION` + +## 1. 前置与结论边界 + +30-02 最新正式报告 v5 为 `PASS(EGL Context Recreation:Accepted Risk / 未验证;用户明确决定不阻塞)`,并明确允许进入 30-03。本轮是 30-03 阶段 A 的验证代码准备。 + +本轮没有运行 Android 编译、Gradle、安装、ADB 或设备操作,也没有用户实际运行证据。因此不得写 PASS;本报告只记录源码和 Linux Core 精确编译可确认的事实。 + +## 2. 当前源码链 + +- Camera:`TransformState` 由 render frame 建立 `TransformParameters`;`PaintParameters::matrixForTile()` 调用当前 `TransformState::matrixFor()`,再乘当前 frame projection matrix。 +- World wrap:`UnwrappedTileID` 用 `wrap + CanonicalTileID` 表达绘制 world copy;`TransformState::matrixFor()` 将 `tileID.wrap * tileScale` 纳入 world translation。 +- 本验证没有独立 Camera、Overlay 或第二套 world 状态机。Composite Tweaker 直接从当前 `PaintParameters` 取得 `matrixForTile({wrap,{0,0,0}})`。 + +## 3. 本次 validation-only 实现 + +`ParticleGpuValidationShader` 的 State / Trail pass 仍是 30-02 的全屏 RenderTarget pass。主 composite 改为 canonical z0 world quad:vertex shader 以当前 frame world-copy matrix 投影,而不是固定 screen quad。 + +每帧按当前未 wrap Camera center 选择中心 wrap 和相邻 wrap (`centerWrap - 1..+1`) 绘制同一 canonical quad;只有中心 copy 可以提交 30-02 slot/frame,其他 copy 仅绘制,故不会把 world copy 数量变成 simulation 推进次数。 + +确定性地理锚点为:A `(0, 0)`、B `(0, -90)`、C `(0, -172.8)`、D `(0, 172.8)`。C/D 位于反经线两侧。它们是 shader 中的 canonical Mercator z0 world fraction,不由 Android UI 计算。 + +ACTIVE 继续运行 30-02 State/Trail A/B;STATIC 保留既有冻结语义:不注册离屏 target、不 update/fade/marker、不 slot swap、不增 simulation frame,但用当前 `PaintParameters` matrix 重画最后 Trail 的地理 composite 与 mask。 + +## 4. Antimeridian 表示 + +Mask fixture 使用正式设计中允许的全球 canonical bounds `west=-180, east=180, south=-85, north=85`,不把单一数据文件表示成跨反经线区域。C/D 两个 canonical anchor 跨越该边界;世界连续性由 `UnwrappedTileID.wrap` 与同一 canonical z0 quad 完成。 + +GPU sampling 采用 canonical world UV `[0,1]`,不在 shader 以 `if lon > 180` 进行局部纠正。每个 world copy 使用同一 canonical mask UV、各自 MapLibre matrix,因此 geometry 与 mask 不会分别选择不同 copy。 + +## 5. Shared Land/Ocean Mask + +当前 `particle-fixtures/coastal-mask/` 在开始时只有占位文件,仓库也尚未具备正式 fixture adapter/upload 链。本轮未伪造 production fixture pipeline;增加的是 validation-only deterministic CPU fixture generator:16×16 RGBA,alpha `0=LAND`、`255=OCEAN`,包含斜向 coast 和非轴对齐缺口。 + +它只创建一次 `Texture2D`,采用 nearest + clamp,`maskGeneration` 单调记录。Camera pan/zoom/bearing/pitch 和 `GLOBAL ↔ OCEAN_ONLY_A ↔ OCEAN_ONLY_B` 只改变 drawable uniform/style property,不重新上传 mask。A/B 都绑定同一 `maskTexture`;GLOBAL 不 discard,A 显示蓝色、B 显示绿色且均在 land discard。NoData 在本 synthetic fixture 中不存在;正式 OCEAN_ONLY contract 的 NoData fail-closed 仍未实现。 + +`markContextDestroyed()` 后下一次 ACTIVE update 丢弃整套 validation GPU resources(包括 mask texture)并以新 `maskGeneration` 从 CPU fixture 重建,不复用 stale handle。真实 EGL context recreation 仍不在本轮被声称验证。 + +## 6. Test App 与日志 + +复用 `Particle Core Layer Validation`,新增最小控制:`GLOBAL`、`OCEAN A`、`OCEAN B`、`DATELINE`、`WORLD WRAP`。A/B/GLOBAL 通过既有 Layer 的 paint properties 更新,预期资源复用,不重载 Style;ACTIVE/STATIC/DISABLE/ENABLE 保持原有入口。Activity Bundle 还保存 mask mode。 + +主要日志标签为 `ParticleValidation30_03`:fixture/mask create、maskGeneration、mode/consumer reuse、first camera-locked draw、每 30 frame 节流 camera 摘要、world wrap、antimeridian 与 30-02 STATIC frozen composite。没有每 frame 输出完整 matrix。 + +## 7. Codex 静态验证 + +- `CCACHE_DISABLE=1 cmake --build build-linux-opengl --target CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o CMakeFiles/mbgl-core.dir/src/mbgl/style/layers/particle_validation_layer.cpp.o CMakeFiles/mbgl-core.dir/src/mbgl/shaders/gl/shader_info.cpp.o -- -j3`:PASS。 +- `clang-format --dry-run --Werror`(受影响 C++/header):PASS。 +- `git diff --check`:PASS。 +- Android 编译/运行、Gradle、ADB、GPU visual result:未执行,遵守任务书。 + +## 8. 用户手工 Android OpenGL 验证 + +编译(由用户手工执行): + +```bash +cd /mnt/sda1/MapLibreParticleLab +source /root/.maplibre-particle-env +cd platform/android +RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64 +``` + +打开 `Custom Layer → Particle Core Layer Validation`,日志可使用: + +```bash +adb -s 192.168.1.146 logcat | grep -E 'ParticleValidation30_02|ParticleValidation30_03|ParticleValidation' +``` + +1. T1–T5:先 ACTIVE 数秒后切 STATIC,在 GLOBAL 下 pan、zoom、bearing、pitch;锚点/mask 必须跟地理底图移动,STATIC frame 不推进且 `maskGeneration` 不变。再切 ACTIVE,确认 simulation 恢复。 +2. T6:点 `WORLD WRAP`,水平连续 pan 至非零 wrap;确认相邻 copy 皆显示、无重复 slot commit,并记录 center/zoom/wrap 日志。 +3. T7/T8:点 `DATELINE`,分别东→西、西→东跨越 ±180°;C/D、pattern 与 mask 不得断裂、翻转、重复或消失。 +4. T9/T10:在 coastal fixture 区域依次选 `OCEAN A`、`GLOBAL`;A 的 land 必须抑制,GLOBAL 的 land 必须显示。 +5. T11:`OCEAN A → OCEAN B → OCEAN A`;A/B land/ocean 判断一致、颜色不同,日志 `maskGeneration` 保持不变并显示 `reuse=true`。 +6. T12:在 DATELINE/WORLD WRAP 场景启用 OCEAN A/B,确认 mask 与 geometry 使用同一 copy,无 ±180° seam。 +7. T13:进行可稳定触发的 rotation/viewport 变化;确认 mode 恢复、地理对齐和新 resource generation。此项不替代 30-02 EGL context-loss 证据。 + +## 9. 未验证与判定 + +未验证:Android OpenGL 编译、所有 T1–T13 视觉/日志结果、真实 EGL context loss、正式 fixture adapter、正式 NoData fail-closed 与生产 coastline 数据。 + +当前唯一合法结论:`WAITING_MANUAL_VALIDATION`。在用户提供 Android OpenGL 实际运行结果前,不允许进入下一项 iOS 最小验证。 diff --git a/docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md b/docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md new file mode 100644 index 000000000..78b6e2188 --- /dev/null +++ b/docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md @@ -0,0 +1,54 @@ +# 30-03 Camera / World Wrap 与 Land/Ocean Mask 验证 Codex 完成报告 v1 + +日期:2026-08-30 + +执行结论:`WAITING_MANUAL_VALIDATION` + +## 1. Git 与前置 + +- 分支:`particle/main`。 +- 开始 HEAD:`3b5a147bbaa3e3d967ee89b16745a3ab8f62c72d`。 +- 公开远端:`origin`,目标分支 `origin/particle/main`。 +- 30-02 前置:v5 正式 `PASS`,允许进入 30-03;EGL context recreation 是用户接受且不阻塞的未验证风险。 +- 开始时有两份用户提供的未跟踪任务指令书(30-02 rotation、30-03);均未修改、未提交。 + +已读取 AGENTS、项目总则、项目规范、基线、STEP-000、调查/架构设计/前序报告、开发记录、30-01/30-02 实际 validation Core 与 Android Test App,以及 Transform/Paint/world-wrap/gfx texture 相关源码。 + +## 2. 已完成 + +- Camera:Composite Tweaker 使用当前 `PaintParameters::matrixForTile()`;没有 UI Camera、独立 timer 或 Overlay。 +- World wrap:通过当前 `UnwrappedTileID{wrap, CanonicalTileID{0,0,0}}` 与 `TransformState::matrixFor()` 绘制中心及相邻 world copy;仅中心 copy commit simulation。 +- Antimeridian:使用全球 canonical bounds 与两侧 C/D anchor,未引入局部 longitude clamp 补丁或第二套 wrap 状态机。 +- Mask:创建一次 validation-only deterministic 16×16 CPU fixture 和一个 shared `Texture2D`;GLOBAL、OCEAN_ONLY_A、OCEAN_ONLY_B 复用同一 texture。 +- 生命周期:Camera 和 consumer 切换不会重传 mask;通知 context destroyed 后下一个 ACTIVE update 放弃旧资源并创建新 mask generation。 +- Test App:在已有页面增加 mask consumer、DATELINE 和 WORLD WRAP 控制;A/B/GLOBAL property update 不 reload Style。 +- 日志:新增 fixture/create/generation/reuse/camera/world-wrap/antimeridian/anchor 字段,camera 摘要节流至每 30 frame。 +- Linux Core 精确对象编译、clang-format、diff check:PASS。 + +## 3. 实际修改 + +- `render_particle_validation_layer.cpp`:当前 frame matrix、world-copy composite、shared mask lifecycle 与日志。 +- `particle_validation_layer.*`:validation-only `particle-validation-mask-mode`(0/1/2)。 +- validation shader/UBO/shader info:world z0 projection、mask sampling与 anchor。 +- `ParticleCoreLayerValidationActivity.kt`、布局、strings:最小人工验证控制与 Bundle 保存。 + +未修改通用 Transform、Projection、RenderTree、gfx API、正式 Field/Mask pipeline、Vulkan、iOS、依赖或项目总则;没有新增第三方依赖。 + +## 4. 未完成与无法验证 + +- 未完成:Android OpenGL 手工编译和 T1–T13 运行验证。 +- 无法验证:真实设备的 pan/zoom/bearing/pitch 对齐、world wrap/antimeridian continuity、A/B visual crop、rotation 后真实 resource generation、真实 EGL context recreation。 +- `particle-fixtures/coastal-mask/` 尚无 production fixture adapter,因此使用最小 deterministic validation generator;它不是生产 coastline pipeline,正式 `mask.u8`/NoData contract 仍未实现。 + +## 5. Android 手工步骤 + +技术报告 v1 第 8 节给出了当前 Makefile 的准确 OpenGL x86_64 command、带 `adb -s 192.168.1.146` 的日志命令和 T1–T13。Codex 未执行这些命令。 + +## 6. Git 收尾 + +- 本报告及代码提交 hash、提交消息、push 结果、最终远端 HEAD 将在提交后按事实补记于最终交付。 +- 本任务只会 stage 30-03 validation 代码、v1 报告和开发记录;两份用户未跟踪指令书保留。 + +## 7. 下一步 + +当前为 `WAITING_MANUAL_VALIDATION`,不允许进入 iOS Core Layer 薄接入。用户完成 Android OpenGL T1–T13 并提供日志/现象后,才可更新为 PASS、FAIL 或 BLOCKED。 diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 0ad7fdc0e..4672cfb5b 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -11,19 +11,38 @@ struct ShaderSource 0.5 && ocean < 0.5) discard; + float stripe = 0.55 + 0.45 * sin((v_world.x + v_world.y) * 100.0 + u_pass.y * 0.07); + // A=(0°,0°), B=(-90°,approximately 0°), C/D straddle ±180°. + float anchors = 0.0; + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.50, 0.50)))); + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.25, 0.50)))); + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.02, 0.50)))); + anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.98, 0.50)))); + vec3 color = u_pass.w < 1.5 ? vec3(0.10, 0.70, 1.00) : vec3(0.35, 1.00, 0.45); + fragColor = vec4(mix(color * stripe, vec3(1.00, 0.82, 0.15), anchors), + 0.28 + min(intensity, 0.35) * 0.35 + anchors * 0.35); } #ifdef OVERDRAW_INSPECTOR diff --git a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp index 7c12f44ec..3f9bee9b6 100644 --- a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp +++ b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp @@ -5,12 +5,14 @@ namespace mbgl { namespace shaders { -// VALIDATION ONLY: parameters for the 30-02 State/Trail ping-pong proof. +// VALIDATION ONLY: 30-02 State/Trail and 30-03 Camera/Mask proof parameters. struct alignas(16) ParticleGpuValidationUBO { std::array pass; std::array marker; + std::array mask; + std::array matrix; }; -static_assert(sizeof(ParticleGpuValidationUBO) == 2 * 16); +static_assert(sizeof(ParticleGpuValidationUBO) == 7 * 16); } // namespace shaders } // namespace mbgl diff --git a/include/mbgl/shaders/shader_defines.hpp b/include/mbgl/shaders/shader_defines.hpp index 3a7d6c31d..39c4026a4 100644 --- a/include/mbgl/shaders/shader_defines.hpp +++ b/include/mbgl/shaders/shader_defines.hpp @@ -340,6 +340,7 @@ enum { enum { idParticleGpuValidationPreviousTexture, + idParticleGpuValidationMaskTexture, particleGpuValidationTextureCount }; diff --git a/include/mbgl/style/layers/particle_validation_layer.hpp b/include/mbgl/style/layers/particle_validation_layer.hpp index 255aacb70..682f8ea89 100644 --- a/include/mbgl/style/layers/particle_validation_layer.hpp +++ b/include/mbgl/style/layers/particle_validation_layer.hpp @@ -13,6 +13,8 @@ public: bool isActive() const; void setActive(bool); + uint8_t getMaskMode() const; + void setMaskMode(uint8_t); class Impl; const Impl& impl() const; diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 3fb071755..236f8c7a4 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -10,6 +10,7 @@ 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.style.layers.PaintPropertyValue import org.maplibre.android.testapp.R /** VALIDATION ONLY: Android OpenGL host for the 30-01/30-02 Core RenderLayer experiments. */ @@ -19,14 +20,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private lateinit var status: TextView private var active = true private var enabled = true + private var maskMode = MASK_GLOBAL override fun onCreate(savedInstanceState: Bundle?) { active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true enabled = savedInstanceState?.getBoolean(STATE_ENABLED, true) ?: true + maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL super.onCreate(savedInstanceState) Log.i( TAG, - "ParticleValidation30_02: Activity state restored active=$active enabled=$enabled " + + "ParticleValidation30_03: Activity state restored active=$active enabled=$enabled maskMode=$maskMode " + "recreated=${savedInstanceState != null}" ) setContentView(R.layout.activity_particle_core_layer_validation) @@ -46,10 +49,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } findViewById