feat: add Android particle capacity baseline
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:
39
docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md
Normal file
39
docs/50_Step记录/STEP-044_Android_OpenGL粒子数量容量基线_v1.md
Normal file
@@ -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 日志和矩阵后,才可判定稳定区间、性能下降区间和压力边界。
|
||||
48
docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md
Normal file
48
docs/报告/40-03_Android_OpenGL粒子数量容量基线_Codex完成报告_v1.md
Normal file
@@ -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`。
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <mbgl/style/layer.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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<Impl> mutableImpl() const;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <list>
|
||||
#include <tuple>
|
||||
@@ -1113,7 +1115,8 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
|
||||
const jni::jfloat value) {
|
||||
auto* layer = map->getStyle().getLayer(jni::Make<std::string>(env, layerId));
|
||||
if (!layer) return false;
|
||||
if (jni::Make<std::string>(env, propertyName) != "raster-particle-time-mix") {
|
||||
const auto property = jni::Make<std::string>(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<uint32_t>(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<std::string>(env, layerId) +
|
||||
" property=" + jni::Make<std::string>(env, propertyName) + " value=" + std::to_string(value) +
|
||||
" result=SUCCESS");
|
||||
" property=" + property + " value=" + std::to_string(value) + " result=SUCCESS");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Button>(R.id.validationMix0).setOnClickListener { setTimeMix(0.0f) }
|
||||
findViewById<Button>(R.id.validationMix50).setOnClickListener { setTimeMix(0.5f) }
|
||||
findViewById<Button>(R.id.validationMix100).setOnClickListener { setTimeMix(1.0f) }
|
||||
findViewById<Button>(R.id.validationParticle256).setOnClickListener { setParticleCount(PARTICLE_COUNT_256) }
|
||||
findViewById<Button>(R.id.validationParticle1024).setOnClickListener { setParticleCount(PARTICLE_COUNT_1024) }
|
||||
findViewById<Button>(R.id.validationParticle4096).setOnClickListener { setParticleCount(PARTICLE_COUNT_4096) }
|
||||
findViewById<Button>(R.id.validationParticle16384).setOnClickListener { setParticleCount(PARTICLE_COUNT_16384) }
|
||||
findViewById<Button>(R.id.validationParticle65536).setOnClickListener { setParticleCount(PARTICLE_COUNT_65536) }
|
||||
findViewById<Button>(R.id.validationParticle1048576).setOnClickListener { setParticleCount(PARTICLE_COUNT_1048576) }
|
||||
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_NORTH
|
||||
@@ -164,6 +172,30 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleGPU: event=MIX_PROPERTY_ONLY value=$timeMix styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun setParticleCount(requestedParticleCount: Int) {
|
||||
if (!styleReady) {
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STYLE_LOADING requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
if (!enabled) {
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=DISABLED requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REQUEST requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
|
||||
if (!maplibreMap.setStyleLayerFloatProperty(
|
||||
PARTICLE_LAYER_ID,
|
||||
"raster-particle-particle-count",
|
||||
requestedParticleCount.toFloat()
|
||||
)
|
||||
) {
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=CORE_PROPERTY_REJECTED requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
particleCount = requestedParticleCount
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_APPLIED requestedParticleCount=$requestedParticleCount effectiveParticleCount=$particleCount styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun statusText(): String {
|
||||
val mask = when (maskMode) {
|
||||
MASK_OCEAN_A -> "OCEAN A"
|
||||
@@ -178,7 +210,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
} else {
|
||||
"EAST"
|
||||
}
|
||||
return "$mask · $runtime · $fields · MIX ${(timeMix * 100).toInt()}%"
|
||||
return "$mask · $runtime · $fields · MIX ${(timeMix * 100).toInt()}% · $particleCount particles"
|
||||
}
|
||||
|
||||
private fun validationStyle(active: Boolean, enabled: Boolean): String {
|
||||
@@ -191,7 +223,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
val particleLayer = if (enabled) {
|
||||
"""{"id":"particle-raster-core","type":"raster-particle","source":"particle-fixture",
|
||||
"paint":{"raster-particle-active":$active,"raster-particle-mask-mode":$maskMode,
|
||||
"raster-particle-time-mix":$timeMix}},"""
|
||||
"raster-particle-time-mix":$timeMix,"raster-particle-particle-count":$particleCount}},"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -239,6 +271,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
outState.putInt(STATE_MASK_MODE, maskMode)
|
||||
outState.putInt(STATE_FIXTURE_MODE, fixtureMode)
|
||||
outState.putFloat(STATE_TIME_MIX, timeMix)
|
||||
outState.putInt(STATE_PARTICLE_COUNT, particleCount)
|
||||
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
mapView.onSaveInstanceState(outState)
|
||||
@@ -256,6 +289,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val STATE_MASK_MODE = "particle_validation.mask_mode"
|
||||
private const val STATE_FIXTURE_MODE = "particle_validation.fixture_mode"
|
||||
private const val STATE_TIME_MIX = "particle_validation.time_mix"
|
||||
private const val STATE_PARTICLE_COUNT = "particle_validation.particle_count"
|
||||
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
|
||||
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
||||
private const val MASK_GLOBAL = 0
|
||||
@@ -263,5 +297,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val MASK_OCEAN_B = 2
|
||||
private const val FIXTURE_UNIFORM_EAST = 0
|
||||
private const val FIXTURE_UNIFORM_NORTH = 1
|
||||
private const val PARTICLE_COUNT_256 = 256
|
||||
private const val PARTICLE_COUNT_1024 = 1024
|
||||
private const val PARTICLE_COUNT_4096 = 4096
|
||||
private const val PARTICLE_COUNT_16384 = 16384
|
||||
private const val PARTICLE_COUNT_65536 = 65536
|
||||
private const val PARTICLE_COUNT_1048576 = 1048576
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,26 @@
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button android:id="@+id/validationParticle256" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_256" />
|
||||
<Button android:id="@+id/validationParticle1024" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_1024" />
|
||||
<Button android:id="@+id/validationParticle4096" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_4096" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button android:id="@+id/validationParticle16384" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_16384" />
|
||||
<Button android:id="@+id/validationParticle65536" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_65536" />
|
||||
<Button android:id="@+id/validationParticle1048576" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_1048576" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="34dp"
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
<string name="particle_validation_mix_0">MIX 0%</string>
|
||||
<string name="particle_validation_mix_50">MIX 50%</string>
|
||||
<string name="particle_validation_mix_100">MIX 100%</string>
|
||||
<string name="particle_validation_particles_256">256</string>
|
||||
<string name="particle_validation_particles_1024">1K</string>
|
||||
<string name="particle_validation_particles_4096">4K</string>
|
||||
<string name="particle_validation_particles_16384">16K</string>
|
||||
<string name="particle_validation_particles_65536">64K</string>
|
||||
<string name="particle_validation_particles_1048576">1M</string>
|
||||
<string name="particle_validation_global_active_status">40-01 GLOBAL ACTIVE: offline field simulation and current-frame composite.</string>
|
||||
<string name="particle_validation_global_static_status">40-01 GLOBAL STATIC: zero-step bootstrap and frozen composite.</string>
|
||||
<string name="particle_validation_ocean_a_status">40-01 OCEAN_ONLY_A: shared coastal-mask consumer.</string>
|
||||
|
||||
@@ -42,7 +42,6 @@ namespace mbgl {
|
||||
namespace {
|
||||
|
||||
constexpr auto ShaderName = "ParticleGpuValidationShader";
|
||||
constexpr Size StateSize{16, 16};
|
||||
constexpr Size MaskSize{16, 16};
|
||||
|
||||
std::atomic_uint64_t nextGpuGeneration{0};
|
||||
@@ -56,16 +55,17 @@ struct ParticleSettings {
|
||||
uint8_t fixtureMode;
|
||||
bool usesFormalSource;
|
||||
float timeMix;
|
||||
uint32_t particleCount;
|
||||
};
|
||||
|
||||
ParticleSettings particleSettings(const Immutable<style::Layer::Impl>& value) {
|
||||
if (value->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo()) {
|
||||
const auto& layer = static_cast<const style::RasterParticleLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, 0, true, layer.timeMix};
|
||||
return {layer.active, layer.maskMode, 0, true, layer.timeMix, layer.particleCount};
|
||||
}
|
||||
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
||||
const auto& layer = static_cast<const style::ParticleValidationLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f};
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256};
|
||||
}
|
||||
|
||||
enum class ValidationPass : uint8_t {
|
||||
@@ -83,6 +83,11 @@ Size validViewportSize(const Size size) {
|
||||
return {std::max<uint32_t>(1, size.width), std::max<uint32_t>(1, size.height)};
|
||||
}
|
||||
|
||||
Size particleStateSize(const uint32_t particleCount) {
|
||||
const auto side = static_cast<uint32_t>(std::sqrt(static_cast<float>(particleCount)));
|
||||
return {side, side};
|
||||
}
|
||||
|
||||
const char* fixtureID(const uint8_t mode) {
|
||||
switch (mode) {
|
||||
case 0:
|
||||
@@ -141,6 +146,15 @@ void removeTargets(const std::vector<RenderTargetPtr>& targets, UniqueChangeRequ
|
||||
}
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
void removeTargets(const std::array<RenderTargetPtr, N>& targets, UniqueChangeRequestVec& changes) {
|
||||
for (const auto& target : targets) {
|
||||
if (target) {
|
||||
changes.emplace_back(std::make_unique<RemoveRenderTargetRequest>(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ParticleGpuResources {
|
||||
@@ -172,9 +186,11 @@ public:
|
||||
uint64_t fieldUploadCount = 0;
|
||||
uint64_t resetCount = 0;
|
||||
float timeMix = 0.0f;
|
||||
uint32_t particleCount = 0;
|
||||
uint8_t fixtureMode = 255;
|
||||
uint8_t loggedMaskMode = 255;
|
||||
Size trailSize{};
|
||||
Size stateSize{};
|
||||
|
||||
std::array<RenderTargetPtr, 2> stateTargets;
|
||||
std::array<RenderTargetPtr, 2> trailTargets;
|
||||
@@ -516,8 +532,12 @@ bool buildQuad(gfx::Context& context,
|
||||
RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable<style::Layer::Impl> impl_)
|
||||
: RenderLayer([&] {
|
||||
const auto settings = particleSettings(impl_);
|
||||
return makeMutable<style::ParticleRenderLayerProperties>(
|
||||
std::move(impl_), settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix);
|
||||
return makeMutable<style::ParticleRenderLayerProperties>(std::move(impl_),
|
||||
settings.active,
|
||||
settings.maskMode,
|
||||
settings.fixtureMode,
|
||||
settings.timeMix,
|
||||
settings.particleCount);
|
||||
}()) {
|
||||
renderLayerInstanceId = ++nextRenderLayerInstanceId;
|
||||
formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo();
|
||||
@@ -544,7 +564,7 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId));
|
||||
const auto settings = particleSettings(baseImpl);
|
||||
auto properties = makeMutable<style::ParticleRenderLayerProperties>(
|
||||
baseImpl, settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix);
|
||||
baseImpl, settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix, settings.particleCount);
|
||||
passes = RenderPass::Translucent;
|
||||
properties->renderPasses = mbgl::underlying_type(passes);
|
||||
evaluatedProperties = std::move(properties);
|
||||
@@ -554,7 +574,7 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
" maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + " fixture=" +
|
||||
(formalSource && sourceField ? sourceField->fixtureID : fixtureID(settings.fixtureMode)) +
|
||||
" layerIndex=" + std::to_string(getLayerIndex()) + " timeMix=" + std::to_string(settings.timeMix) +
|
||||
" stage=40-02");
|
||||
" particleCount=" + std::to_string(settings.particleCount) + " stage=40-03");
|
||||
}
|
||||
|
||||
bool RenderRasterParticleLayer::needsRepaint() const {
|
||||
@@ -572,6 +592,10 @@ void RenderRasterParticleLayer::markLayerRenderable(bool willRender, UniqueChang
|
||||
|
||||
void RenderRasterParticleLayer::layerRemoved(UniqueChangeRequestVec& changes) {
|
||||
if (gpuResources) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_RESOURCES_RELEASE reason=LAYER_REMOVED particleCount=" +
|
||||
std::to_string(gpuResources->particleCount) +
|
||||
" resourceGeneration=" + std::to_string(gpuResources->generation));
|
||||
removeTargets(gpuResources->registeredTargets, changes);
|
||||
gpuResources->registeredTargets.clear();
|
||||
}
|
||||
@@ -581,6 +605,10 @@ void RenderRasterParticleLayer::layerRemoved(UniqueChangeRequestVec& changes) {
|
||||
void RenderRasterParticleLayer::markContextDestroyed() {
|
||||
if (gpuResources) {
|
||||
gpuResources->contextDestroyed = true;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_RESOURCES_RELEASE reason=CONTEXT_DESTROYED particleCount=" +
|
||||
std::to_string(gpuResources->particleCount) +
|
||||
" resourceGeneration=" + std::to_string(gpuResources->generation));
|
||||
Log::Info(
|
||||
Event::Render,
|
||||
"ParticleValidation30_02: context lost discard generation=" + std::to_string(gpuResources->generation) +
|
||||
@@ -618,6 +646,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
const bool active = properties.active;
|
||||
const uint8_t maskMode = properties.maskMode;
|
||||
const uint8_t fixtureMode = properties.fixtureMode;
|
||||
const uint32_t particleCount = properties.particleCount;
|
||||
auto resources = gpuResources;
|
||||
if (resources && resources->contextDestroyed) {
|
||||
// VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale
|
||||
@@ -634,9 +663,11 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
owned->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
|
||||
RenderStaticData::heatmapTextureVertices());
|
||||
owned->segments = RenderStaticData::heatmapTextureSegments();
|
||||
owned->particleCount = particleCount;
|
||||
owned->stateSize = particleStateSize(owned->particleCount);
|
||||
for (uint8_t i = 0; i < 2; ++i) {
|
||||
owned->stateTargets[i] = createValidationTarget(
|
||||
context, StateSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest);
|
||||
context, owned->stateSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest);
|
||||
}
|
||||
if (!owned->shader || !owned->stateTargets[0] || !owned->stateTargets[1]) {
|
||||
Log::Error(Event::Render, "ParticleValidation30_02: State resource creation failed");
|
||||
@@ -710,7 +741,14 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
resources = gpuResources;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) +
|
||||
" size=16x16 independent=true");
|
||||
" size=" + std::to_string(resources->stateSize.width) + "x" +
|
||||
std::to_string(resources->stateSize.height) + " independent=true");
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_RESOURCES_CREATE reason=RESOURCE_CREATE requestedParticleCount=" +
|
||||
std::to_string(particleCount) +
|
||||
" effectiveParticleCount=" + std::to_string(resources->particleCount) + " stateGrid=" +
|
||||
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation));
|
||||
Log::Info(
|
||||
Event::Render,
|
||||
"ParticleField: fixture loaded id=" + resources->field.fixtureID + " type=" + resources->field.fieldType +
|
||||
@@ -745,6 +783,42 @@ 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");
|
||||
}
|
||||
|
||||
if (resources->particleCount != particleCount) {
|
||||
const auto previousCount = resources->particleCount;
|
||||
const auto previousSize = resources->stateSize;
|
||||
const auto requestedSize = particleStateSize(particleCount);
|
||||
std::array<RenderTargetPtr, 2> replacementTargets;
|
||||
for (uint8_t i = 0; i < 2; ++i) {
|
||||
replacementTargets[i] = createValidationTarget(
|
||||
context, requestedSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest);
|
||||
}
|
||||
if (!replacementTargets[0] || !replacementTargets[1]) {
|
||||
Log::Error(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED "
|
||||
"requestedParticleCount=" +
|
||||
std::to_string(particleCount) +
|
||||
" effectiveParticleCount=" + std::to_string(resources->particleCount) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation));
|
||||
return;
|
||||
}
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_RESOURCES_RELEASE reason=PARTICLE_COUNT_CHANGE particleCount=" +
|
||||
std::to_string(previousCount) + " stateGrid=" + std::to_string(previousSize.width) + "x" +
|
||||
std::to_string(previousSize.height) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation));
|
||||
removeTargets(resources->stateTargets, changes);
|
||||
resources->stateTargets = std::move(replacementTargets);
|
||||
resources->particleCount = particleCount;
|
||||
resources->stateSize = requestedSize;
|
||||
resources->resetSequence("PARTICLE_COUNT_CHANGE");
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: event=PARTICLE_RESOURCES_REBUILD reason=PARTICLE_COUNT_CHANGE requestedParticleCount=" +
|
||||
std::to_string(particleCount) +
|
||||
" effectiveParticleCount=" + std::to_string(resources->particleCount) + " stateGrid=" +
|
||||
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation));
|
||||
}
|
||||
|
||||
const bool sourceChanged = formalSource && sourceField && sourceNextField &&
|
||||
(particle::fieldIdentityChanged(resources->field, *sourceField) ||
|
||||
particle::fieldIdentityChanged(resources->nextField, *sourceNextField));
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
|
||||
#include <mbgl/style/layer_properties.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mbgl::style {
|
||||
|
||||
class ParticleRenderLayerProperties final : public LayerProperties {
|
||||
public:
|
||||
ParticleRenderLayerProperties(
|
||||
Immutable<Layer::Impl> impl, bool active_, uint8_t maskMode_, uint8_t fixtureMode_, float timeMix_)
|
||||
ParticleRenderLayerProperties(Immutable<Layer::Impl> impl,
|
||||
bool active_,
|
||||
uint8_t maskMode_,
|
||||
uint8_t fixtureMode_,
|
||||
float timeMix_,
|
||||
uint32_t particleCount_)
|
||||
: LayerProperties(std::move(impl)),
|
||||
active(active_),
|
||||
maskMode(maskMode_),
|
||||
fixtureMode(fixtureMode_),
|
||||
timeMix(timeMix_) {}
|
||||
timeMix(timeMix_),
|
||||
particleCount(particleCount_) {}
|
||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||
bool active;
|
||||
uint8_t maskMode;
|
||||
uint8_t fixtureMode;
|
||||
float timeMix;
|
||||
uint32_t particleCount;
|
||||
};
|
||||
|
||||
} // namespace mbgl::style
|
||||
|
||||
@@ -12,6 +12,15 @@ namespace {
|
||||
constexpr const char* ActiveProperty = "raster-particle-active";
|
||||
constexpr const char* MaskModeProperty = "raster-particle-mask-mode";
|
||||
constexpr const char* TimeMixProperty = "raster-particle-time-mix";
|
||||
constexpr const char* ParticleCountProperty = "raster-particle-particle-count";
|
||||
constexpr uint32_t MinimumParticleCount = 256;
|
||||
constexpr uint32_t MaximumParticleCount = 1024 * 1024;
|
||||
|
||||
bool isSupportedParticleCount(const uint32_t count) {
|
||||
if (count < MinimumParticleCount || count > MaximumParticleCount) return false;
|
||||
const auto side = static_cast<uint32_t>(std::sqrt(static_cast<float>(count)));
|
||||
return side * side == count;
|
||||
}
|
||||
const LayerTypeInfo typeInfo{.type = "raster-particle",
|
||||
.source = LayerTypeInfo::Source::Required,
|
||||
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
|
||||
@@ -71,6 +80,17 @@ void RasterParticleLayer::setTimeMix(float timeMix) {
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
uint32_t RasterParticleLayer::getParticleCount() const {
|
||||
return impl().particleCount;
|
||||
}
|
||||
void RasterParticleLayer::setParticleCount(const uint32_t particleCount) {
|
||||
if (isSupportedParticleCount(particleCount) && particleCount != getParticleCount()) {
|
||||
auto next = mutableImpl();
|
||||
next->particleCount = particleCount;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const std::string& name,
|
||||
const conversion::Convertible& value) {
|
||||
conversion::Error error;
|
||||
@@ -94,6 +114,16 @@ std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const
|
||||
setTimeMix(*converted);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (name == ParticleCountProperty) {
|
||||
auto converted = conversion::convert<float>(value, error);
|
||||
if (!converted || !std::isfinite(*converted) || std::floor(*converted) != *converted ||
|
||||
!isSupportedParticleCount(static_cast<uint32_t>(*converted))) {
|
||||
return conversion::Error{
|
||||
"raster-particle-particle-count must be a supported square count from 256 to 1048576"};
|
||||
}
|
||||
setParticleCount(static_cast<uint32_t>(*converted));
|
||||
return std::nullopt;
|
||||
}
|
||||
return conversion::Error{"layer doesn't support this property"};
|
||||
}
|
||||
StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
|
||||
@@ -101,6 +131,8 @@ StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
|
||||
if (name == MaskModeProperty)
|
||||
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
|
||||
if (name == TimeMixProperty) return StyleProperty(Value{getTimeMix()}, StyleProperty::Kind::Constant);
|
||||
if (name == ParticleCountProperty)
|
||||
return StyleProperty(Value{static_cast<uint64_t>(getParticleCount())}, StyleProperty::Kind::Constant);
|
||||
return {};
|
||||
}
|
||||
Value RasterParticleLayer::serialize() const {
|
||||
@@ -108,6 +140,7 @@ Value RasterParticleLayer::serialize() const {
|
||||
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
|
||||
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
|
||||
serializeProperty(result, getProperty(TimeMixProperty), TimeMixProperty, true);
|
||||
serializeProperty(result, getProperty(ParticleCountProperty), ParticleCountProperty, true);
|
||||
return result;
|
||||
}
|
||||
std::unique_ptr<Layer> RasterParticleLayer::cloneRef(const std::string& id) const {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <mbgl/style/layer_impl.hpp>
|
||||
#include <mbgl/style/layers/raster_particle_layer.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mbgl::style {
|
||||
|
||||
class RasterParticleLayer::Impl final : public Layer::Impl {
|
||||
@@ -15,6 +17,7 @@ public:
|
||||
bool active = true;
|
||||
uint8_t maskMode = 0;
|
||||
float timeMix = 0.0f;
|
||||
uint32_t particleCount = 256;
|
||||
DECLARE_LAYER_TYPE_INFO;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <mbgl/style/layers/line_layer_impl.hpp>
|
||||
#include <mbgl/style/layers/raster_layer.hpp>
|
||||
#include <mbgl/style/layers/raster_layer_impl.hpp>
|
||||
#include <mbgl/style/layers/raster_particle_layer.hpp>
|
||||
#include <mbgl/style/layers/symbol_layer.hpp>
|
||||
#include <mbgl/style/layers/symbol_layer_impl.hpp>
|
||||
#include <mbgl/style/style_impl.hpp>
|
||||
@@ -111,6 +112,22 @@ TEST(Layer, CircleProperties) {
|
||||
EXPECT_EQ(layer->getCircleTranslateAnchor(), translateAnchor);
|
||||
}
|
||||
|
||||
TEST(Layer, RasterParticleCountProperties) {
|
||||
RasterParticleLayer layer{"particle", "raster-array"};
|
||||
|
||||
EXPECT_EQ(256u, layer.getParticleCount());
|
||||
|
||||
layer.setParticleCount(65536);
|
||||
EXPECT_EQ(65536u, layer.getParticleCount());
|
||||
|
||||
// Non-square and out-of-range counts are rejected by the Core Layer so
|
||||
// RenderRasterParticleLayer always receives a valid State texture grid.
|
||||
layer.setParticleCount(1000);
|
||||
EXPECT_EQ(65536u, layer.getParticleCount());
|
||||
layer.setParticleCount(1048577);
|
||||
EXPECT_EQ(65536u, layer.getParticleCount());
|
||||
}
|
||||
|
||||
TEST(Layer, FillProperties) {
|
||||
auto layer = std::make_unique<FillLayer>("fill", "source");
|
||||
ASSERT_STREQ("fill", layer->getTypeInfo()->type);
|
||||
|
||||
@@ -258,3 +258,11 @@
|
||||
- STEP-043B 最终使用 `RasterParticleLayer::setTimeMix(float)`;Android x86_64 OpenGL Debug 编译、安装、启动通过,T1--T10 全部 PASS。
|
||||
- A/B→B/C GPU texture reuse、MIX property-only、DISABLE/ENABLE 生命周期通过;`uniform-north + vortex` 的 MIX 50% 局部低速/近零速是正常逐点插值。
|
||||
- 单 ABI APK 约 696.5 MiB 是独立 packaging 技术债,不阻塞本阶段。40-02 / 40-02C / STEP-043B 正式 `CLOSED / PASS`。
|
||||
|
||||
## 2026-09-03 — 40-03 Android OpenGL 粒子数量容量基线(代码阶段)
|
||||
|
||||
- 正式 RasterParticle GPU 链的粒子数实际由 State A/B render target 网格承载:既有 `16x16` 即 256 个 State shader texel。新增 `raster-particle-particle-count` Core 属性与强类型 `RasterParticleLayer::setParticleCount(uint32_t)`,只接受 256..1,048,576 的平方数,确保实际 count 与 State grid 面积一致。
|
||||
- Android Validation 页面新增 256、1K、4K、16K、64K、1M 档位;使用既有 Android→Core 薄桥,不建立 Java layer peer、CPU simulation 或第二 GPU simulation。切换只重建 State A/B、执行一次可解释的 `PARTICLE_COUNT_CHANGE` reset/reseed,Field T0/T1、MIX、interpolation、GPU content reuse、mask 与既有 lifecycle 均不改变。
|
||||
- 日志包含请求/实际 count、State grid、State target create/rebuild/release 和拒绝原因;Trail 继续用四个确定性 State texel作低开销可视健康指示,实际并行 simulation 数仍是整个 State 网格。
|
||||
- Linux OpenGL 受影响 Core 和新增 Layer test object 精确编译、C++ format、diff check 均通过;未运行 Android Gradle、ADB、APK 或设备,也未加入 Jest。
|
||||
- 当前状态只能是 `IMPLEMENTED / WAITING FOR ANDROID VALIDATION`。用户将以 x86_64 Android OpenGL 逐档填写容量矩阵后,才能确定稳定区间、性能下降区间和压力边界。
|
||||
|
||||
Reference in New Issue
Block a user