fix: 修复 V15 WeatherColor Metal 纹理生命周期
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-09-27 18:41:56 +09:00
parent cd71640a84
commit 5af91fe7a1
7 changed files with 236 additions and 4 deletions

View File

@@ -0,0 +1,60 @@
# V15 iOS WeatherColor 纹理生命周期修复
状态:**进行中(Codex 源码已实施;等待 RDC Mac 定向构建与测试、之后等待 iPad 人工验收)**
日期:2026-09-27(JST)
## 目标与基线
- 仅修复 V15 WeatherColor 的 Metal 纹理绑定生命周期,不涉及 V12/V13、JavaScript、NavSea API、Android 行为或发行版本/tag。
- 仓库:`/Users/tei/dev/MapLibreParticleLab`;机器:Mac mini、Darwin 24.6.0、x86_64;分支:`particle/main`。
- 开工 HEAD:`cd71640a84901212ce4681e296f027befdff4ee0`;`maplibre-particle/rn/13.4.1-particle.15` peeled commit 与 HEAD 相同;`origin/particle/main` 同步于 HEAD。
- manifest 的 V15 `sourceCommit` 为 `1fdc5df625d405233703136eb2dc12032d932da2`;后续 finalize release 生成的 V15 tag 指向 HEAD `cd71640a84901212ce4681e296f027befdff4ee0`。这两个坐标分别表示 manifest 源码提交与后续 tag HEAD,不构成冲突;本任务相关已跟踪源码在两提交间相同,tag 与发行内容未改。
- 开工时仅有用户提供的 V15 任务书为未跟踪文件;始终保留。现有 submodule 均已初始化,本轮未修改。
- 执行模型/推理档位:GPT-6 Luna High。
## 根因与修改
Metal LayerGroup 在 drawable 的 `upload()` 后才执行 Tweaker。旧实现于 Tweaker 的 `execute()` 才向既有 drawable 调用 `setTexture()`;当 endpoint 纹理已在 WeatherColor `update()` 中更换、wrap 范围不变时,drawable 的上传阶段仍持有旧纹理,随后 draw 才首次绑定新纹理,可能触发 `assert(!textureDirty)`。
本轮修改:
- 在 `update()` 进入资源更新时保存 drawable 原有 T0/T1 实际纹理指针;资源 endpoint 安装完成后比较新旧指针。
- 把 wrap 区间变化或任一纹理 endpoint 指针变化统一作为重建条件;重建在 WeatherColor update 阶段执行,builder 在 drawable 上传前安装正确纹理。
- 移除 WeatherColor Tweaker 的三处 `setTexture()`;Tweaker 只写 UBO 中的 mix、opacity、color stops、scale/bounds 等参数。
- 新增 `WeatherColorTextureLifecycle` 定向测试,覆盖无变化、单端/双端替换、T0/T1 交叉复用、T0=T1 合并/保持、wrap 改变、隐藏期间 endpoint 替换后恢复、空资源首次创建与释放。
动态 mix/opacity/stops 不参与 endpoint 比较,因此这些值单独变化不会触发 drawable 重建或纹理上传。Resources 清空后下次更新会重新创建纹理和 drawables;隐藏期间不更新时,恢复后的 endpoint 比较仍在下一次 update、drawable upload 之前发生。
## 修改文件
- `include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp`(新增)
- `src/mbgl/renderer/layers/render_weather_color_layer.cpp`
- `test/renderer/weather_color_texture_lifecycle.test.cpp`(新增)
- `test/CMakeLists.txt`
- `docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md`(新增)
- `docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md`(新增)
- `粒子开发记录.md`(追加)
未修改任务书、用户其他文件、公共 API/ABI、Style Z-order、shader slot、版本锁、release tag、ZIP/manifest、Android、RN/JS、NavSea、V12/V13 或粒子 advection/State/Trail/Retained 代码。
## 本阶段检查与状态
- 已完成只读源码调用链复核和 diff 静态核对准备。
- `git diff --check`:由 Codex 执行,结果记录于完成报告。
- RDC 首次 OpenGL runner build:**FAIL**,`weather_color_texture_lifecycle.test.cpp:60:17` 因 `nullptr` 参数无法推导模板类型;RDC 反馈 production renderer object 已编译。Codex 仅为该断言显式指定 `<int>`。RDC 随后 OpenGL build retry exit 0、定向 GTest 6/6 PASS;Metal host build exit 0、定向 GTest 6/6 PASS。原始日志位于 `/tmp/navsea_v15_weathercolor_validation/`(`opengl_build.log`、`opengl_build_retry.log`、`opengl_test.log`、`metal_host_build.log`、`metal_host_test.log`)。Codex 未运行构建或测试。
- iOS App/Xcode 编译、签名、安装、Metal 真机渲染和 iPad 事故场景:**等待用户手工验收**。
- 现有仓库内未找到 `STEP_RECORD.md`;按根 `AGENTS.md` 追加 `粒子开发记录.md`,没有新建或猜造该文件。
- 不提交、不推送;等待 RDC 测试反馈后再按反馈修复和决定后续 Git 收尾。
## RDC Mac 定向验证命令
先按 OpenGL,再按 Metal host 顺序执行:
```bash
cmake --build build-step088-opengl --target mbgl-test-runner --parallel 3
build-step088-opengl/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*'
cmake --build build-step097-metal-host --target mbgl-test-runner --parallel 3
build-step097-metal-host/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*'
```
RDC 已按顺序完成 OpenGL 与 Metal host 验证,结果均 PASS(各 6/6)。Metal host 测试仅验证 host 可执行代码,不等于 iOS 设备 Metal PASS。当前等待用户手工 iPad 验收;本阶段不再构建。

View File

@@ -0,0 +1,83 @@
# V15 iOS WeatherColor 纹理生命周期修复 Codex 完成报告 v1
日期:2026-09-27(JST)
结论:**IMPLEMENTED / WAITING FOR RDC MAC VALIDATION AND MANUAL IPAD VALIDATION**。源码修复已实施,测试代码已添加;没有运行构建或测试,不能标记 Metal/iPad PASS。
## 1. 执行配置与 Git 基线
- 实际执行模型与档位:**GPT-6 Luna High**(按本轮用户指定记录;未切换或升级)。
- 仓库:`/Users/tei/dev/MapLibreParticleLab`;机器:Mac mini,Darwin 24.6.0,x86_64。
- 分支:`particle/main`;开工/当前 HEAD:`cd71640a84901212ce4681e296f027befdff4ee0`。
- V15 tag `maplibre-particle/rn/13.4.1-particle.15` peeled commit 与 HEAD 相同;`origin/particle/main` 与 HEAD 相同。
- manifest 中的 V15 `sourceCommit` 是 `1fdc5df625d405233703136eb2dc12032d932da2`;后续 finalize release 生成的 V15 tag 指向 HEAD `cd71640a84901212ce4681e296f027befdff4ee0`。两者角色不同且本任务相关已跟踪源码相同,因此不存在基线冲突。任务书保留原样,tag/发行内容未修改。
- 开工工作树只存在用户提供且未跟踪的 V15 任务书;本轮保留未跟踪状态。没有 stage、commit、push、reset、clean、版本/tag/发行包覆盖。
- `platform/android/VERSION` 为 `13.4.1-particle.15`;`platform/ios/VERSION` 当前内容为 `6.28.0`。本轮未改版本坐标。
## 2. 根因链和修复
源码复核确认:`RenderWeatherColorLayer::update()` 上传/替换 Resources 中的纹理,但旧 Drawable 仅在 wrap 区间变化时重建;Metal `LayerGroup::upload()` 先于 `render()`;旧 Tweaker 在 `render()` 阶段才将新纹理写入 Drawable。`gfx::Drawable::setTexture()` 仅更换资源指针,不会补做前序 upload,因此新 Metal texture 可能在 draw 时仍为 dirty。IPS 不包含 Drawable/Texture ID,不能据此声称事故中的确切对象身份已由日志证明。
修复在 update 阶段比较 Drawable 原有与期望的 T0/T1 实际纹理指针。wrap 范围或任一 endpoint 指针变化时,当场重建该层 drawables,确保 builder 在本帧 upload 前安装纹理。Tweaker 不再绑定纹理,只更新 UBO 动态值。`assert(!textureDirty)` 和 Metal 通用绑定逻辑未改。
指针转移结果:
| 场景 | 指针比较 | 预期动作 |
|---|---|---|
| T0/T1 均不变 | 两指针相同 | 不重建、不重传 |
| 仅 T0 或仅 T1 被替换 | 一指针不同 | update 阶段重建 |
| 两端均替换 | 两指针不同 | update 阶段重建 |
| 旧 T0/T1 交叉复用 | 顺序变化 | update 阶段重建 |
| T0=T1 | 相同共享指针 | 保持时不重建;从双端切换/反向时重建 |
| 仅 mix/opacity/stops 改变 | endpoint 不参与这些参数 | Tweaker 更新 UBO,不重建/重传 |
| wrap 改变 | endpoint 可相同 | 重建 wrap drawables |
| Resources 清空后恢复 | null → 新端点且 wrap 哨兵失配 | 新建资源与 drawables |
## 3. 实际修改文件
- `include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp`:新增 drawable endpoint/wrap 重建判定 helper。
- `src/mbgl/renderer/layers/render_weather_color_layer.cpp`:update 阶段检测 endpoint 指针并重建;Tweaker 删除 draw 阶段纹理绑定。
- `test/renderer/weather_color_texture_lifecycle.test.cpp`:新增六组指针/生命周期决策测试。
- `test/CMakeLists.txt`:将新测试加入 CMake test target;Bazel 通过既有测试 glob 收录。
- `docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md`:新增本阶段记录。
- `粒子开发记录.md`:追加本轮事实。
- 本 Codex 完成报告。
没有更改 V15 任务书、V12/V13、JS/RN、NavSea 宿主 API、Android、release pipeline、release tag/archive/manifest/version、粒子 advection/State/Trail/Retained、公共 API/ABI、Layer ID、Z-order、shader slot 或 Metal assert。
## 4. 检查和验证结果
| 检查 | 结果 |
|---|---|
| 源码调用链只读复核 | 已完成;事实和限制如上 |
| `git diff --check` | **PASS**(已跟踪 diff 无 whitespace error;新增文件另行查看,未纳入该命令的 Git diff 范围) |
| RDC 首次 OpenGL host 编译 | **FAIL**:`test/renderer/weather_color_texture_lifecycle.test.cpp:60:17` 的 `nullptr` 参数无法参与模板类型推导;RDC 同时反馈 production `render_weather_color_layer.cpp` object 已编译 |
| OpenGL host 修正 | 已将该断言调用写为 `weatherColorDrawableBindingsChanged<int>(...)`,仅修正测试 helper 的模板类型推导 |
| RDC OpenGL host build retry | **PASS**, exit 0 |
| RDC OpenGL 定向 GTest | **PASS**, 6/6 |
| RDC Metal host 编译 | **PASS**, exit 0 |
| RDC Metal host 定向 GTest | **PASS**, 6/6 |
| iOS/Xcode build、签名、安装 | 未执行,用户按项目边界手工完成 |
| iPad Metal 运行、崩溃断言/内存/纹理计数 | 未执行,等待人工集中验收 |
| Android/Gradle/APK/ADB/Jest/JS | 未执行 |
| commit/push/release pipeline | 未执行 |
## 5. RDC Mac 精确命令
按 OpenGL → Metal host 顺序执行;每个 build 成功后运行该新增测试:
```bash
cmake --build build-step088-opengl --target mbgl-test-runner --parallel 3
build-step088-opengl/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*'
cmake --build build-step097-metal-host --target mbgl-test-runner --parallel 3
build-step097-metal-host/mbgl-test-runner --gtest_filter='WeatherColorTextureLifecycle.*'
```
RDC 日志目录:`/tmp/navsea_v15_weathercolor_validation/`。日志文件:`opengl_build.log`(首轮失败)、`opengl_build_retry.log`(重试成功)、`opengl_test.log`(6/6)、`metal_host_build.log`(成功)、`metal_host_test.log`(6/6)。未从回传摘要/日志记录到耗时,因此不补写耗时。Metal host 结果不是 iOS 真机通过。
## 6. 限制、剩余风险与人工验收
- 新测试覆盖 endpoint 判定矩阵,不是 drawable/Metal GPU 端到端模拟;`upload()` 到 draw 的实际时序仍需 RDC Mac host 验证和 iPad 验收。
- 任务书引用的事故 IPS 与外部调查报告未在本机读取;没有推断其中未提供的内容。源代码链是可推导的候选根因,而非 IPS 已证明纹理 ID。
- iPad 需使用后续正确版本 Native 手工编译;先核对二进制 UUID,再执行冷启动风场、风→浪→风、风→雨→潮→风、邻帧/第九帧淘汰后重入、快速 Seek、天气关闭重开、平移缩放、前后台和旋转。检查 assert/crash、WeatherColor/Particle 画面、上传/重建次数、内存及异常日志。本船放大、上层闪烁和 Android 场景独立验收。
- 未发现 `STEP_RECORD.md`;本轮按根规则更新 Step 记录并追加 `粒子开发记录.md`。
- 当前结论为源码已实施、RDC OpenGL/Metal host 定向验证 PASS,等待用户手工 iPad 验收;host PASS 不代表设备 PASS。Codex 阶段按用户授权 scoped commit/push 到 `origin/particle/main`,commit 和回读的远端 HEAD 记录在本报告后续 Git 收尾项及最终交付中。iPad 实测通过前状态不得提升为 CLOSED/PASS。

View File

@@ -0,0 +1,14 @@
#pragma once
namespace mbgl {
template <class Texture>
constexpr bool weatherColorDrawableBindingsChanged(const Texture* previous0,
const Texture* previous1,
const Texture* next0,
const Texture* next1,
bool wrapsChanged) noexcept {
return wrapsChanged || previous0 != next0 || previous1 != next1;
}
} // namespace mbgl

View File

@@ -1,4 +1,5 @@
#include <mbgl/renderer/layers/render_weather_color_layer.hpp>
#include <mbgl/renderer/layers/weather_color_texture_lifecycle.hpp>
#include <mbgl/gfx/color_mode.hpp>
#include <mbgl/gfx/context.hpp>
@@ -104,9 +105,6 @@ public:
wrap(wrap_) {}
void init(gfx::Drawable&) override {}
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture);
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationFieldTexture);
drawable.setTexture(resources->texture1, shaders::idParticleGpuValidationNextFieldTexture);
shaders::ParticleGpuValidationUBO ubo = {
.pass = {5.0f, 0.0f, 0.0f, 0.0f},
.marker = {resources->mix, 0.0f, 0.0f, 0.0f},
@@ -268,6 +266,8 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders,
sourceNextField = timelineFrame.t1;
}
auto state = std::static_pointer_cast<Resources>(resources);
const auto previousTexture0 = state ? state->texture0.get() : nullptr;
const auto previousTexture1 = state ? state->texture1.get() : nullptr;
if (!state) {
state = std::make_shared<Resources>();
state->shader = context.getGenericShader(shaders, ShaderName);
@@ -348,7 +348,13 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders,
const auto centerWrap = static_cast<int16_t>(std::floor((center.longitude() + 180.0) / 360.0));
const int16_t firstWrap = centerWrap - 1;
const int16_t lastWrap = centerWrap + 1;
if (state->firstWrap == firstWrap && state->lastWrap == lastWrap) return;
const bool wrapsChanged = state->firstWrap != firstWrap || state->lastWrap != lastWrap;
if (!weatherColorDrawableBindingsChanged(previousTexture0,
previousTexture1,
state->texture0.get(),
state->texture1.get(),
wrapsChanged))
return;
group.clearDrawables();
state->firstWrap = firstWrap;
state->lastWrap = lastWrap;

View File

@@ -29,6 +29,7 @@ add_library(
${PROJECT_SOURCE_DIR}/test/renderer/image_manager.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/pattern_atlas.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/particle_viewport_reseed.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/weather_color_texture_lifecycle.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/retained_underlay_cache.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/render_orchestrator_error.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/shader_registry.test.cpp

View File

@@ -0,0 +1,61 @@
#include <mbgl/renderer/layers/weather_color_texture_lifecycle.hpp>
#include <gtest/gtest.h>
using namespace mbgl;
TEST(WeatherColorTextureLifecycle, KeepsDrawablesForUnchangedEndpoints) {
int texture0 = 0;
int texture1 = 1;
EXPECT_FALSE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &texture0, &texture1, false));
// mix, opacity and color stops are dynamic UBO values and are not inputs
// to the binding decision.
}
TEST(WeatherColorTextureLifecycle, RebuildsForEitherOrBothReplacedEndpoints) {
int texture0 = 0;
int texture1 = 1;
int replacement0 = 2;
int replacement1 = 3;
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &replacement0, &texture1, false));
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &texture0, &replacement1, false));
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &replacement0, &replacement1, false));
}
TEST(WeatherColorTextureLifecycle, RebuildsWhenEndpointsAreCrossReusedOrCollapsed) {
int texture0 = 0;
int texture1 = 1;
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &texture1, &texture0, false));
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &texture0, &texture0, false));
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture0, &texture0, &texture1, false));
EXPECT_FALSE(weatherColorDrawableBindingsChanged(&texture0, &texture0, &texture0, &texture0, false));
}
TEST(WeatherColorTextureLifecycle, RebuildsWhenWrapRangeChangesWithoutTextureChanges) {
int texture0 = 0;
int texture1 = 1;
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture1, &texture0, &texture1, true));
}
TEST(WeatherColorTextureLifecycle, DetectsEndpointReplacementAfterHiddenPeriod) {
int previous0 = 0;
int previous1 = 1;
int resumed0 = 2;
int resumed1 = 1;
// The layer may skip updates while hidden; on resume, the next update must
// rebuild before drawable upload if its selected endpoint changed.
EXPECT_TRUE(weatherColorDrawableBindingsChanged(&previous0, &previous1, &resumed0, &resumed1, false));
}
TEST(WeatherColorTextureLifecycle, HandlesInitialAndClearedEndpoints) {
int texture0 = 0;
EXPECT_FALSE(weatherColorDrawableBindingsChanged<int>(nullptr, nullptr, nullptr, nullptr, false));
EXPECT_TRUE(weatherColorDrawableBindingsChanged<int>(nullptr, nullptr, &texture0, &texture0, false));
EXPECT_TRUE(weatherColorDrawableBindingsChanged<int>(&texture0, &texture0, nullptr, nullptr, false));
}

View File

@@ -1008,3 +1008,10 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
- 完成报告 `docs/报告/STEP-098_QBIN_V4单帧与八帧滚动缓存_Codex完成报告_v1.md`;源码交付等待 RDC 按 B→C 构建测试和后续 Android/iOS 配套实测。当前不进入 V15 pipeline。
- 2026-09-27 STEP-098 RDC:Core B 19/19、OpenGL Host 119/119、Metal Host 119/119 定向 GTest PASS;修复 Source 私有 Store 测试断言和两处无效 Grid fixture。Android/RN/iOS 制品、真机 GPU/内存与 V15 Pipeline 尚未验证;报告见 docs/报告/STEP-098_RDC定向验收报告_v1.md。
### 2026-09-27 V15 iOS WeatherColor 纹理生命周期修复(Codex 源码交付)
- 核实 manifest V15 `sourceCommit=1fdc5df625d405233703136eb2dc12032d932da2`,后续 finalize release 生成的 tag 指向 HEAD `cd71640a84901212ce4681e296f027befdff4ee0`;两者角色不同且任务相关源码相同,不构成冲突。用户未跟踪任务书继续保留。
- WeatherColor update 阶段现比较 T0/T1 GPU texture 指针;endpoint 或 wrap 变化会在 LayerGroup upload 前重建 Drawable。移除 Tweaker draw 阶段的 texture 绑定,仅保留 UBO 动态更新;新增 endpoint 转移矩阵定向测试。
- RDC 首次 OpenGL build 在测试文件第 60 行遇到 `nullptr` 模板推导失败;production renderer object 已编译。Codex 对该测试调用显式指定 `<int>`。RDC OpenGL retry exit 0、6/6;Metal host build exit 0、6/6。日志位于 `/tmp/navsea_v15_weathercolor_validation/`。iPad/Xcode/设备实测待用户手工完成;tag 和发行内容未覆盖。
- Step:`docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md`;完成报告:`docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md`。