From 5af91fe7a105f0ac047e18db1dc063c9ffd69d44 Mon Sep 17 00:00:00 2001 From: tei Date: Sun, 27 Sep 2026 18:41:56 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20V15=20WeatherColor?= =?UTF-8?q?=20Metal=20=E7=BA=B9=E7=90=86=E7=94=9F=E5=91=BD=E5=91=A8?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V15_iOS_WeatherColor纹理生命周期修复_v1.md | 60 ++++++++++++++ ...herColor纹理生命周期修复_Codex完成报告_v1.md | 83 +++++++++++++++++++ .../weather_color_texture_lifecycle.hpp | 14 ++++ .../layers/render_weather_color_layer.cpp | 14 +++- test/CMakeLists.txt | 1 + .../weather_color_texture_lifecycle.test.cpp | 61 ++++++++++++++ 粒子开发记录.md | 7 ++ 7 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md create mode 100644 docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md create mode 100644 include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp create mode 100644 test/renderer/weather_color_texture_lifecycle.test.cpp diff --git a/docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md b/docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md new file mode 100644 index 000000000..a5e7d45f5 --- /dev/null +++ b/docs/50_Step记录/V15_iOS_WeatherColor纹理生命周期修复_v1.md @@ -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 仅为该断言显式指定 ``。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 验收;本阶段不再构建。 diff --git a/docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md b/docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md new file mode 100644 index 000000000..fd6937e78 --- /dev/null +++ b/docs/报告/V15_iOS_WeatherColor纹理生命周期修复_Codex完成报告_v1.md @@ -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(...)`,仅修正测试 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。 diff --git a/include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp b/include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp new file mode 100644 index 000000000..4a9ea7179 --- /dev/null +++ b/include/mbgl/renderer/layers/weather_color_texture_lifecycle.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace mbgl { + +template +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 diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index dde0a0672..c8b0da698 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -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); + const auto previousTexture0 = state ? state->texture0.get() : nullptr; + const auto previousTexture1 = state ? state->texture1.get() : nullptr; if (!state) { state = std::make_shared(); state->shader = context.getGenericShader(shaders, ShaderName); @@ -348,7 +348,13 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, const auto centerWrap = static_cast(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; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8bb807359..6ad485e08 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 diff --git a/test/renderer/weather_color_texture_lifecycle.test.cpp b/test/renderer/weather_color_texture_lifecycle.test.cpp new file mode 100644 index 000000000..c549298c3 --- /dev/null +++ b/test/renderer/weather_color_texture_lifecycle.test.cpp @@ -0,0 +1,61 @@ +#include + +#include + +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(nullptr, nullptr, nullptr, nullptr, false)); + EXPECT_TRUE(weatherColorDrawableBindingsChanged(nullptr, nullptr, &texture0, &texture0, false)); + EXPECT_TRUE(weatherColorDrawableBindingsChanged(&texture0, &texture0, nullptr, nullptr, false)); +} diff --git a/粒子开发记录.md b/粒子开发记录.md index 38ddec360..656291165 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -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 对该测试调用显式指定 ``。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`。