From 1cd3e771095922863b3717689c690fc8ab0ed8c6 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 2 Sep 2026 08:06:06 +0800 Subject: [PATCH] fix: distinguish raster array field snapshots --- ...040_Android离线Fixture正式粒子最小闭环_v1.md | 2 +- ...erArrayFieldIdentity修复_Codex完成报告_v1.md | 27 ++++++++ .../style/sources/raster_array_source.hpp | 1 - .../offline_fixture_particle_adapter.hpp | 8 +++ .../render_particle_validation_layer.cpp | 13 ++-- .../style/sources/raster_array_source.cpp | 18 +++++- test/style/source.test.cpp | 63 +++++++++++++++++++ 粒子开发记录.md | 9 +++ 8 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md diff --git a/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md b/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md index 10e82afcc..1d4cd74a1 100644 --- a/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md +++ b/docs/50_Step记录/STEP-040_Android离线Fixture正式粒子最小闭环_v1.md @@ -36,4 +36,4 @@ ## 当前结果、阻塞与下一步 -正式 Source/Layer/RenderLayer 已不依赖 source-less `particle-validation` Layer;历史 validation Layer 只保留为既有验证工具。Android 首次实际启动的 RasterArray source RenderItem 空指针已在 source 内补为合法 no-op RenderItem,保持全局 RenderSource 契约。随后实机日志发现 ACTIVE/STATIC 每帧重置:RenderOrchestrator 的按 source 可渲染性扫描会短暂调用 `markLayerRenderable(false)`,它不是持久 disabled;粒子层原先却在该回调中 `resetSequence()`,每帧清空跨 render 的 frame/slot/seed 状态。已删除该错误 reset 边界,真实重建仍限于 field、viewport、context 与 layer removal;新增状态转换及前 3 帧采样日志。正式 Composite 已改为显示 Trail RGB,legacy validation display 保留原 stripe/anchor/world-wrap 分支。最新实机仍不可见的根因进一步确认是 State seed:MapLibre RenderTarget 新 target 清为 opaque black,原 shader 错把 `previous.a` 当 unseeded 标记,令所有 State texel 从 `(0,0)` 开始。现以 `ParticleGpuResources::stateSeeded` 为权威,使用 `u_mask.w` 显式指示 `SEED_STATE`;ACTIVE 与 STATIC zero-step bootstrap 都从各自 `v_uv` 分散初始化,后续 frame 继续 State read。Linux 精确编译、shader generator 幂等、GLSL/header parity、format 与 diff 已通过;Android 实机验收仍由用户执行,40-01 保持 `WAITING_MANUAL_VALIDATION`。 +正式 Source/Layer/RenderLayer 已不依赖 source-less `particle-validation` Layer;历史 validation Layer 只保留为既有验证工具。Android 首次实际启动的 RasterArray source RenderItem 空指针已在 source 内补为合法 no-op RenderItem,保持全局 RenderSource 契约。随后实机日志发现 ACTIVE/STATIC 每帧重置:RenderOrchestrator 的按 source 可渲染性扫描会短暂调用 `markLayerRenderable(false)`,它不是持久 disabled;粒子层原先却在该回调中 `resetSequence()`,每帧清空跨 render 的 frame/slot/seed 状态。已删除该错误 reset 边界,真实重建仍限于 field、viewport、context 与 layer removal;新增状态转换及前 3 帧采样日志。正式 Composite 已改为显示 Trail RGB,legacy validation display 保留原 stripe/anchor/world-wrap 分支;State 以 `stateSeeded`/`u_mask.w` 而非 RenderTarget alpha 进行显式 seed。最新实机确认 east direction 通过,但 source recreation 后 north 仍显示 east:`RasterArraySource` 的 instance-local generation 在新 source 从 1 重启,renderer 又只比较 generation,错误复用 east GPU field。generation 现为 process-wide atomic runtime snapshot revision,renderer 同时比较 generation、fixtureID、dataVersion;fixture switch 保持既有 field texture replace、resetSequence、state re-seed 与 trail clear。Linux 精确编译、定向 source test object、format 与 diff 已通过;Android 实机验收仍由用户执行,40-01 保持 `WAITING_MANUAL_VALIDATION`。 diff --git a/docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md b/docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md new file mode 100644 index 000000000..e552d3215 --- /dev/null +++ b/docs/报告/40_01_RasterArrayFieldIdentity修复_Codex完成报告_v1.md @@ -0,0 +1,27 @@ +# 40-01 RasterArray Field Identity 修复 Codex 完成报告 v1 + +## 1. 结论 + +已修复 Android Style/source recreation 后 `uniform-north` 错误复用 `uniform-east` GPU field 的 identity collision。40-01 继续为 `WAITING_MANUAL_VALIDATION`;未运行 Android Gradle、ADB 或真机。 + +## 2. 根因 + +`RasterArraySource::tryLoad()` 将 `ParticleFieldSnapshot::generation` 由 source 实例成员递增。相同 source ID 的 Style 重建会新建实例,east 首次 load 与 north 首次 load 都可能得到 generation 1。`RenderRasterParticleLayer` 正式 sourceChanged 只比较 generation,故将不同 fixture 错认为同一 field,复用了旧 east GPU texture。 + +## 3. 新 contract + +- `ParticleFieldSnapshot::generation` 是 process-wide、单调递增的运行时 snapshot revision;由 `raster_array_source.cpp` 的 atomic 分配。它不表示 metadata `dataVersion`,不使用时间戳或随机数。 +- renderer 的 `fieldIdentityChanged(current, next)` 同时比较 `generation`、`fixtureID`、`dataVersion`。任一变化均触发既有 field upload、`resetSequence()`、State re-seed 与 Trail clear;三者都相同则不重复上传。 +- 未修改 shader U/V、State/Trail/Composite slot wiring、ACTIVE/STATIC lifecycle、Rotation bootstrap、mask、world wrap、antimeridian、RenderTarget 或 Android UI。 + +## 4. 诊断与测试 + +- source load 日志:`ParticleField: source loaded fixtureId=... generation=... dataVersion=...`。 +- renderer switch 日志:`ParticleRender: fixture switched from=... to=... oldGeneration=... newGeneration=... gpuFieldGeneration=... trail=cleared stateSeed=INITIALIZE`。 +- 定向 C++ 测试覆盖两个独立 east/north RasterArraySource 实例 generation 不同;generation 相同、fixture 不同仍为 changed;同 fixture/generation/dataVersion 为 unchanged,dataVersion 改变为 changed。 +- Linux OpenGL 精确编译:`raster_array_source.cpp.o`、`render_particle_validation_layer.cpp.o`、`source.test.cpp.o`,PASS。 +- `clang-format --dry-run --Werror` 与 `git diff --check`:PASS。 + +## 5. Git 收尾 + +最终代码 commit、push 结果与 `origin/particle/main` HEAD 在交付收尾后回填。 diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index 3a755aa41..1a72d1935 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -30,7 +30,6 @@ private: std::unique_ptr fieldRequest; std::optional metadata; std::optional> fieldBytes; - uint64_t generation = 0; mapbox::base::WeakPtrFactory weakFactory{this}; }; diff --git a/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp b/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp index ca92f11a4..93c8c0e1a 100644 --- a/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp +++ b/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp @@ -29,6 +29,14 @@ struct ParticleFieldSnapshot { bool isValid() const; }; +// A snapshot revision identifies a runtime load, while fixtureID and +// dataVersion identify its declared content. Keep all three checks at the GPU +// upload boundary so a recreated source cannot silently reuse an old field. +inline bool fieldIdentityChanged(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) { + return current.generation != next.generation || current.fixtureID != next.fixtureID || + current.dataVersion != next.dataVersion; +} + // Phase-one OfflineFixtureParticleAdapter. Its public contract deliberately // contains no Android/UI/GPU objects, allowing the same decoded snapshot to be // used by future Android Vulkan and iOS renderers. diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 9f2475574..0268b21e6 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -684,9 +684,12 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, "ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0"); } - const bool sourceChanged = formalSource && sourceField && resources->fieldGeneration != sourceField->generation; + const bool sourceChanged = formalSource && sourceField && + particle::fieldIdentityChanged(resources->field, *sourceField); if ((!formalSource && resources->fixtureMode != fixtureMode) || sourceChanged) { try { + const auto oldFixtureID = resources->field.fixtureID; + const auto oldFieldGeneration = resources->fieldGeneration; const auto fieldGeneration = formalSource ? sourceField->generation : ++nextFieldGeneration; auto field = formalSource ? *sourceField @@ -709,9 +712,11 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, resources->fixtureMode = fixtureMode; resources->resetSequence(); Log::Info(Event::Render, - "ParticleRender: fixture switched id=" + resources->field.fixtureID + - " fieldGeneration=" + std::to_string(resources->fieldGeneration) + " gpuFieldGeneration=" + - std::to_string(resources->gpuFieldGeneration) + " upload reason=FIELD_CHANGED trail=cleared"); + "ParticleRender: fixture switched from=" + oldFixtureID + " to=" + resources->field.fixtureID + + " oldGeneration=" + std::to_string(oldFieldGeneration) + + " newGeneration=" + std::to_string(resources->fieldGeneration) + + " gpuFieldGeneration=" + std::to_string(resources->gpuFieldGeneration) + + " trail=cleared stateSeed=INITIALIZE upload reason=FIELD_CHANGED"); Log::Info(Event::Render, "ParticleField: fixture validated id=" + resources->field.fixtureID + " noData=" + std::to_string(resources->field.noData) + diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index 93ed17b8a..f283b8d5b 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -8,8 +8,19 @@ #include #include +#include + namespace mbgl::style { +namespace { + +// Snapshot revision is process-wide rather than source-instance local: a +// Style recreation can replace an east source with a north source under the +// same source ID, and that must always invalidate the prior GPU field. +std::atomic_uint64_t nextRasterArrayFieldGeneration{0}; + +} // namespace + RasterArraySource::RasterArraySource(std::string id, std::string fixtureID, std::string metadataURL, @@ -55,13 +66,16 @@ void RasterArraySource::loadDescription(FileSource& fileSource) { void RasterArraySource::tryLoad() { if (!metadata || !fieldBytes) return; try { + const auto fieldGeneration = ++nextRasterArrayFieldGeneration; auto field = particle::OfflineFixtureParticleAdapter::load( - impl().fixtureID, *metadata, *fieldBytes, ++generation); + impl().fixtureID, *metadata, *fieldBytes, fieldGeneration); baseImpl = makeMutable(impl(), std::move(field)); loaded = true; + const auto& loadedField = *impl().field; Log::Info(Event::Render, "ParticleField: source loaded fixtureId=" + impl().fixtureID + " metadata=" + impl().metadataURL + - " field=" + impl().fieldURL); + " field=" + impl().fieldURL + " generation=" + std::to_string(loadedField.generation) + + " dataVersion=" + std::to_string(loadedField.dataVersion)); observer->onSourceLoaded(*this); observer->onSourceChanged(*this); } catch (const std::exception& error) { diff --git a/test/style/source.test.cpp b/test/style/source.test.cpp index 2ce0ffdd0..3c4d63fbf 100644 --- a/test/style/source.test.cpp +++ b/test/style/source.test.cpp @@ -54,6 +54,14 @@ #include +namespace { + +std::string fixtureBytes(const std::string& fixtureID, const std::string& file) { + return util::read_file("particle-fixtures/" + fixtureID + "/" + file); +} + +} // namespace + using namespace mbgl; using SourceType = mbgl::style::SourceType; @@ -156,6 +164,61 @@ TEST(Source, RasterArrayRenderItem) { EXPECT_FALSE(item->hasRenderPass(RenderPass::Translucent)); } +TEST(Source, RasterArraySnapshotsUseProcessWideGenerations) { + SourceTest test; + const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json"); + const auto eastField = fixtureBytes("uniform-east", "field.f32"); + const auto northMetadata = fixtureBytes("uniform-north", "metadata.json"); + const auto northField = fixtureBytes("uniform-north", "field.f32"); + + test.fileSource->sourceResponse = [&](const Resource& resource) -> Response { + Response response; + if (resource.url == "east-metadata") { + response.data = std::make_unique(eastMetadata); + } else if (resource.url == "east-field") { + response.data = std::make_unique(eastField); + } else if (resource.url == "north-metadata") { + response.data = std::make_unique(northMetadata); + } else if (resource.url == "north-field") { + response.data = std::make_unique(northField); + } else { + ADD_FAILURE() << "Unexpected RasterArray resource: " << resource.url; + } + return response; + }; + + style::RasterArraySource east{"source", "uniform-east", "east-metadata", "east-field"}; + east.setObserver(&test.styleObserver); + style::RasterArraySource north{"source", "uniform-north", "north-metadata", "north-field"}; + north.setObserver(&test.styleObserver); + + uint8_t loadedSources = 0; + test.styleObserver.sourceChanged = [&](Source&) { + if (++loadedSources == 2) { + test.end(); + } + }; + east.loadDescription(*test.fileSource); + north.loadDescription(*test.fileSource); + + test.run(); + ASSERT_TRUE(east.impl().field); + ASSERT_TRUE(north.impl().field); + + EXPECT_NE(east.impl().field->generation, north.impl().field->generation); +} + +TEST(Source, RasterArrayFieldIdentityIncludesFixtureAndDataVersion) { + const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 7); + const auto northWithSameGeneration = particle::OfflineFixtureParticleAdapter::load("uniform-north", 7); + auto eastWithNewDataVersion = east; + ++eastWithNewDataVersion.dataVersion; + + EXPECT_TRUE(particle::fieldIdentityChanged(east, northWithSameGeneration)); + EXPECT_TRUE(particle::fieldIdentityChanged(east, eastWithNewDataVersion)); + EXPECT_FALSE(particle::fieldIdentityChanged(east, east)); +} + TEST(Source, LoadingCorrupt) { SourceTest test; diff --git a/粒子开发记录.md b/粒子开发记录.md index b5ea8e3ee..750644f25 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -205,3 +205,12 @@ - `ParticleGpuResources::stateSeeded` 现在是唯一权威。State pass 把 `!stateSeeded` 通过现有 UBO 的 `u_mask.w` 传入;shader 仅以该 flag 选择 `v_uv` 或 `previous.rg`,不再以 texture alpha 推断生命周期。Field `alpha` 仍仅表示 NoData validity。 - ACTIVE 第一次 State pass 与 STATIC zero-step bootstrap 都对每个 texel 进行 `v_uv` seed;Composite commit 后 `stateSeeded=true`,之后继续 read texture。新 resources、context recreation 和 fixture/field `resetSequence()` 保持重新 seed;普通 ACTIVE、Camera、STATIC 与 transient source renderability 不会 seed。 - 新增低频 `ParticleRender: stateSeed=INITIALIZE|CONTINUE` 日志,含 generation、State A/B 与 fixture。shader source/header generator parity、生成器幂等、Linux Core 与 Source test object 编译、format 和 diff 均通过;未运行 Android Gradle/ADB/真机,状态仍为 `WAITING_MANUAL_VALIDATION`。 + +## 2026-09-02 — 40-01 RasterArray Source 重建 Field Identity 修复 + +- 用户实机确认 GLOBAL ACTIVE uniform-east、ACTIVE→STATIC→ACTIVE 均通过;点击 uniform-north 后仍 eastward。Android Test App 已正确重建 `raster-array` source,根因不在 UI 或 shader/ping-pong。 +- `RasterArraySource::tryLoad()` 原有 `generation` 是 source 实例成员;Style 用同一 source ID 重建时 east 与 north 都可得到 generation 1。`RenderRasterParticleLayer` 正式路径又只比较 generation,因此错误保留 east GPU field texture。 +- runtime generation 改为 `raster_array_source.cpp` 的 process-wide atomic 单调 revision;它只表达运行时 snapshot identity,不混用 metadata `dataVersion`,也不使用时间或随机数。source load 日志现在含 fixtureId、generation、dataVersion。 +- renderer 使用 shared `fieldIdentityChanged()` 三元判断:generation、fixtureID、dataVersion 任一改变都会重新上传 field texture 并保留既有 `resetSequence()`、State re-seed、Trail clear 路径;三者相同不重复上传。fixture switch 日志记录 from/to、old/new generation、gpuFieldGeneration、trail clear 与 state seed。 +- 新增定向 C++ 测试:两个独立 RasterArraySource 实例加载 east/north 得到不同 generation;同 generation 的 east/north 仍判定 identity changed;同 fixture/generation/dataVersion 不判定变更,dataVersion 改变则判定变更。 +- Linux OpenGL 的 RasterArraySource、RenderLayer 与 `source.test.cpp` 精确编译,format/diff 均通过;未运行 Android Gradle/ADB/真机,40-01 继续 `WAITING_MANUAL_VALIDATION`。下一轮只验收 GLOBAL + ACTIVE + uniform-east 后切 uniform-north 的方向变化。