fix: distinguish raster array field snapshots

This commit is contained in:
OpenAI Codex
2026-09-02 08:06:06 +08:00
parent 6ef084ab98
commit 1cd3e77109
8 changed files with 133 additions and 8 deletions

View File

@@ -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`。

View File

@@ -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 在交付收尾后回填。

View File

@@ -30,7 +30,6 @@ private:
std::unique_ptr<AsyncRequest> fieldRequest;
std::optional<std::string> metadata;
std::optional<std::vector<char>> fieldBytes;
uint64_t generation = 0;
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
};

View File

@@ -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.

View File

@@ -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) +

View File

@@ -8,8 +8,19 @@
#include <mbgl/util/async_request.hpp>
#include <mbgl/util/logging.hpp>
#include <atomic>
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>(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) {

View File

@@ -54,6 +54,14 @@
#include <optional>
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<std::string>(eastMetadata);
} else if (resource.url == "east-field") {
response.data = std::make_unique<std::string>(eastField);
} else if (resource.url == "north-metadata") {
response.data = std::make_unique<std::string>(northMetadata);
} else if (resource.url == "north-field") {
response.data = std::make_unique<std::string>(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;

View File

@@ -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 的方向变化。