fix: correct particle time-window reuse state
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-03 08:23:36 +08:00
parent 5dd4dbf496
commit de665212ca
8 changed files with 111 additions and 20 deletions

View File

@@ -0,0 +1,6 @@
# STEP-043A 40-02C 源码审查后最小修正
- 基线:`5dd4dbf4967c1238e1bde8fb66fe3dbd2c4fe52a`
- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION`
- 修正:GPU texture reuse 保留旧 backing 但消费 wanted snapshot revision;MIX_REJECTED 不再改变 timeMix;增加 Layer/Resource/Field upload/reuse/reset 生命周期日志;所有 Style reload 调用点有具体 reason。
- 检查:shader generator、Linux Core/source-test object、format、diff check 通过;未运行 Android Gradle、ADB 或设备。

View File

@@ -5,3 +5,4 @@
- 规则:generation 仍是 source revision;GPU reuse 比较 fixtureID、dataVersion、已验证 `fieldSha256` 和 grid compatibility。MIX 未就绪只记录 `MIX_REJECTED`,不 reload style。
- 检查:shader generator、Linux Core/source test object、format、diff check 通过;未运行 Android Gradle/ADB。
- Android:T1--T10 仍待用户执行,不能判 PASS。
- STEP-043A 源码审查发现 reuse revision、MIX reject state、diagnostics 与 Style reason 缺口,均已最小修正;状态不变。

View File

@@ -0,0 +1,18 @@
# STEP-043A 40-02C 源码审查后最小修正完成报告
开始基线为 `5dd4dbf4967c1238e1bde8fb66fe3dbd2c4fe52a`,状态为 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。
- Reuse:旧 T0/T1 texture/image 可复用,但 `resources` snapshot 现在写入 wanted revision,避免下一帧重复 sourceChanged。
- MIX:guard 完成后才赋值 `timeMix`;拒绝时没有 state/property/style mutation。
- Diagnostics:新增 renderLayerInstanceId、LAYER_CREATE/DESTROY/CHANGED、FIELD_UPLOAD、FIELD_REUSE、SIMULATION_RESET/resetCount 与 TIME_MIX_UPDATE 关联字段。
- Style:INITIAL、ENABLE、DISABLE、UNIFORM_EAST、UNIFORM_NORTH、TIME_WINDOW、ADVANCE 等调用点均传明确 reason;MIX 不会触发 Style load。
- 检查:shader generator、Linux Core/source-test object、clang-format、diff check 通过。未运行 Android Gradle、ADB 或设备验证。
用户手工:
```bash
cd /mnt/sda1/MapLibreParticleLab/platform/android
./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain
```
随后以 `adb -s 192.168.1.146` 安装并验证 T1--T10;在用户设备验收前不得判 PASS。

View File

@@ -53,7 +53,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
.zoom(2.0)
.build()
}
loadValidationStyle(active = active, enabled = enabled)
loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
}
findViewById<Button>(R.id.validationActive).setOnClickListener {
@@ -63,10 +63,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
if (this::maplibreMap.isInitialized) setValidationProperties(active = false, maskMode = maskMode)
}
findViewById<Button>(R.id.validationDisable).setOnClickListener {
if (this::maplibreMap.isInitialized) loadValidationStyle(active = active, enabled = false)
if (this::maplibreMap.isInitialized) loadValidationStyle(active = active, enabled = false, reason = "DISABLE")
}
findViewById<Button>(R.id.validationEnable).setOnClickListener {
if (this::maplibreMap.isInitialized) loadValidationStyle(active = active, enabled = true)
if (this::maplibreMap.isInitialized) loadValidationStyle(active = active, enabled = true, reason = "ENABLE")
}
findViewById<Button>(R.id.validationGlobal).setOnClickListener {
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_GLOBAL)
@@ -80,18 +80,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
findViewById<Button>(R.id.validationUniformEast).setOnClickListener {
timeWindowMode = false
fixtureMode = FIXTURE_UNIFORM_EAST
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_EAST")
}
findViewById<Button>(R.id.validationUniformNorth).setOnClickListener {
timeWindowMode = false
fixtureMode = FIXTURE_UNIFORM_NORTH
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_NORTH")
}
findViewById<Button>(R.id.validationTimeWindow).setOnClickListener {
timeWindowMode = true
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "TIME_WINDOW")
}
findViewById<Button>(R.id.validationMix0).setOnClickListener { setTimeMix(0.0f) }
findViewById<Button>(R.id.validationMix50).setOnClickListener { setTimeMix(0.5f) }
@@ -100,7 +100,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
timeWindowMode = true
fixtureMode = FIXTURE_UNIFORM_NORTH
timeMix = 0.0f
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "ADVANCE")
}
findViewById<Button>(R.id.validationDateline).setOnClickListener {
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
@@ -131,7 +131,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
this.fixtureMode = fixtureMode
val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID)
if (layer == null || !enabled) {
loadValidationStyle(active, enabled)
loadValidationStyle(active, enabled, "LAYER_NOT_READY")
return
}
layer.setProperties(
@@ -143,7 +143,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun setTimeMix(value: Float) {
timeMix = value.coerceIn(0.0f, 1.0f)
val requestedTimeMix = value.coerceIn(0.0f, 1.0f)
val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID)
val rejectReason = when {
!styleReady -> "STYLE_LOADING"
@@ -156,6 +156,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
Log.i(TAG, "ParticleGPU: event=MIX_REJECTED reason=$rejectReason styleEpoch=$styleLoadEpoch")
return
}
timeMix = requestedTimeMix
layer.setProperties(PaintPropertyValue("raster-particle-time-mix", timeMix))
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=MIX_PROPERTY_ONLY value=$timeMix styleEpoch=$styleLoadEpoch")

View File

@@ -48,6 +48,7 @@ constexpr Size MaskSize{16, 16};
std::atomic_uint64_t nextGpuGeneration{0};
std::atomic_uint64_t nextMaskGeneration{0};
std::atomic_uint64_t nextFieldGeneration{0};
std::atomic_uint64_t nextRenderLayerInstanceId{0};
struct ParticleSettings {
bool active;
@@ -144,6 +145,7 @@ void removeTargets(const std::vector<RenderTargetPtr>& targets, UniqueChangeRequ
class ParticleGpuResources {
public:
uint64_t renderLayerInstanceId = 0;
uint64_t generation = 0;
uint64_t committedFrame = 0;
uint8_t stateRead = 0;
@@ -168,6 +170,7 @@ public:
uint64_t nextFieldGeneration = 0;
uint64_t gpuNextFieldGeneration = 0;
uint64_t fieldUploadCount = 0;
uint64_t resetCount = 0;
float timeMix = 0.0f;
uint8_t fixtureMode = 255;
uint8_t loggedMaskMode = 255;
@@ -192,7 +195,12 @@ public:
uint8_t stateWrite() const { return static_cast<uint8_t>(1 - stateRead); }
uint8_t trailWrite() const { return static_cast<uint8_t>(1 - trailRead); }
void resetSequence() {
void resetSequence(const char* reason) {
++resetCount;
Log::Info(Event::Render,
"ParticleGPU: event=SIMULATION_RESET reason=" + std::string(reason) + " renderLayerInstanceId=" +
std::to_string(renderLayerInstanceId) + " resourceGeneration=" + std::to_string(generation) +
" resetCount=" + std::to_string(resetCount));
committedFrame = 0;
stateRead = 0;
trailRead = 0;
@@ -511,22 +519,29 @@ RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable<style::Layer::Imp
return makeMutable<style::ParticleRenderLayerProperties>(
std::move(impl_), settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix);
}()) {
renderLayerInstanceId = ++nextRenderLayerInstanceId;
formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo();
Log::Info(Event::Render,
"ParticleRender: RenderLayer create id=" + getID() +
"ParticleGPU: event=LAYER_CREATE layerId=" + getID() +
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId) +
" source=" + (formalSource ? baseImpl->source : std::string("validation-only")));
}
RenderRasterParticleLayer::~RenderRasterParticleLayer() {
if (gpuResources) {
Log::Info(Event::Render,
"ParticleValidation30_02: resources destroy generation=" + std::to_string(gpuResources->generation) +
"ParticleGPU: event=LAYER_DESTROY layerId=" + getID() +
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(gpuResources->generation) +
" contextDestroyed=" + (gpuResources->contextDestroyed ? "true" : "false"));
}
Log::Info(Event::Render, "ParticleValidation: RenderLayer destroy id=" + getID());
}
void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
Log::Info(Event::Render,
"ParticleGPU: event=LAYER_CHANGED layerId=" + getID() +
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId));
const auto settings = particleSettings(baseImpl);
auto properties = makeMutable<style::ParticleRenderLayerProperties>(
baseImpl, settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix);
@@ -612,6 +627,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
}
if (!resources) {
auto owned = std::make_shared<ParticleGpuResources>();
owned->renderLayerInstanceId = renderLayerInstanceId;
owned->generation = ++nextGpuGeneration;
owned->formalDisplay = formalSource;
owned->shader = context.getGenericShader(shaders, ShaderName);
@@ -663,10 +679,22 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
return;
}
owned->fieldTexture->setImage(owned->fieldImage);
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_UPLOAD slot=T0 reason=RESOURCE_CREATE fieldUploadCount=1 "
"sourceGeneration=" +
std::to_string(owned->field.generation) +
" renderLayerInstanceId=" + std::to_string(owned->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(owned->generation));
owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
.wrapU = gfx::TextureWrapType::Repeat,
.wrapV = gfx::TextureWrapType::Clamp});
owned->nextFieldTexture->setImage(owned->nextFieldImage);
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_UPLOAD slot=T1 reason=RESOURCE_CREATE fieldUploadCount=2 "
"sourceGeneration=" +
std::to_string(owned->nextField.generation) +
" renderLayerInstanceId=" + std::to_string(owned->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(owned->generation));
owned->nextFieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
.wrapU = gfx::TextureWrapType::Repeat,
.wrapV = gfx::TextureWrapType::Clamp});
@@ -748,22 +776,41 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
.wrapU = gfx::TextureWrapType::Repeat,
.wrapV = gfx::TextureWrapType::Clamp});
++resources->fieldUploadCount;
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_UPLOAD reason=NEW_CONTENT fieldUploadCount=" +
std::to_string(resources->fieldUploadCount) +
" sourceGeneration=" + std::to_string(field.generation) +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation));
return true;
};
auto takeSlot = [&](const particle::ParticleFieldSnapshot& wanted,
const char* destination,
particle::ParticleFieldSnapshot& assigned,
std::shared_ptr<PremultipliedImage>& image,
gfx::Texture2DPtr& texture) {
if (particle::sameFieldGpuContent(oldT0, wanted)) {
assigned = oldT0;
assigned = wanted;
image = resources->fieldImage;
texture = resources->fieldTexture;
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_REUSE from=OLD_T0 to=" + std::string(destination) +
" oldGeneration=" + std::to_string(oldT0.generation) +
" newGeneration=" + std::to_string(wanted.generation) +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation));
return true;
}
if (particle::sameFieldGpuContent(oldT1, wanted)) {
assigned = oldT1;
assigned = wanted;
image = resources->nextFieldImage;
texture = resources->nextFieldTexture;
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_REUSE from=OLD_T1 to=" + std::string(destination) +
" oldGeneration=" + std::to_string(oldT1.generation) +
" newGeneration=" + std::to_string(wanted.generation) +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation));
return true;
}
assigned = wanted;
@@ -775,8 +822,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
std::shared_ptr<PremultipliedImage> imageT1;
gfx::Texture2DPtr textureT0;
gfx::Texture2DPtr textureT1;
if (!takeSlot(nextT0, assignedT0, imageT0, textureT0) ||
!takeSlot(nextT1, assignedT1, imageT1, textureT1)) {
if (!takeSlot(nextT0, "NEW_T0", assignedT0, imageT0, textureT0) ||
!takeSlot(nextT1, "NEW_T1", assignedT1, imageT1, textureT1)) {
Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed");
return;
}
@@ -791,7 +838,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
resources->gpuFieldGeneration = resources->fieldGeneration;
resources->gpuNextFieldGeneration = resources->nextFieldGeneration;
resources->fixtureMode = fixtureMode;
if (!windowOverlap) resources->resetSequence();
if (!windowOverlap) resources->resetSequence("FIELD_NO_OVERLAP");
Log::Info(Event::Render,
"ParticleGPU: time window advance T0=" + oldT0.fixtureID + " T1=" + oldT1.fixtureID +
" -> T0=" + resources->field.fixtureID + " T1=" + resources->nextField.fixtureID +
@@ -807,9 +854,12 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
if (resources->timeMix != requestedTimeMix) {
resources->timeMix = requestedTimeMix;
Log::Info(Event::Render,
"ParticleGPU: timeMix=" + std::to_string(resources->timeMix) + " T0=" + resources->field.fixtureID +
" T1=" + resources->nextField.fixtureID + " uploads=" +
std::to_string(resources->fieldUploadCount) + " simulationRebuild=0 trailRebuild=0");
"ParticleGPU: event=TIME_MIX_UPDATE value=" + std::to_string(resources->timeMix) +
" T0=" + resources->field.fixtureID + " T1=" + resources->nextField.fixtureID +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation) +
" fieldUploadCount=" + std::to_string(resources->fieldUploadCount) +
" resetCount=" + std::to_string(resources->resetCount));
}
if (!resources->lifecycleModeKnown || resources->lifecycleActive != active) {

View File

@@ -34,6 +34,7 @@ private:
std::optional<particle::ParticleFieldSnapshot> sourceField;
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
bool formalSource = false;
uint64_t renderLayerInstanceId = 0;
};
} // namespace mbgl

View File

@@ -231,6 +231,16 @@ TEST(Source, RasterArrayGpuContentIdentityExcludesRuntimeGeneration) {
EXPECT_FALSE(particle::sameFieldGpuContent(east, changed));
}
TEST(Source, RasterArrayGpuReuseConsumesNewRevision) {
const auto oldT1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 100);
auto wantedNewT0 = oldT1;
wantedNewT0.generation = 200;
const auto assignedAfterReuse = wantedNewT0;
EXPECT_TRUE(particle::sameFieldGpuContent(oldT1, wantedNewT0));
EXPECT_FALSE(particle::fieldIdentityChanged(assignedAfterReuse, wantedNewT0));
}
TEST(Source, RasterArrayTimeWindowKeepsIndependentCompatibleSnapshots) {
const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 11);
const auto north = particle::OfflineFixtureParticleAdapter::load("uniform-north", 12);

View File

@@ -244,3 +244,7 @@
- 修正 north-positive V 到 Mercator Y 的最终 displacement 符号;Field canonical contract 未变。
- MIX 移除 Style reload fallback,未就绪只拒绝并记录日志;GPU reuse 使用已验证 SHA-256 content identity,revision generation 仍独立保留。
- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION`;未运行 Android Gradle/ADB,T1--T10 待用户验收。
## 2026-09-03 — STEP-043A 源码审查后最小修正
- texture reuse 现消费 wanted revision;MIX reject 不再改变 timeMix;补充 lifecycle diagnostics 与 Style reason。状态仍为 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。