diff --git a/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md b/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md index 0907b4298..d490ea215 100644 --- a/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md +++ b/docs/50_Step记录/STEP-041_Android双时间片GPU时间插值_v1.md @@ -4,6 +4,7 @@ - 前置:STEP-040(40-01)已在 Android OpenGL 手工验收为 PASS。 - 目标:在唯一正式链路 `RasterArraySource -> RasterParticleLayer -> GPU Simulation -> Trail -> Composite` 中,把单 Field 扩展为 T0/T1 两个独立 GPU Field,并在 State pass 对速度作 GPU `mix`。 - 实现提交:`4675a8a29dd6fc7b684bfb64913a8c9075ad8583`(`feat: add GPU particle time interpolation`)。 +- 40-02C 最终修正:原实现的 MIX fallback、GPU identity 与 NORTH Mercator displacement 问题由 STEP-043 修正;在 Android T1--T10 前仍不得判 PASS。 ## 本轮实现 diff --git a/docs/50_Step记录/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_v1.md b/docs/50_Step记录/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_v1.md new file mode 100644 index 000000000..3baa77a56 --- /dev/null +++ b/docs/50_Step记录/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_v1.md @@ -0,0 +1,7 @@ +# STEP-043 40-02C Android 双时间片 GPU 时间插值最终修正 + +- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` +- 修正:MIX hard property-only guard、revision/content identity 分离、A/B→B/C content reuse、Mercator Y displacement 与 north-positive V 的边界转换。 +- 规则: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。 diff --git a/docs/报告/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_Codex完成报告_v1.md b/docs/报告/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_Codex完成报告_v1.md new file mode 100644 index 000000000..129787ac8 --- /dev/null +++ b/docs/报告/STEP-043_40-02C_Android双时间片GPU时间插值最终修正_Codex完成报告_v1.md @@ -0,0 +1,19 @@ +# STEP-043 40-02C Android 双时间片 GPU 时间插值最终修正完成报告 + +状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。开始 HEAD 为 `d4c17fc8b0d5005bb25e603bd0046fcb19602aa3`。 + +- NORTH:Field/decode/texture/timeMix 继续为 V north-positive;仅 State shader 最终 Mercator displacement 使用 `dy=-v`。 +- MIX:Activity 记录 style epoch/ready;未 ready、disabled、layer 缺失或 time-window off 时记录 `MIX_REJECTED`,绝不 fallback `setStyle()`;正常路径仅 `setProperties` 并记录 `MIX_PROPERTY_ONLY`。 +- Identity:generation/fixture/dataVersion 继续标识 revision;snapshot 保存已验证 metadata SHA-256,`sameFieldGpuContent` 用 fixture/data/hash/compatibility,令跨 reload 的 B 可复用。 +- ADVANCE:Renderer slot matching/overlap 改用 GPU content identity,因此 B generation 变化但内容不变时可复用旧 texture,仅新 C 需要 upload;无内容 overlap 仍保留安全 reset。 +- 检查:shader generator、Linux Core 与 source test object、clang-format、diff check 通过。未执行 Android Gradle/ADB。 + +用户手工: + +```bash +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug --console=plain +adb -s 192.168.1.146 install -r platform/android/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +``` + +T1--T10(EAST/NORTH、MIX 0 upload/reset、MIX reject、layer lifecycle、B→C +1 upload、回归)均待用户 Android 日志/视觉验收。 diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index f41041e09..6ae141412 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -67,7 +67,10 @@ void main() { position = v_uv; velocity = vec2(0.0); } - position = fract(position + velocity * u_pass.y * 0.004); + // Field V remains north-positive. Mercator Y increases southward, so + // convert only at the physical-velocity -> map-displacement boundary. + vec2 mercatorVelocity = vec2(velocity.x, -velocity.y); + position = fract(position + mercatorVelocity * u_pass.y * 0.004); if (any(isnan(velocity))) position = v_uv; fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0); } else if (mode < 1.5) { diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index d22096414..473c0b676 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -24,6 +24,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var fixtureMode = FIXTURE_UNIFORM_EAST private var timeMix = 0.0f private var timeWindowMode = false + private var styleLoadEpoch = 0L + private var styleReady = false override fun onCreate(savedInstanceState: Bundle?) { active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true @@ -110,12 +112,15 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } } - private fun loadValidationStyle(active: Boolean, enabled: Boolean) { + private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") { this.active = active this.enabled = enabled val state = statusText() - Log.i(TAG, "ParticleValidation30_03: Test App switching to $state") + styleReady = false + styleLoadEpoch += 1 + Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$styleLoadEpoch reason=$reason") maplibreMap.setStyle(Style.Builder().fromJson(validationStyle(active, enabled))) { + styleReady = true status.text = state } } @@ -140,15 +145,20 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private fun setTimeMix(value: Float) { timeMix = value.coerceIn(0.0f, 1.0f) val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID) - if (layer == null || !enabled || !timeWindowMode) { - timeWindowMode = true - fixtureMode = FIXTURE_UNIFORM_EAST - loadValidationStyle(active, enabled) + val rejectReason = when { + !styleReady -> "STYLE_LOADING" + layer == null -> "LAYER_NOT_READY" + !enabled -> "DISABLED" + !timeWindowMode -> "TIME_WINDOW_OFF" + else -> null + } + if (rejectReason != null) { + Log.i(TAG, "ParticleGPU: event=MIX_REJECTED reason=$rejectReason styleEpoch=$styleLoadEpoch") return } layer.setProperties(PaintPropertyValue("raster-particle-time-mix", timeMix)) status.text = statusText() - Log.i(TAG, "ParticleGPU: test timeMix=$timeMix; expect no Field upload or simulation rebuild") + Log.i(TAG, "ParticleGPU: event=MIX_PROPERTY_ONLY value=$timeMix styleEpoch=$styleLoadEpoch") } private fun statusText(): String { diff --git a/shaders/particle_gpu_validation.fragment.glsl b/shaders/particle_gpu_validation.fragment.glsl index e97f8113f..79c7c6171 100644 --- a/shaders/particle_gpu_validation.fragment.glsl +++ b/shaders/particle_gpu_validation.fragment.glsl @@ -33,7 +33,10 @@ void main() { position = v_uv; velocity = vec2(0.0); } - position = fract(position + velocity * u_pass.y * 0.004); + // Field V remains north-positive. Mercator Y increases southward, so + // convert only at the physical-velocity -> map-displacement boundary. + vec2 mercatorVelocity = vec2(velocity.x, -velocity.y); + position = fract(position + mercatorVelocity * u_pass.y * 0.004); if (any(isnan(velocity))) position = v_uv; fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0); } else if (mode < 1.5) { diff --git a/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp b/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp index e9b1e3462..2e7667ad5 100644 --- a/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp +++ b/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp @@ -193,7 +193,8 @@ ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fix if (bytes.size() != expected || member(integrity, "fieldByteLength").GetUint64() != expected) { throw std::invalid_argument("fixture field length mismatch: " + fixtureID); } - if (sha256(bytes) != member(integrity, "fieldSha256").GetString()) { + field.fieldSha256 = member(integrity, "fieldSha256").GetString(); + if (sha256(bytes) != field.fieldSha256) { throw std::invalid_argument("fixture field checksum mismatch: " + fixtureID); } field.velocity.resize(expected / sizeof(float)); diff --git a/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp b/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp index f2ad997e9..eb5ea26c6 100644 --- a/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp +++ b/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp @@ -21,6 +21,9 @@ struct ParticleFieldSnapshot { float noData = -9999.0f; uint64_t dataVersion = 0; uint64_t generation = 0; + // Verified integrity.fieldSha256 identifies immutable GPU-upload content + // across source reloads; generation remains a separate runtime revision. + std::string fieldSha256; // Row-major, little-endian contract decoded as interleaved canonical // U-east/V-north values. A noData component makes the cell invalid. std::vector velocity; @@ -41,6 +44,11 @@ inline bool fieldIdentityChanged(const ParticleFieldSnapshot& current, const Par current.dataVersion != next.dataVersion; } +inline bool sameFieldGpuContent(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) { + return current.fixtureID == next.fixtureID && current.dataVersion == next.dataVersion && + current.fieldSha256 == next.fieldSha256 && current.isTimeInterpolationCompatibleWith(next); +} + // A formal source time window. T0/T1 remain independent immutable snapshots; // the renderer uploads and samples them independently on the GPU. struct ParticleFieldTimeWindow { diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 770854adc..90a0aced5 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -732,10 +732,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, Log::Error(Event::Render, "ParticleGPU: rejected incompatible time-window fields"); return; } - const bool reuseOldT0 = !particle::fieldIdentityChanged(oldT0, nextT0); - const bool reuseOldT1 = !particle::fieldIdentityChanged(oldT1, nextT0); - const bool reuseNewT0 = !particle::fieldIdentityChanged(oldT0, nextT1); - const bool reuseNewT1 = !particle::fieldIdentityChanged(oldT1, nextT1); + const bool reuseOldT0 = particle::sameFieldGpuContent(oldT0, nextT0); + const bool reuseOldT1 = particle::sameFieldGpuContent(oldT1, nextT0); + const bool reuseNewT0 = particle::sameFieldGpuContent(oldT0, nextT1); + const bool reuseNewT1 = particle::sameFieldGpuContent(oldT1, nextT1); const bool windowOverlap = reuseOldT0 || reuseOldT1 || reuseNewT0 || reuseNewT1; const auto upload = [&](const particle::ParticleFieldSnapshot& field, std::shared_ptr& image, @@ -754,13 +754,13 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, particle::ParticleFieldSnapshot& assigned, std::shared_ptr& image, gfx::Texture2DPtr& texture) { - if (!particle::fieldIdentityChanged(oldT0, wanted)) { + if (particle::sameFieldGpuContent(oldT0, wanted)) { assigned = oldT0; image = resources->fieldImage; texture = resources->fieldTexture; return true; } - if (!particle::fieldIdentityChanged(oldT1, wanted)) { + if (particle::sameFieldGpuContent(oldT1, wanted)) { assigned = oldT1; image = resources->nextFieldImage; texture = resources->nextFieldTexture; diff --git a/test/style/source.test.cpp b/test/style/source.test.cpp index 480d1dd31..5e17888df 100644 --- a/test/style/source.test.cpp +++ b/test/style/source.test.cpp @@ -219,6 +219,18 @@ TEST(Source, RasterArrayFieldIdentityIncludesFixtureAndDataVersion) { EXPECT_FALSE(particle::fieldIdentityChanged(east, east)); } +TEST(Source, RasterArrayGpuContentIdentityExcludesRuntimeGeneration) { + const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 100); + auto reloadedEast = east; + reloadedEast.generation = 200; + auto changed = reloadedEast; + ++changed.dataVersion; + + EXPECT_TRUE(particle::fieldIdentityChanged(east, reloadedEast)); + EXPECT_TRUE(particle::sameFieldGpuContent(east, reloadedEast)); + EXPECT_FALSE(particle::sameFieldGpuContent(east, changed)); +} + TEST(Source, RasterArrayTimeWindowKeepsIndependentCompatibleSnapshots) { const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 11); const auto north = particle::OfflineFixtureParticleAdapter::load("uniform-north", 12); diff --git a/粒子开发记录.md b/粒子开发记录.md index b1d5390e8..549871126 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -238,3 +238,9 @@ - EAST/NORTH、T0/T1、ADVANCE、MIX、ACTIVE/STATIC、DISABLE/ENABLE、GLOBAL/OCEAN、DATELINE/WORLD WRAP 的 16 个既有 ID/click binding 均保留;状态改为单行 mask/runtime/field/mix 表达,不改变 40-02 业务语义。 - 未修改 C++ Core、shader、GPU、simulation、trail、composite、mask、Gradle 或 ABI。静态 ID/binding 核对与 diff check 通过;未运行 Android Gradle、ADB 或设备。状态为 `IMPLEMENTED / WAITING FOR ANDROID UI VALIDATION`。 - 实现提交为 `f4d519ca789c0488d6b9d8259427ac070b15eb63`(`ui: compact particle validation controls`);文档收尾提交另行记录。 + +## 2026-09-02 — STEP-043 / 40-02C 最终修正 + +- 修正 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 待用户验收。