feat: establish formal raster particle source chain

This commit is contained in:
OpenAI Codex
2026-08-31 12:17:06 +08:00
parent 343cecc778
commit 39c7ae22ed
40 changed files with 811 additions and 130 deletions

View File

@@ -249,6 +249,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/gfx/vertex_buffer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/background_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/particle_validation_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/raster_particle_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/circle_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/custom_layer_factory.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/layermanager/fill_extrusion_layer_factory.hpp
@@ -361,6 +362,8 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/style/layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/background_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/particle_validation_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/raster_particle_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/sources/raster_array_source.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/circle_layer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/custom_layer_host.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/layers/custom_layer_init_parameters.hpp
@@ -506,6 +509,7 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/gfx/vertex_vector.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/background_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/particle_validation_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/raster_particle_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/circle_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_extrusion_layer_factory.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/layermanager/fill_layer_factory.cpp
@@ -585,6 +589,7 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_background_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_particle_validation_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_particle_validation_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_raster_particle_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_circle_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_circle_layer.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/render_fill_extrusion_layer.cpp
@@ -756,6 +761,9 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/particle_validation_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/particle_validation_layer_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/particle_render_layer_properties.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/raster_particle_layer.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/raster_particle_layer_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_impl.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/layers/background_layer_properties.cpp
@@ -833,6 +841,8 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/raster_dem_source.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/tile_source.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/raster_source.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/raster_array_source.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/raster_array_source_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/tile_source_impl.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/tile_source_impl.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/sources/vector_source.cpp

View File

@@ -0,0 +1,39 @@
# STEP-040 Android 离线 Fixture 正式粒子最小闭环 v1
## 状态
进行中。40-01 Stage A 的代码和 Linux 静态验证完成;最终 Android 状态固定为 `WAITING_MANUAL_VALIDATION`,尚未进入手工 APK 验收。
## 目标与基线
- 目标:建立 `RasterArraySource -> RenderRasterArraySource -> RasterParticleLayer -> RenderRasterParticleLayer` 的第一条正式离线粒子闭环,并修复 STATIC recreation 的零步 bootstrap。
- 基线:`343cecc778c3ffaa00adf94f82d7ad5f494a3877`,分支 `particle/main`。
- 前置:30-01、30-02、30-03 均为 PASS;EGL context recreation 的独立实机证据仍是已接受风险。
## 必做项
- [x] `RasterArraySource` 以 FileSource byte boundary 读取 fixture metadata 与 field,生成 immutable `ParticleFieldSnapshot`。
- [x] `RenderRasterArraySource`、`RasterParticleLayer`(`Source::Required`)和 `RenderRasterParticleLayer` 进入 Style/RenderTree 链。
- [x] 正式 RenderLayer 复用既有 GPU field、State A/B、Trail A/B、mask、camera/world-wrap/antimeridian 与 resource lifecycle;没有第二套 simulation。
- [x] Android Test App 从 APK `asset://integration/particle-fixtures/...` 提供正式 source 输入;Linux smoke helper 仍可读取仓库 fixture。
- [x] NoData cell 作为 GPU field texture alpha=0 上传;shader 遇到 invalid sample 确定性回到 seed position,不把 NoData 当速度。
- [x] STATIC recreation 仍走 zero-step State/Trail/bootstrap composite,commit 不增加 `simulationFrame`。
- [ ] Android OpenGL APK 手工编译与 T1-T12:仅 GPT 复审远端代码通过后由用户执行。
## 允许修改
隔离 particle field/source/layer/render source/render layer、最小 LayerManager/Style Source/shader 注册、Android Test App fixture assets 与本 Step 文档。
## 禁止修改
不得运行 Gradle、安装 APK、ADB/T1-T12;不得新增 Overlay、Android UI/Core 耦合、在线数据、第二套 GPU simulation,或声称 Android/Vulkan/iOS PASS。
## 构建与检查
- Linux OpenGL 受影响 Core 精确对象编译:PASS。
- `clang-format --dry-run --Werror`、`git diff --check`、fixture generator:待本轮最终收尾复跑。
- adapter smoke:`uniform-east`、`uniform-north`、`vortex` 均 valid;`nodata` valid 且有 1 个 invalid cell。
## 当前结果、阻塞与下一步
正式 Source/Layer/RenderLayer 已不依赖 source-less `particle-validation` Layer;历史 validation Layer 只保留为既有验证工具。唯一阻塞是按用户指令等待 GPT 复审新远端 commit;复审通过后才允许用户开始 Android OpenGL APK 手工验收。

View File

@@ -9,23 +9,21 @@
- 分支:`particle/main`
- 开始 HEAD:`1ec44701146925dec9a87618e3f90bee9a12073f`
- 平台范围:Android OpenGL;未修改 iOS、Vulkan、NavSea 或在线数据链。
- Test App 继续使用既有 `Particle Core Layer Validation` 入口;Core adapter 不依赖 Kotlin/Android UI。
- Test App 继续使用既有 `Particle Core Layer Validation` 入口,但其 Style 现在创建正式 `raster-array` source 和 `raster-particle` layer;Core adapter 不依赖 Kotlin/Android UI 或 Android asset API。
## 已实现的 Stage A 链路
```text
particle-fixtures/{uniform-east,uniform-north,vortex}
-> OfflineFixtureParticleAdapter
-> ParticleFieldSnapshot
-> GPU field texture
-> State A/B field sampling
-> Trail A/B consuming State A/B
-> canonical world composite / shared coastal mask
APK asset://integration/particle-fixtures/{uniform-east,uniform-north}/
-> RasterArraySource -> RenderRasterArraySource
-> ParticleFieldSnapshot -> GPU field texture
-> RasterParticleLayer (Source::Required) -> RenderRasterParticleLayer
-> State A/B field sampling -> Trail A/B -> canonical world composite / shared coastal mask
```
- fixture contract 使用 `metadata.json + field.f32`,校验 schema、id、WIND/GLOBAL、EPSG:4326、bounds、grid registration/order、interleaved UV、little-endian float32、NoData 声明、长度和 SHA-256。
- fixture contract 使用 `metadata.json + field.f32`,校验 schema、id、WIND/GLOBAL、EPSG:4326、bounds、grid registration/order、interleaved UV、little-endian float32、NoData 声明、长度和 SHA-256。Android asset 由 Test App 打包,Core 只接收 FileSource 返回的 metadata/field bytes;Linux helper 才读取仓库 `particle-fixtures/`。
- `uniform-east`、`uniform-north` 和 `vortex` 均为确定性 16x16 fixture;不会在每帧解析或上传。切换 fixture 时递增 `fieldGeneration/gpuFieldGeneration`,清空旧 trail;Camera 操作不触发 field upload。
- shader 的 State pass 实际采样 GPU field texture,Trail pass 从 State write texture 读取粒子位置,未使用 Kotlin/CPU 每帧粒子循环。
- shader 的 State pass 实际采样 GPU field texture,Trail pass 从 State write texture 读取粒子位置,未使用 Kotlin/CPU 每帧粒子循环。field texture alpha 表示 cell validity;NoData cell 上传为 alpha=0,State pass 在该 sample 上确定性 respawn 到 seed position,绝不将 `-9999` clamp 成速度。
- STATIC 不再因资源缺失提前返回。新的 context/resource generation 会建立 field、mask、trail 和 state;STATIC 首帧走零步 bootstrap,提交 state/trail slot 但不增加 `simulationFrame`,之后保持 frozen composite。
- Activity Bundle 保存/恢复 active、enabled、mask mode 与 fixture mode;GPU runtime state 不写入 Bundle。
@@ -33,11 +31,12 @@ particle-fixtures/{uniform-east,uniform-north,vortex}
| 检查 | 结果 |
|---|---|
| `ninja -C build-linux-opengl` 受影响 Core 对象 | PASS:`offline_fixture_particle_adapter.cpp`、`render_particle_validation_layer.cpp`、`shader_info.cpp`、`particle_validation_layer.cpp` |
| `ninja -C build-linux-opengl` 受影响 Core 对象 | PASS:正式 `raster_array_source.cpp`、`raster_particle_layer.cpp`、`raster_particle_layer_factory.cpp`、`render_source.cpp`、`RenderRasterParticleLayer` 实现、adapter 与 source conversion |
| `clang-format --dry-run --Werror` | PASS |
| `git diff --check` | PASS |
| `python3 tools/generate_particle_fixture_f32.py` 后 metadata JSON 与 SHA-256 | PASS |
| adapter smoke test | PASS:`uniform-east`、`uniform-north`、`vortex` 分别加载为 16x16 |
| adapter smoke test | PASS:`uniform-east`、`uniform-north`、`vortex` 分别加载为 16x16;`nodata` 为合法 16x16 field 且恰有 1 个 invalid cell |
| APK asset parity | PASS:四份 Android asset 的 metadata 与 field bytes 均与仓库 fixture 完全相同 |
首次整库构建尝试被 sandbox 默认 ccache 目录只读中断;以 `CCACHE_DIR=/tmp/maplibre-particle-ccache` 重新执行上述受影响对象检查后通过。这不是代码编译失败。
@@ -57,7 +56,7 @@ cd platform/android
- T1-T12 Android 设备验收、截图和 OpenGL shader 实机链接尚未执行。
- 独立 EGL context-loss 设备证据沿用 30-02 的 Accepted Risk;本实现已按 resource generation 重建 CPU field 的 GPU texture。
- iOS、Metal、Android Vulkan、真实数据、生产 coastline/mask、最终粒子数和性能指标均不在本 Stage A 范围。
- fixture 含 NoData cell 的实机路径尚未有单独 fixture;adapter 已校验 NoData contract,当前三份 deterministic field 均无 NoData cell。
- Android T1-T12 和实机 OpenGL shader 链接尚未执行;NoData 已有 deterministic fixture/static smoke 覆盖,但没有实机视觉证据。
## 下一步

View File

@@ -0,0 +1,15 @@
#pragma once
#include <mbgl/layermanager/layer_factory.hpp>
namespace mbgl {
class RasterParticleLayerFactory final : public LayerFactory {
public:
const style::LayerTypeInfo* getTypeInfo() const noexcept override;
std::unique_ptr<style::Layer> createLayer(const std::string&,
const style::conversion::Convertible&) noexcept override;
std::unique_ptr<RenderLayer> createRenderLayer(Immutable<style::Layer::Impl>) noexcept override;
};
} // namespace mbgl

View File

@@ -56,7 +56,12 @@ void main() {
// texture encodes canonical U-east/V-north at that geographic sample.
vec2 position = previous.rg;
if (previous.a < 0.5) position = v_uv;
vec2 velocity = texture(u_field_texture, position).rg * 2.0 - 1.0;
vec4 field = texture(u_field_texture, position);
vec2 velocity = field.rg * 2.0 - 1.0;
if (field.a < 0.5) {
position = v_uv;
velocity = vec2(0.0);
}
position = fract(position + velocity * 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);

View File

@@ -0,0 +1,28 @@
#pragma once
#include <mbgl/style/layer.hpp>
namespace mbgl::style {
class RasterParticleLayer final : public Layer {
public:
RasterParticleLayer(const std::string& layerID, const std::string& sourceID);
~RasterParticleLayer() override;
bool isActive() const;
void setActive(bool);
uint8_t getMaskMode() const;
void setMaskMode(uint8_t);
class Impl;
const Impl& impl() const;
Mutable<Impl> mutableImpl() const;
private:
explicit RasterParticleLayer(Immutable<Impl>);
std::optional<conversion::Error> setPropertyInternal(const std::string&, const conversion::Convertible&) override;
StyleProperty getProperty(const std::string&) const override;
Value serialize() const override;
std::unique_ptr<Layer> cloneRef(const std::string&) const override;
Mutable<Layer::Impl> mutableBaseImpl() const override;
};
} // namespace mbgl::style

View File

@@ -0,0 +1,43 @@
#pragma once
#include <mbgl/style/source.hpp>
#include <optional>
namespace mbgl {
class AsyncRequest;
namespace style {
class RasterArraySource final : public Source {
public:
RasterArraySource(std::string id, std::string fixtureID, std::string metadataURL, std::string fieldURL);
~RasterArraySource() override;
class Impl;
const Impl& impl() const;
void loadDescription(FileSource&) override;
bool supportsLayerType(const LayerTypeInfo*) const override;
mapbox::base::WeakPtr<Source> makeWeakPtr() override { return weakFactory.makeWeakPtr(); }
protected:
Mutable<Source::Impl> createMutable() const noexcept override;
private:
void tryLoad();
std::unique_ptr<AsyncRequest> metadataRequest;
std::unique_ptr<AsyncRequest> fieldRequest;
std::optional<std::string> metadata;
std::optional<std::vector<char>> fieldBytes;
uint64_t generation = 0;
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
};
template <>
inline bool Source::is<RasterArraySource>() const {
return getType() == SourceType::RasterArray;
}
} // namespace style
} // namespace mbgl

View File

@@ -15,7 +15,8 @@ enum class SourceType : uint8_t {
Video,
Annotations,
Image,
CustomVector
CustomVector,
RasterArray
};
enum class VisibilityType : bool {

Binary file not shown.

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "nodata",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "5756a6ac403a435ff001ff7261160de718c4a2964434ba7b9be2bfe74f071ff0"}
}

View File

@@ -20,6 +20,7 @@
#include "fill_extrusion_layer.hpp"
#include "location_indicator_layer.hpp"
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
#include <mbgl/layermanager/raster_particle_layer_factory.hpp>
namespace mbgl {
@@ -28,6 +29,7 @@ namespace android {
LayerManagerAndroid::LayerManagerAndroid() {
// VALIDATION ONLY: JSON style access; intentionally no Android product API/JNI peer.
addLayerTypeCoreOnly(std::make_unique<ParticleValidationLayerFactory>());
addLayerTypeCoreOnly(std::make_unique<RasterParticleLayerFactory>());
#if defined(MBGL_LAYER_FILL_DISABLE_RUNTIME)
addLayerTypeCoreOnly(std::make_unique<FillLayerFactory>());
#elif !defined(MBGL_LAYER_FILL_DISABLE_ALL)

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "nodata",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "5756a6ac403a435ff001ff7261160de718c4a2964434ba7b9be2bfe74f071ff0"}
}

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "uniform-east",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "0da648fdcf61b5ea6383960583bdc74287c3fa758c6e5a7eb7d234f2d7c3283f"}
}

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "uniform-north",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "20eb0da1812e616cee1740ee8e22a7bda377d6785cc6e7007fb26afa5ab255bc"}
}

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "vortex",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "aa7fd26d5e4605dc363636b99e17c6bb495d51350a5a2cd8abe5bf0ada741c5a"}
}

View File

@@ -13,7 +13,7 @@ import org.maplibre.android.maps.Style
import org.maplibre.android.style.layers.PaintPropertyValue
import org.maplibre.android.testapp.R
/** VALIDATION ONLY: Android OpenGL host for the 30-01/30-02 Core RenderLayer experiments. */
/** Android OpenGL host for the formal raster-array to raster-particle fixture path. */
class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var maplibreMap: MapLibreMap
@@ -72,10 +72,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_OCEAN_B)
}
findViewById<Button>(R.id.validationUniformEast).setOnClickListener {
if (this::maplibreMap.isInitialized) setValidationProperties(active, maskMode, FIXTURE_UNIFORM_EAST)
fixtureMode = FIXTURE_UNIFORM_EAST
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
}
findViewById<Button>(R.id.validationUniformNorth).setOnClickListener {
if (this::maplibreMap.isInitialized) setValidationProperties(active, maskMode, FIXTURE_UNIFORM_NORTH)
fixtureMode = FIXTURE_UNIFORM_NORTH
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
}
findViewById<Button>(R.id.validationDateline).setOnClickListener {
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
@@ -107,9 +109,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
return
}
layer.setProperties(
PaintPropertyValue("particle-validation-active", active),
PaintPropertyValue("particle-validation-mask-mode", maskMode),
PaintPropertyValue("particle-fixture-mode", fixtureMode)
PaintPropertyValue("raster-particle-active", active),
PaintPropertyValue("raster-particle-mask-mode", maskMode)
)
status.text = statusText()
Log.i(TAG, "ParticleRender: property update active=$active maskMode=$maskMode fixtureMode=$fixtureMode")
@@ -124,9 +125,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun validationStyle(active: Boolean, enabled: Boolean): String {
val fixtureId = fixtureId()
val particleLayer = if (enabled) {
"""{"id":"particle-validation-core","type":"particle-validation",
"paint":{"particle-validation-active":$active,"particle-validation-mask-mode":$maskMode,"particle-fixture-mode":$fixtureMode}},"""
"""{"id":"particle-raster-core","type":"raster-particle","source":"particle-fixture",
"paint":{"raster-particle-active":$active,"raster-particle-mask-mode":$maskMode}},"""
} else {
""
}
@@ -135,6 +137,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
"version": 8,
"name": "Particle Core Layer Validation",
"sources": {
"particle-fixture": {
"type": "raster-array",
"fixture": "$fixtureId",
"metadata": "asset://integration/particle-fixtures/$fixtureId/metadata.json",
"field": "asset://integration/particle-fixtures/$fixtureId/field.f32"
},
"validation-point": {
"type": "geojson",
"data": {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]}}
@@ -168,13 +176,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
mapView.onSaveInstanceState(outState)
}
private fun fixtureId(): String = when (fixtureMode) {
FIXTURE_UNIFORM_NORTH -> "uniform-north"
else -> "uniform-east"
}
companion object {
private const val TAG = "ParticleValidation"
private const val STATE_ACTIVE = "particle_validation.active"
private const val STATE_ENABLED = "particle_validation.enabled"
private const val STATE_MASK_MODE = "particle_validation.mask_mode"
private const val STATE_FIXTURE_MODE = "particle_validation.fixture_mode"
private const val PARTICLE_LAYER_ID = "particle-validation-core"
private const val PARTICLE_LAYER_ID = "particle-raster-core"
private const val MASK_GLOBAL = 0
private const val MASK_OCEAN_A = 1
private const val MASK_OCEAN_B = 2

View File

@@ -11,6 +11,7 @@
#include <mbgl/layermanager/line_layer_factory.hpp>
#include <mbgl/layermanager/location_indicator_layer_factory.hpp>
#include <mbgl/layermanager/particle_validation_layer_factory.hpp>
#include <mbgl/layermanager/raster_particle_layer_factory.hpp>
#include <mbgl/layermanager/raster_layer_factory.hpp>
#include <mbgl/layermanager/symbol_layer_factory.hpp>
#include <mbgl/util/logging.hpp>
@@ -52,6 +53,7 @@ void LayerManagerDefault::addLayerTypeCoreOnly(std::unique_ptr<mbgl::LayerFactor
LayerManagerDefault::LayerManagerDefault() {
// VALIDATION ONLY: 30-01 source-less Core RenderLayer experiment.
addLayerType(std::make_unique<ParticleValidationLayerFactory>());
addLayerType(std::make_unique<RasterParticleLayerFactory>());
#if !defined(MBGL_LAYER_FILL_DISABLE_ALL)
addLayerType(std::make_unique<FillLayerFactory>());
#endif

View File

@@ -22,7 +22,12 @@ void main() {
// texture encodes canonical U-east/V-north at that geographic sample.
vec2 position = previous.rg;
if (previous.a < 0.5) position = v_uv;
vec2 velocity = texture(u_field_texture, position).rg * 2.0 - 1.0;
vec4 field = texture(u_field_texture, position);
vec2 velocity = field.rg * 2.0 - 1.0;
if (field.a < 0.5) {
position = v_uv;
velocity = vec2(0.0);
}
position = fract(position + velocity * 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);

View File

@@ -18,8 +18,7 @@ std::unique_ptr<style::Layer> ParticleValidationLayerFactory::createLayer(
std::unique_ptr<RenderLayer> ParticleValidationLayerFactory::createRenderLayer(
Immutable<style::Layer::Impl> impl) noexcept {
assert(impl->getTypeInfo() == getTypeInfo());
return std::make_unique<RenderParticleValidationLayer>(
staticImmutableCast<style::ParticleValidationLayer::Impl>(impl));
return std::make_unique<RenderParticleValidationLayer>(std::move(impl));
}
} // namespace mbgl

View File

@@ -0,0 +1,25 @@
#include <mbgl/layermanager/raster_particle_layer_factory.hpp>
#include <mbgl/renderer/layers/render_raster_particle_layer.hpp>
#include <mbgl/style/layers/raster_particle_layer.hpp>
#include <mbgl/style/layers/raster_particle_layer_impl.hpp>
namespace mbgl {
const style::LayerTypeInfo* RasterParticleLayerFactory::getTypeInfo() const noexcept {
return style::RasterParticleLayer::Impl::staticTypeInfo();
}
std::unique_ptr<style::Layer> RasterParticleLayerFactory::createLayer(
const std::string& id, const style::conversion::Convertible& value) noexcept {
const auto source = getSource(value);
return source ? std::make_unique<style::RasterParticleLayer>(id, *source) : nullptr;
}
std::unique_ptr<RenderLayer> RasterParticleLayerFactory::createRenderLayer(
Immutable<style::Layer::Impl> impl) noexcept {
assert(impl->getTypeInfo() == getTypeInfo());
return std::make_unique<RenderRasterParticleLayer>(std::move(impl));
}
} // namespace mbgl

View File

@@ -14,7 +14,8 @@ namespace mbgl::particle {
namespace {
bool knownFixture(const std::string& fixtureID) {
return fixtureID == "uniform-east" || fixtureID == "uniform-north" || fixtureID == "vortex";
return fixtureID == "uniform-east" || fixtureID == "uniform-north" || fixtureID == "vortex" ||
fixtureID == "nodata";
}
std::string readText(const std::filesystem::path& path) {
@@ -108,25 +109,28 @@ std::string sha256(const std::vector<char>& input) {
bool ParticleFieldSnapshot::isValid() const {
if (fixtureID.empty() || fieldType != "WIND" || displayDomain != "GLOBAL" || width == 0 || height == 0 ||
!(west < east) || !(south < north) || velocity.size() != static_cast<size_t>(width) * height * 2) {
!(west < east) || !(south < north) || velocity.size() != static_cast<size_t>(width) * height * 2 ||
valid.size() != static_cast<size_t>(width) * height) {
return false;
}
for (const float component : velocity) {
if (!std::isfinite(component) || component == noData) {
if (!std::isfinite(component)) {
return false;
}
}
return true;
}
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID, const uint64_t generation) {
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID,
const std::string& metadataText,
const std::vector<char>& bytes,
const uint64_t generation) {
if (!knownFixture(fixtureID)) {
throw std::invalid_argument("unknown offline particle fixture: " + fixtureID);
}
const auto directory = std::filesystem::path{"particle-fixtures"} / fixtureID;
rapidjson::Document metadata;
metadata.Parse(readText(directory / "metadata.json").c_str());
metadata.Parse(metadataText.c_str());
if (metadata.HasParseError() || !metadata.IsObject() || !member(metadata, "schema").IsString() ||
std::string(member(metadata, "schema").GetString()) != "maplibre-particle-fixture/v1" ||
!member(metadata, "fixtureId").IsString() ||
@@ -174,9 +178,11 @@ ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fix
field.east = bounds["east"].GetDouble();
field.north = bounds["north"].GetDouble();
field.noData = static_cast<float>(vector["noData"].GetDouble());
if (!std::isfinite(field.noData)) {
throw std::invalid_argument("non-finite fixture noData: " + fixtureID);
}
field.dataVersion = member(metadata, "dataVersion").GetUint64();
field.generation = generation;
const auto bytes = readBinary(directory / "field.f32");
const size_t expected = static_cast<size_t>(field.width) * field.height * 2 * sizeof(float);
if (bytes.size() != expected || member(integrity, "fieldByteLength").GetUint64() != expected) {
throw std::invalid_argument("fixture field length mismatch: " + fixtureID);
@@ -186,10 +192,27 @@ ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fix
}
field.velocity.resize(expected / sizeof(float));
std::memcpy(field.velocity.data(), bytes.data(), expected);
field.valid.resize(static_cast<size_t>(field.width) * field.height);
for (size_t cell = 0; cell < field.valid.size(); ++cell) {
const float u = field.velocity[cell * 2];
const float v = field.velocity[cell * 2 + 1];
if (!std::isfinite(u) || !std::isfinite(v)) {
throw std::invalid_argument("non-finite fixture velocity: " + fixtureID);
}
field.valid[cell] = (u == field.noData || v == field.noData) ? 0 : 1;
}
if (!field.isValid()) {
throw std::invalid_argument("invalid offline particle fixture: " + fixtureID);
}
return field;
}
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID, const uint64_t generation) {
if (!knownFixture(fixtureID)) {
throw std::invalid_argument("unknown offline particle fixture: " + fixtureID);
}
const auto directory = std::filesystem::path{"particle-fixtures"} / fixtureID;
return load(fixtureID, readText(directory / "metadata.json"), readBinary(directory / "field.f32"), generation);
}
} // namespace mbgl::particle

View File

@@ -24,6 +24,7 @@ struct ParticleFieldSnapshot {
// 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;
std::vector<uint8_t> valid;
bool isValid() const;
};
@@ -33,6 +34,14 @@ struct ParticleFieldSnapshot {
// used by future Android Vulkan and iOS renderers.
class OfflineFixtureParticleAdapter final {
public:
// Generic loader boundary used by FileSource-backed platform inputs. It
// intentionally receives bytes only: no Android asset API crosses Core.
static ParticleFieldSnapshot load(const std::string& fixtureID,
const std::string& metadata,
const std::vector<char>& fieldBytes,
uint64_t generation);
// Linux fixture smoke-test helper. Android must use the byte loader above.
static ParticleFieldSnapshot load(const std::string& fixtureID, uint64_t generation);
};

View File

@@ -1,4 +1,4 @@
#include <mbgl/renderer/layers/render_particle_validation_layer.hpp>
#include <mbgl/renderer/layers/render_raster_particle_layer.hpp>
#include <mbgl/gfx/color_mode.hpp>
#include <mbgl/gfx/context.hpp>
@@ -17,10 +17,14 @@
#include <mbgl/renderer/render_pass.hpp>
#include <mbgl/renderer/render_static_data.hpp>
#include <mbgl/renderer/render_target.hpp>
#include <mbgl/renderer/sources/render_raster_array_source.hpp>
#include <mbgl/shaders/particle_gpu_validation_ubo.hpp>
#include <mbgl/shaders/shader_defines.hpp>
#include <mbgl/shaders/shader_program_base.hpp>
#include <mbgl/style/types.hpp>
#include <mbgl/style/layers/raster_particle_layer_impl.hpp>
#include <mbgl/style/layers/particle_render_layer_properties.hpp>
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
#include <mbgl/tile/tile_id.hpp>
#include <mbgl/util/image.hpp>
#include <mbgl/util/logging.hpp>
@@ -45,9 +49,21 @@ std::atomic_uint64_t nextGpuGeneration{0};
std::atomic_uint64_t nextMaskGeneration{0};
std::atomic_uint64_t nextFieldGeneration{0};
const style::ParticleValidationLayer::Impl& impl(const Immutable<style::Layer::Impl>& value) {
struct ParticleSettings {
bool active;
uint8_t maskMode;
uint8_t fixtureMode;
bool usesFormalSource;
};
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};
}
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
return static_cast<const style::ParticleValidationLayer::Impl&>(*value);
const auto& layer = static_cast<const style::ParticleValidationLayer::Impl&>(*value);
return {layer.active, layer.maskMode, layer.fixtureMode, false};
}
enum class ValidationPass : uint8_t {
@@ -87,7 +103,7 @@ std::shared_ptr<PremultipliedImage> makeVelocityFieldImage(const particle::Parti
image->data[pixel + 1] = static_cast<uint8_t>(
std::clamp(field.velocity[cell + 1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
image->data[pixel + 2] = 0;
image->data[pixel + 3] = 255;
image->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
}
}
return image;
@@ -125,7 +141,7 @@ void removeTargets(const std::vector<RenderTargetPtr>& targets, UniqueChangeRequ
} // namespace
class ParticleGpuValidationResources {
class ParticleGpuResources {
public:
uint64_t generation = 0;
uint64_t committedFrame = 0;
@@ -203,7 +219,7 @@ namespace {
class ParticleGpuValidationTweaker final : public gfx::DrawableTweaker {
public:
ParticleGpuValidationTweaker(std::shared_ptr<ParticleGpuValidationResources> state_,
ParticleGpuValidationTweaker(std::shared_ptr<ParticleGpuResources> state_,
ValidationPass pass_,
uint8_t readSlot_,
uint8_t writeSlot_,
@@ -351,7 +367,7 @@ public:
}
private:
std::weak_ptr<ParticleGpuValidationResources> state;
std::weak_ptr<ParticleGpuResources> state;
ValidationPass pass;
uint8_t readSlot;
uint8_t writeSlot;
@@ -382,7 +398,7 @@ RenderTargetPtr createValidationTarget(gfx::Context& context,
}
bool buildQuad(gfx::Context& context,
const std::shared_ptr<ParticleGpuValidationResources>& resources,
const std::shared_ptr<ParticleGpuResources>& resources,
LayerGroup& group,
const std::string& name,
const gfx::Texture2DPtr& source,
@@ -448,74 +464,87 @@ bool buildQuad(gfx::Context& context,
} // namespace
RenderParticleValidationLayer::RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl> impl_)
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(
impl_, impl_->active, impl_->maskMode, impl_->fixtureMode)) {
Log::Info(
Event::Render,
"ParticleValidation: RenderLayer create id=" + getID() + " source-less=true pass=translucent stage=30-02");
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);
}()) {
formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo();
Log::Info(Event::Render,
"ParticleRender: RenderLayer create id=" + getID() +
" source=" + (formalSource ? baseImpl->source : std::string("validation-only")));
}
RenderParticleValidationLayer::~RenderParticleValidationLayer() {
if (gpuValidation) {
RenderRasterParticleLayer::~RenderRasterParticleLayer() {
if (gpuResources) {
Log::Info(Event::Render,
"ParticleValidation30_02: resources destroy generation=" + std::to_string(gpuValidation->generation) +
" contextDestroyed=" + (gpuValidation->contextDestroyed ? "true" : "false"));
"ParticleValidation30_02: resources destroy generation=" + std::to_string(gpuResources->generation) +
" contextDestroyed=" + (gpuResources->contextDestroyed ? "true" : "false"));
}
Log::Info(Event::Render, "ParticleValidation: RenderLayer destroy id=" + getID());
}
void RenderParticleValidationLayer::evaluate(const PropertyEvaluationParameters&) {
const bool active = impl(baseImpl).active;
const uint8_t maskMode = impl(baseImpl).maskMode;
const uint8_t fixtureMode = impl(baseImpl).fixtureMode;
auto properties = makeMutable<style::ParticleValidationLayerProperties>(
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active, maskMode, fixtureMode);
void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
const auto settings = particleSettings(baseImpl);
auto properties = makeMutable<style::ParticleRenderLayerProperties>(
baseImpl, settings.active, settings.maskMode, settings.fixtureMode);
passes = RenderPass::Translucent;
properties->renderPasses = mbgl::underlying_type(passes);
evaluatedProperties = std::move(properties);
Log::Info(Event::Render,
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") +
" maskMode=" + std::to_string(maskMode) + " id=" + getID() + " fixture=" + fixtureID(fixtureMode) +
"ParticleRender: state=" + std::string(settings.active ? "ACTIVE" : "STATIC") +
" maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + " fixture=" +
(formalSource && sourceField ? sourceField->fixtureID : fixtureID(settings.fixtureMode)) +
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=40-01");
}
bool RenderParticleValidationLayer::needsRepaint() const {
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
bool RenderRasterParticleLayer::needsRepaint() const {
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*evaluatedProperties);
return properties.active && baseImpl->visibility != style::VisibilityType::None;
}
void RenderParticleValidationLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) {
void RenderRasterParticleLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) {
RenderLayer::markLayerRenderable(willRender, changes);
if (!willRender && gpuValidation) {
removeTargets(gpuValidation->registeredTargets, changes);
gpuValidation->registeredTargets.clear();
gpuValidation->resetSequence();
if (!willRender && gpuResources) {
removeTargets(gpuResources->registeredTargets, changes);
gpuResources->registeredTargets.clear();
gpuResources->resetSequence();
Log::Info(Event::Render,
"ParticleValidation30_02: disabled reset generation=" + std::to_string(gpuValidation->generation));
"ParticleValidation30_02: disabled reset generation=" + std::to_string(gpuResources->generation));
}
}
void RenderParticleValidationLayer::layerRemoved(UniqueChangeRequestVec& changes) {
if (gpuValidation) {
removeTargets(gpuValidation->registeredTargets, changes);
gpuValidation->registeredTargets.clear();
void RenderRasterParticleLayer::layerRemoved(UniqueChangeRequestVec& changes) {
if (gpuResources) {
removeTargets(gpuResources->registeredTargets, changes);
gpuResources->registeredTargets.clear();
}
RenderLayer::layerRemoved(changes);
}
void RenderParticleValidationLayer::markContextDestroyed() {
if (gpuValidation) {
gpuValidation->contextDestroyed = true;
Log::Info(Event::Render,
"ParticleValidation30_02: context lost discard generation=" +
std::to_string(gpuValidation->generation) + " stateRead=" + slotName(gpuValidation->stateRead) +
" trailRead=" + slotName(gpuValidation->trailRead));
void RenderRasterParticleLayer::markContextDestroyed() {
if (gpuResources) {
gpuResources->contextDestroyed = true;
Log::Info(
Event::Render,
"ParticleValidation30_02: context lost discard generation=" + std::to_string(gpuResources->generation) +
" stateRead=" + slotName(gpuResources->stateRead) + " trailRead=" + slotName(gpuResources->trailRead));
}
}
void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
void RenderRasterParticleLayer::prepare(const LayerPrepareParameters& parameters) {
if (!formalSource || !parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) {
return;
}
const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source);
if (source.impl().field) {
sourceField = *source.impl().field;
}
}
void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
gfx::Context& context,
const TransformState& transformState,
const std::shared_ptr<UpdateParameters>&,
@@ -530,19 +559,19 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
return;
}
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*evaluatedProperties);
const bool active = properties.active;
const uint8_t maskMode = properties.maskMode;
const uint8_t fixtureMode = properties.fixtureMode;
auto resources = gpuValidation;
auto resources = gpuResources;
if (resources && resources->contextDestroyed) {
// VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale
// State, Trail, or Mask texture survives a notified context loss.
gpuValidation.reset();
gpuResources.reset();
resources.reset();
}
if (!resources) {
auto owned = std::make_shared<ParticleGpuValidationResources>();
auto owned = std::make_shared<ParticleGpuResources>();
owned->generation = ++nextGpuGeneration;
owned->shader = context.getGenericShader(shaders, ShaderName);
owned->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
@@ -568,9 +597,18 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
.wrapV = gfx::TextureWrapType::Clamp});
owned->maskGeneration = ++nextMaskGeneration;
try {
if (formalSource) {
if (!sourceField) {
Log::Error(Event::Render, "ParticleField: raster-array source has no loaded field");
return;
}
owned->field = *sourceField;
owned->fieldGeneration = owned->field.generation;
} else {
owned->fieldGeneration = ++nextFieldGeneration;
owned->field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode),
owned->fieldGeneration);
}
owned->fieldImage = makeVelocityFieldImage(owned->field);
owned->fieldTexture = context.createTexture2D();
if (!owned->fieldImage || !owned->fieldTexture) {
@@ -587,8 +625,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
Log::Error(Event::Render, "ParticleField: fixture load failed: " + std::string(error.what()));
return;
}
gpuValidation = std::move(owned);
resources = gpuValidation;
gpuResources = std::move(owned);
resources = gpuResources;
Log::Info(Event::Render,
"ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) +
" size=16x16 independent=true");
@@ -621,10 +659,13 @@ void RenderParticleValidationLayer::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->fixtureMode != fixtureMode) {
const bool sourceChanged = formalSource && sourceField && resources->fieldGeneration != sourceField->generation;
if ((!formalSource && resources->fixtureMode != fixtureMode) || sourceChanged) {
try {
const auto fieldGeneration = ++nextFieldGeneration;
auto field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), fieldGeneration);
const auto fieldGeneration = formalSource ? sourceField->generation : ++nextFieldGeneration;
auto field = formalSource
? *sourceField
: particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), fieldGeneration);
auto image = makeVelocityFieldImage(field);
auto texture = context.createTexture2D();
if (!image || !texture) {

View File

@@ -1,38 +1,11 @@
#pragma once
#include <mbgl/renderer/render_layer.hpp>
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
#include <mbgl/renderer/layers/render_raster_particle_layer.hpp>
namespace mbgl {
class ParticleGpuValidationResources;
// VALIDATION ONLY: proves Core RenderLayer/GPU ping-pong integration.
// STATIC redraws only the frozen Trail composite; it never advances State/Trail slots.
class RenderParticleValidationLayer final : public RenderLayer {
public:
explicit RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl>);
~RenderParticleValidationLayer() override;
void update(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&,
UniqueChangeRequestVec&) override;
private:
void transition(const TransitionParameters&) override {}
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override { return false; }
bool hasCrossfade() const override { return false; }
bool needsRepaint() const override;
void prepare(const LayerPrepareParameters&) override {}
void markLayerRenderable(bool, UniqueChangeRequestVec&) override;
void layerRemoved(UniqueChangeRequestVec&) override;
void markContextDestroyed() override;
std::shared_ptr<ParticleGpuValidationResources> gpuValidation;
};
// Validation styles reuse the formal GPU renderer and do not own another
// particle simulation.
using RenderParticleValidationLayer = RenderRasterParticleLayer;
} // namespace mbgl

View File

@@ -0,0 +1,38 @@
#pragma once
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/renderer/render_layer.hpp>
namespace mbgl {
class ParticleGpuResources;
class RenderRasterParticleLayer final : public RenderLayer {
public:
explicit RenderRasterParticleLayer(Immutable<style::Layer::Impl>);
~RenderRasterParticleLayer() override;
void update(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&,
UniqueChangeRequestVec&) override;
private:
void transition(const TransitionParameters&) override {}
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override { return false; }
bool hasCrossfade() const override { return false; }
bool needsRepaint() const override;
void prepare(const LayerPrepareParameters&) override;
void markLayerRenderable(bool, UniqueChangeRequestVec&) override;
void layerRemoved(UniqueChangeRequestVec&) override;
void markContextDestroyed() override;
std::shared_ptr<ParticleGpuResources> gpuResources;
std::optional<particle::ParticleFieldSnapshot> sourceField;
bool formalSource = false;
};
} // namespace mbgl

View File

@@ -8,7 +8,9 @@
#include <mbgl/renderer/sources/render_image_source.hpp>
#include <mbgl/renderer/sources/render_raster_dem_source.hpp>
#include <mbgl/renderer/sources/render_raster_source.hpp>
#include <mbgl/renderer/sources/render_raster_array_source.hpp>
#include <mbgl/renderer/sources/render_vector_source.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/renderer/tile_parameters.hpp>
#include <mbgl/renderer/update_parameters.hpp>
#include <mbgl/tile/tile.hpp>
@@ -49,6 +51,8 @@ std::unique_ptr<RenderSource> RenderSource::create(const Immutable<Source::Impl>
case SourceType::CustomVector:
return std::make_unique<RenderCustomGeometrySource>(staticImmutableCast<CustomGeometrySource::Impl>(impl),
threadPool);
case SourceType::RasterArray:
return std::make_unique<RenderRasterArraySource>(staticImmutableCast<RasterArraySource::Impl>(impl));
}
// Not reachable, but placate GCC.

View File

@@ -0,0 +1,40 @@
#pragma once
#include <mbgl/renderer/render_source.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/style/sources/raster_array_source_impl.hpp>
namespace mbgl {
class RenderRasterArraySource final : public RenderSource {
public:
explicit RenderRasterArraySource(Immutable<style::RasterArraySource::Impl> impl)
: RenderSource(std::move(impl)) {}
bool isLoaded() const override { return impl().field.has_value(); }
void update(Immutable<style::Source::Impl> value,
const std::vector<Immutable<style::LayerProperties>>&,
bool needsRendering,
bool,
const TileParameters&) override {
baseImpl = std::move(value);
enabled = needsRendering && isLoaded();
}
std::unique_ptr<RenderItem> createRenderItem() override { return {}; }
void prepare(const SourcePrepareParameters&) override {}
void updateFadingTiles() override {}
bool hasFadingTiles() const override { return false; }
std::unordered_map<std::string, std::vector<Feature>> queryRenderedFeatures(
const ScreenLineString&,
const TransformState&,
const std::unordered_map<std::string, const RenderLayer*>&,
const RenderedQueryOptions&,
const mat4&) const override {
return {};
}
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
void reduceMemoryUse() override {}
void dumpDebugLogs() const override {}
const style::RasterArraySource::Impl& impl() const {
return static_cast<const style::RasterArraySource::Impl&>(*baseImpl);
}
};
} // namespace mbgl

View File

@@ -10,6 +10,7 @@
#include <mbgl/style/sources/raster_dem_source.hpp>
#include <mbgl/style/sources/vector_source.hpp>
#include <mbgl/style/sources/image_source.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/util/geo.hpp>
namespace mbgl {
@@ -202,6 +203,26 @@ std::optional<std::unique_ptr<Source>> convertImageSource(const std::string& id,
return {std::move(result)};
}
std::optional<std::unique_ptr<Source>> convertRasterArraySource(const std::string& id,
const Convertible& value,
Error& error) {
const auto fixture = objectMember(value, "fixture");
const auto metadata = objectMember(value, "metadata");
const auto field = objectMember(value, "field");
if (!fixture || !metadata || !field) {
error.message = "raster-array source requires fixture, metadata, and field";
return std::nullopt;
}
const auto fixtureID = toString(*fixture);
const auto metadataURL = toString(*metadata);
const auto fieldURL = toString(*field);
if (!fixtureID || !metadataURL || !fieldURL) {
error.message = "raster-array fixture inputs must be strings";
return std::nullopt;
}
return {std::make_unique<RasterArraySource>(id, *fixtureID, *metadataURL, *fieldURL)};
}
} // namespace
std::optional<std::unique_ptr<Source>> Converter<std::unique_ptr<Source>>::operator()(const Convertible& value,
@@ -234,6 +255,8 @@ std::optional<std::unique_ptr<Source>> Converter<std::unique_ptr<Source>>::opera
return convertGeoJSONSource(id, value, error);
} else if (tname == "image") {
return convertImageSource(id, value, error);
} else if (tname == "raster-array") {
return convertRasterArraySource(id, value, error);
} else {
error.message = "invalid source type";
return std::nullopt;

View File

@@ -0,0 +1,20 @@
#pragma once
#include <mbgl/style/layer_properties.hpp>
namespace mbgl::style {
class ParticleRenderLayerProperties final : public LayerProperties {
public:
ParticleRenderLayerProperties(Immutable<Layer::Impl> impl, bool active_, uint8_t maskMode_, uint8_t fixtureMode_)
: LayerProperties(std::move(impl)),
active(active_),
maskMode(maskMode_),
fixtureMode(fixtureMode_) {}
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
bool active;
uint8_t maskMode;
uint8_t fixtureMode;
};
} // namespace mbgl::style

View File

@@ -0,0 +1,99 @@
#include <mbgl/style/layers/raster_particle_layer.hpp>
#include <mbgl/style/conversion/constant.hpp>
#include <mbgl/style/conversion_impl.hpp>
#include <mbgl/style/layer_observer.hpp>
#include <mbgl/style/layers/raster_particle_layer_impl.hpp>
#include <cmath>
namespace mbgl::style {
namespace {
constexpr const char* ActiveProperty = "raster-particle-active";
constexpr const char* MaskModeProperty = "raster-particle-mask-mode";
const LayerTypeInfo typeInfo{.type = "raster-particle",
.source = LayerTypeInfo::Source::Required,
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
.layout = LayerTypeInfo::Layout::NotRequired,
.fadingTiles = LayerTypeInfo::FadingTiles::NotRequired,
.crossTileIndex = LayerTypeInfo::CrossTileIndex::NotRequired,
.tileKind = LayerTypeInfo::TileKind::NotRequired};
} // namespace
const LayerTypeInfo* RasterParticleLayer::Impl::staticTypeInfo() noexcept {
return &typeInfo;
}
RasterParticleLayer::RasterParticleLayer(const std::string& id, const std::string& source)
: Layer(makeMutable<Impl>(id, source)) {}
RasterParticleLayer::RasterParticleLayer(Immutable<Impl> impl_)
: Layer(std::move(impl_)) {}
RasterParticleLayer::~RasterParticleLayer() {
weakFactory.invalidateWeakPtrs();
}
const RasterParticleLayer::Impl& RasterParticleLayer::impl() const {
return static_cast<const Impl&>(*baseImpl);
}
Mutable<RasterParticleLayer::Impl> RasterParticleLayer::mutableImpl() const {
return makeMutable<Impl>(impl());
}
bool RasterParticleLayer::isActive() const {
return impl().active;
}
uint8_t RasterParticleLayer::getMaskMode() const {
return impl().maskMode;
}
void RasterParticleLayer::setActive(bool active) {
if (active != isActive()) {
auto next = mutableImpl();
next->active = active;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
void RasterParticleLayer::setMaskMode(uint8_t mode) {
if (mode <= 2 && mode != getMaskMode()) {
auto next = mutableImpl();
next->maskMode = mode;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) {
conversion::Error error;
if (name == ActiveProperty) {
auto converted = conversion::convert<bool>(value, error);
if (!converted) return error;
setActive(*converted);
return std::nullopt;
}
if (name == MaskModeProperty) {
auto converted = conversion::convert<float>(value, error);
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted)
return conversion::Error{"raster-particle-mask-mode must be 0, 1, or 2"};
setMaskMode(static_cast<uint8_t>(*converted));
return std::nullopt;
}
return conversion::Error{"layer doesn't support this property"};
}
StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
if (name == ActiveProperty) return StyleProperty(Value{isActive()}, StyleProperty::Kind::Constant);
if (name == MaskModeProperty)
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
return {};
}
Value RasterParticleLayer::serialize() const {
auto result = Layer::serialize();
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
return result;
}
std::unique_ptr<Layer> RasterParticleLayer::cloneRef(const std::string& id) const {
auto next = mutableImpl();
next->id = id;
return std::unique_ptr<Layer>(new RasterParticleLayer(std::move(next)));
}
Mutable<Layer::Impl> RasterParticleLayer::mutableBaseImpl() const {
return staticMutableCast<Layer::Impl>(mutableImpl());
}
} // namespace mbgl::style

View File

@@ -0,0 +1,20 @@
#pragma once
#include <mbgl/style/layer_impl.hpp>
#include <mbgl/style/layers/raster_particle_layer.hpp>
namespace mbgl::style {
class RasterParticleLayer::Impl final : public Layer::Impl {
public:
Impl(const std::string& id, const std::string& source)
: Layer::Impl(id, source) {}
Impl(const Impl&) = default;
bool hasLayoutDifference(const Layer::Impl&) const override { return false; }
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override {}
bool active = true;
uint8_t maskMode = 0;
DECLARE_LAYER_TYPE_INFO;
};
} // namespace mbgl::style

View File

@@ -0,0 +1,80 @@
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/storage/file_source.hpp>
#include <mbgl/storage/resource.hpp>
#include <mbgl/style/source_observer.hpp>
#include <mbgl/style/sources/raster_array_source_impl.hpp>
#include <mbgl/util/async_request.hpp>
#include <mbgl/util/logging.hpp>
namespace mbgl::style {
RasterArraySource::RasterArraySource(std::string id,
std::string fixtureID,
std::string metadataURL,
std::string fieldURL)
: Source(makeMutable<Impl>(std::move(id), std::move(fixtureID), std::move(metadataURL), std::move(fieldURL))) {}
RasterArraySource::~RasterArraySource() {
weakFactory.invalidateWeakPtrs();
}
const RasterArraySource::Impl& RasterArraySource::impl() const {
return static_cast<const Impl&>(*baseImpl);
}
void RasterArraySource::loadDescription(FileSource& fileSource) {
if (metadataRequest || fieldRequest) return;
metadataRequest = fileSource.request(
Resource::source(impl().metadataURL), [this, self = makeWeakPtr()](const Response& response) {
if (auto guard = self.lock(); self) {
if (response.error || response.noContent || !response.data) {
observer->onSourceError(
*this, std::make_exception_ptr(std::runtime_error("particle metadata request failed")));
return;
}
metadata = *response.data;
tryLoad();
}
});
fieldRequest = fileSource.request(
Resource::source(impl().fieldURL), [this, self = makeWeakPtr()](const Response& response) {
if (auto guard = self.lock(); self) {
if (response.error || response.noContent || !response.data) {
observer->onSourceError(
*this, std::make_exception_ptr(std::runtime_error("particle field request failed")));
return;
}
fieldBytes = std::vector<char>(response.data->begin(), response.data->end());
tryLoad();
}
});
}
void RasterArraySource::tryLoad() {
if (!metadata || !fieldBytes) return;
try {
auto field = particle::OfflineFixtureParticleAdapter::load(
impl().fixtureID, *metadata, *fieldBytes, ++generation);
baseImpl = makeMutable<Impl>(impl(), std::move(field));
loaded = true;
Log::Info(Event::Render,
"ParticleField: source loaded fixtureId=" + impl().fixtureID + " metadata=" + impl().metadataURL +
" field=" + impl().fieldURL);
observer->onSourceLoaded(*this);
observer->onSourceChanged(*this);
} catch (const std::exception& error) {
observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what())));
}
}
bool RasterArraySource::supportsLayerType(const LayerTypeInfo* info) const {
return info && std::string(info->type) == "raster-particle";
}
Mutable<Source::Impl> RasterArraySource::createMutable() const noexcept {
return staticMutableCast<Source::Impl>(makeMutable<Impl>(impl()));
}
} // namespace mbgl::style

View File

@@ -0,0 +1,31 @@
#pragma once
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/style/source_impl.hpp>
namespace mbgl::style {
class RasterArraySource::Impl final : public Source::Impl {
public:
Impl(std::string id, std::string fixtureID_, std::string metadataURL_, std::string fieldURL_)
: Source::Impl(SourceType::RasterArray, std::move(id)),
fixtureID(std::move(fixtureID_)),
metadataURL(std::move(metadataURL_)),
fieldURL(std::move(fieldURL_)) {}
Impl(const Impl&) = default;
Impl(const Impl& other, particle::ParticleFieldSnapshot field_)
: Source::Impl(other),
fixtureID(other.fixtureID),
metadataURL(other.metadataURL),
fieldURL(other.fieldURL),
field(std::move(field_)) {}
std::optional<std::string> getAttribution() const override { return {}; }
std::string fixtureID;
std::string metadataURL;
std::string fieldURL;
std::optional<particle::ParticleFieldSnapshot> field;
};
} // namespace mbgl::style

View File

@@ -12,7 +12,8 @@ MBGL_DEFINE_ENUM(SourceType,
{SourceType::Video, "video"},
{SourceType::Annotations, "annotations"},
{SourceType::Image, "image"},
{SourceType::CustomVector, "customvector"}});
{SourceType::CustomVector, "customvector"},
{SourceType::RasterArray, "raster-array"}});
MBGL_DEFINE_ENUM(VisibilityType,
{

View File

@@ -7,6 +7,10 @@ import struct
GRID = 16
ROOT = Path(__file__).resolve().parents[1] / "particle-fixtures"
ANDROID_ASSETS = (
Path(__file__).resolve().parents[1]
/ "platform/android/MapLibreAndroidTestApp/src/main/assets/integration/particle-fixtures"
)
def velocity(fixture: str, x: int, y: int) -> tuple[float, float]:
@@ -14,14 +18,20 @@ def velocity(fixture: str, x: int, y: int) -> tuple[float, float]:
return 1.0, 0.0
if fixture == "uniform-north":
return 0.0, 1.0
if fixture == "nodata":
return (-9999.0, -9999.0) if (x, y) == (3, 4) else (1.0, 0.0)
local_x = (x + 0.5) / GRID - 0.5
local_y = (y + 0.5) / GRID - 0.5
return -2.0 * local_y, 2.0 * local_x
for fixture in ("uniform-east", "uniform-north", "vortex"):
for fixture in ("uniform-east", "uniform-north", "vortex", "nodata"):
payload = bytearray()
for y in range(GRID):
for x in range(GRID):
payload.extend(struct.pack("<ff", *velocity(fixture, x, y)))
(ROOT / fixture / "field.f32").write_bytes(payload)
asset_path = ANDROID_ASSETS / fixture / "field.f32"
asset_path.parent.mkdir(parents=True, exist_ok=True)
asset_path.write_bytes(payload)
(asset_path.parent / "metadata.json").write_bytes((ROOT / fixture / "metadata.json").read_bytes())

View File

@@ -158,3 +158,11 @@
- Linux OpenGL 受影响 Core 对象、C++ format、diff、fixture checksum 和 adapter smoke test 均通过;未运行 Gradle/ADB/实机验证。
- 当前结论:`WAITING_MANUAL_VALIDATION`。用户仍需完成 Android OpenGL T1-T12,尤其 T1 rotation 无按钮恢复;iOS、Vulkan、真实数据、生产 mask 和性能均未覆盖,尚不建议进入下一 Android 子任务。
- Stage A 实现提交 `0b076c6d52fffb2ee4411cac64f06d43cea260c8` 已成功 push 到 `origin/particle/main`。
## 2026-08-31 — 40-01 Stage A 正式链、Android fixture 边界与 NoData 修正
- 纠正此前实现仍借用 source-less `ParticleValidationLayer` 的偏差:新增正式 `RasterArraySource -> RenderRasterArraySource -> RasterParticleLayer (Source::Required) -> RenderRasterParticleLayer`,并让正式 RenderLayer 复用既有 GPU field、State A/B、Trail A/B、Mask、Camera/World Wrap/Antimeridian 与生命周期。
- Android Test App 的 Style 现在创建 `raster-array` / `raster-particle`,fixture 由 APK `assets/integration/particle-fixtures/` 的 `asset://` URI 提供;Core adapter 仅接收 FileSource bytes。Linux smoke helper 继续读取仓库 fixture,生成工具验证并同步两端完全相同的 metadata/field 内容。
- `ParticleFieldSnapshot` 将 cell validity 独立于 fixture validity:合法 NoData cell 产生 invalid mask,GPU texture alpha 表示该 mask;State sample 命中 NoData 后确定性 respawn,不将 `-9999` 当普通速度;NaN/Inf 仍拒绝。新增 `nodata` deterministic fixture。
- STATIC/context recreation 的 zero-step bootstrap 未推翻:新资源自动建立并首帧 composite,bootstrap commit 不增加 simulation frame。
- Linux Core 精确编译、format/diff、fixture generator、adapter/asset parity 检查在最终收尾复跑;未运行 Gradle、APK、ADB 或 T1-T12。最终状态仍只能是 `WAITING_MANUAL_VALIDATION`,等待 GPT 再次复审远端代码。