diff --git a/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md new file mode 100644 index 000000000..1f6d9ca38 --- /dev/null +++ b/docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md @@ -0,0 +1,39 @@ +# STEP-044 Android OpenGL 粒子数量容量基线 + +- 状态:`IMPLEMENTED / WAITING FOR ANDROID VALIDATION` +- 前置:STEP-041(40-02)已 `CLOSED / PASS`。 +- 目标:在唯一正式链路 `RasterArraySource -> RasterParticleLayer -> GPU State A/B -> Trail A/B -> Composite` 上,为 Android OpenGL 提供可重复的 State 粒子容量档位;本 Step 不宣称任何容量实测结果。 + +## 实现 + +- 正式 `RasterParticleLayer` 新增常量 paint property `raster-particle-particle-count`,默认 `256`。Core 只接受 `256..1048576` 的平方数,保证 Renderer 总能将其映射为合法的正方形 State texture。 +- 数量的实际承载点是正式 GPU State A/B render target:默认 `16x16=256`,可切换为 `32x32=1024`、`64x64=4096`、`128x128=16384`、`256x256=65536`、`512x512=262144`(Core 支持的中间平方值)和页面最高 `1024x1024=1048576`。每个 State texel 继续运行既有 State shader,故 effective count 即该网格面积。 +- Trail 仍只读取四个确定性 State texel 作为低开销视觉健康指示;这不是第二个 simulation,也不会改变 State A/B、Trail A/B、Composite 或 40-02 Field/MIX 语义。 +- Android 验证页提供 `256 / 1K / 4K / 16K / 64K / 1M` 档位。请求通过既有 `MapLibreMap -> NativeMapView -> JNI -> Style::getLayer()` 薄桥,并经强类型 `RasterParticleLayer::setParticleCount(uint32_t)` 生效;没有 Java raster-particle Layer peer。 +- 切换数量只替换 State A/B,并执行一次 `SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE` 以 seed 新网格、清理旧 Trail。Field T0/T1、GPU content reuse、MIX、interpolation、mask、viewport 逻辑和 layer instance/resource generation 均不因该属性本身改变。 + +## 观测契约 + +- Android host:`PARTICLE_COUNT_REQUEST`、`PARTICLE_COUNT_APPLIED`、拒绝原因;状态行显示当前 count。 +- Core/JNI:`STYLE_PROPERTY_SET result=SUCCESS|INVALID_PARTICLE_COUNT`。 +- Renderer:`PARTICLE_RESOURCES_CREATE|REBUILD|RELEASE`,每条含 requested/effective count、State grid 和 resource generation;现有低频 schedule/pass/commit 继续证明 simulation、trail 与 composite 持续工作。 +- State target 创建失败时记录 `PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED`,旧资源/旧 effective count 保持。 + +## Android 待验收矩阵 + +| 粒子数量 | 是否持续运行 | 画面/帧表现 | 内存趋势 | GL/GPU 错误 | 生命周期异常 | 结论 | +|---:|---|---|---|---|---|---| +| 256 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 1,024 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 4,096 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 16,384 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 65,536 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | +| 1,048,576 | 待测 | 待测 | 待测 | 待测 | 待测 | 待测 | + +## 本地验证边界 + +- Linux/OpenGL:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o` 精确编译通过;新增 Layer 测试覆盖默认、合法及非法 count 的 Core guard。 +- `clang-format` 与 `git diff --check` 通过。 +- 未运行 Android Gradle、ADB、APK 安装或设备测试;未加入/执行 Jest。 + +Android 用户完成各档长时间测试、GL/GPU 日志和矩阵后,才可判定稳定区间、性能下降区间和压力边界。 diff --git a/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md new file mode 100644 index 000000000..700883689 --- /dev/null +++ b/docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md @@ -0,0 +1,48 @@ +# 40-03 Android OpenGL 粒子数量容量基线 Codex 完成报告 + +## 状态 + +`40-03:IMPLEMENTED / WAITING FOR ANDROID VALIDATION` + +40-02 的已关闭结论未改变。本报告只交付正式 GPU 链的容量测试入口;Android x86_64 实机矩阵尚无结果,不能写 `PASS` 或声称存在稳定容量区间。 + +## 已完成 + +1. 当前正式粒子数的最终控制点是 `RenderRasterParticleLayer` 的 State A/B render target。原固定 `16x16` 即 `256` 个 State shader texel;每个 texel保存并推进一粒子的 RG position、B age、A validity。 +2. 新增 `RasterParticleLayer::setParticleCount(uint32_t)` 与 `raster-particle-particle-count`。默认仍为 256,Core 接受 256 到 1,048,576 的平方数,实际生效 count 为 State grid 面积。 +3. Android 验证页增加 `256 / 1K / 4K / 16K / 64K / 1M`。请求经既有 JNI float-property 薄桥进入强类型 Core setter,成功后才更新 host count;没有 CPU simulation、第二 Layer 或 Java Layer peer。 +4. 切换 count 会释放并替换 State A/B,记录一次有理由的 `SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE`,随后重新 seed State/Trail。它不会执行 Style reload、Field upload、MIX 更新或时间窗推进;Field T0/T1、SHA-256 reuse、interpolation、mask 和 40-02 生命周期语义保持。 +5. 新增低频资源诊断:`PARTICLE_COUNT_REQUEST/APPLIED/REJECTED`、`PARTICLE_RESOURCES_CREATE/REBUILD/RELEASE`、`STYLE_PROPERTY_SET`。它们含 requested/effective count、State grid、resource generation 或失败原因;原有采样 schedule/pass/commit 日志继续用于确认 simulation、trail 与 composite。 + +Trail 仍将四个确定性 State texel投射为可视 marker/trail。这是正式 State simulation 的低开销可视健康指示,不改变实际 State texel 并行数,也没有复制任何 simulation。 + +## 本地检查 + +- `clang-format`:受影响 C++ 文件已格式化。 +- Linux/OpenGL 精确编译:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o` 均通过。 +- 新增 `Layer.RasterParticleCountProperties` 测试对象已成功编译,覆盖默认 256、合法 65,536、非平方与越界拒绝;完整测试二进制在当前 build tree 不存在,因此未将其运行误报为通过。 +- `git diff --check`:通过。 +- Android Gradle:未执行。ADB / APK 安装 / 设备测试:未执行。Jest:未加入、未执行。 + +## 未完成/等待用户验证 + +- 六个档位的持续运行、帧表现、内存趋势、GL/GPU 错误与 crash 结果。 +- 稳定运行区间、性能明显下降区间和当前压力边界。 +- Android Kotlin/JNI 的最终 Android toolchain 编译和 x86_64 OpenGL 实测。 + +## Windows 11 手工验证 + +在 PowerShell 或 CMD 中依次执行: + +```powershell +cd /mnt/sda1/MapLibreParticleLab/platform/android +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pandroid.injected.build.abi=x86_64 --console=plain +adb -s 192.168.1.146 install -r .\MapLibreAndroidTestApp\build\intermediates\apk\opengl\debug\MapLibreAndroidTestApp-opengl-debug.apk +adb -s 192.168.1.146 logcat -c +adb -s 192.168.1.146 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity +adb -s 192.168.1.146 logcat -v time ParticleValidation:I ParticleGPU:I "*:S" +``` + +页面默认 256;依次点击 `1K`、`4K`、`16K`、`64K`、`1M`,每档至少观察持续推进、四个代表性 trail、交互、内存和日志。每次切换应见 `PARTICLE_COUNT_REQUEST`、JNI `STYLE_PROPERTY_SET result=SUCCESS`、`PARTICLE_RESOURCES_RELEASE`、`SIMULATION_RESET reason=PARTICLE_COUNT_CHANGE` 与 `PARTICLE_RESOURCES_REBUILD`;不应见 `FIELD_UPLOAD`、Style reload 或无理由 reset。若高档创建失败,应记录 `PARTICLE_COUNT_REJECTED` 而保持旧档稳定运行。 + +APK 路径:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk`。 diff --git a/include/mbgl/style/layers/raster_particle_layer.hpp b/include/mbgl/style/layers/raster_particle_layer.hpp index d497e0c9f..ea50e57dc 100644 --- a/include/mbgl/style/layers/raster_particle_layer.hpp +++ b/include/mbgl/style/layers/raster_particle_layer.hpp @@ -2,6 +2,8 @@ #include +#include + namespace mbgl::style { class RasterParticleLayer final : public Layer { @@ -14,6 +16,8 @@ public: void setMaskMode(uint8_t); float getTimeMix() const; void setTimeMix(float); + uint32_t getParticleCount() const; + void setParticleCount(uint32_t); class Impl; const Impl& impl() const; Mutable mutableImpl() const; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index 8f86e2573..3470bf9e4 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -1113,7 +1115,8 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env, const jni::jfloat value) { auto* layer = map->getStyle().getLayer(jni::Make(env, layerId)); if (!layer) return false; - if (jni::Make(env, propertyName) != "raster-particle-time-mix") { + const auto property = jni::Make(env, propertyName); + if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count") { mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY"); return false; } @@ -1122,11 +1125,23 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env, mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_LAYER"); return false; } - particleLayer->setTimeMix(value); + if (property == "raster-particle-time-mix") { + particleLayer->setTimeMix(value); + } else { + if (!std::isfinite(value) || value < 0.0f || std::floor(value) != value) { + mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT"); + return false; + } + const auto requestedCount = static_cast(value); + particleLayer->setParticleCount(requestedCount); + if (particleLayer->getParticleCount() != requestedCount) { + mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT"); + return false; + } + } mbgl::Log::Info(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET layerId=" + jni::Make(env, layerId) + - " property=" + jni::Make(env, propertyName) + " value=" + std::to_string(value) + - " result=SUCCESS"); + " property=" + property + " value=" + std::to_string(value) + " result=SUCCESS"); return true; } 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 b2b4b24da..8da7e2d5e 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 @@ -23,6 +23,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var maskMode = MASK_GLOBAL private var fixtureMode = FIXTURE_UNIFORM_EAST private var timeMix = 0.0f + private var particleCount = PARTICLE_COUNT_256 private var timeWindowMode = false private var styleLoadEpoch = 0L private var styleReady = false @@ -33,6 +34,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST timeMix = savedInstanceState?.getFloat(STATE_TIME_MIX, 0.0f) ?: 0.0f + particleCount = savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256 timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false super.onCreate(savedInstanceState) Log.i( @@ -96,6 +98,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { findViewById