fix: finalize GPU particle time interpolation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -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。
|
||||
|
||||
## 本轮实现
|
||||
|
||||
|
||||
@@ -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。
|
||||
@@ -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 日志/视觉验收。
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<float> 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 {
|
||||
|
||||
@@ -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<PremultipliedImage>& image,
|
||||
@@ -754,13 +754,13 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
particle::ParticleFieldSnapshot& assigned,
|
||||
std::shared_ptr<PremultipliedImage>& 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 待用户验收。
|
||||
|
||||
Reference in New Issue
Block a user