feat: establish android particle field rendering loop
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:
@@ -210,6 +210,8 @@ list(APPEND SRC_FILES
|
|||||||
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/symbol_layer_tweaker.hpp
|
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/symbol_layer_tweaker.hpp
|
||||||
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/collision_layer_tweaker.cpp
|
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/collision_layer_tweaker.cpp
|
||||||
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/collision_layer_tweaker.hpp
|
${PROJECT_SOURCE_DIR}/src/mbgl/renderer/layers/collision_layer_tweaker.hpp
|
||||||
|
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/offline_fixture_particle_adapter.cpp
|
||||||
|
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/offline_fixture_particle_adapter.hpp
|
||||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/shader_program_base.cpp
|
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/shader_program_base.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
64
docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md
Normal file
64
docs/60_测试/40_01_Android离线Fixture到正式粒子渲染最小闭环验证报告_v1.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 40-01 Android 离线 Fixture 到正式粒子渲染最小闭环验证报告 v1
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
`WAITING_MANUAL_VALIDATION`。30-01、30-02、30-03 的正式记录均为 `PASS`,30-03 允许进入本任务。本报告只记录 Codex 的 Stage A 事实,不把未执行的 Android 编译或设备视觉验收写为 PASS。
|
||||||
|
|
||||||
|
## 基线与范围
|
||||||
|
|
||||||
|
- 分支:`particle/main`
|
||||||
|
- 开始 HEAD:`1ec44701146925dec9a87618e3f90bee9a12073f`
|
||||||
|
- 平台范围:Android OpenGL;未修改 iOS、Vulkan、NavSea 或在线数据链。
|
||||||
|
- Test App 继续使用既有 `Particle Core Layer Validation` 入口;Core adapter 不依赖 Kotlin/Android UI。
|
||||||
|
|
||||||
|
## 已实现的 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
|
||||||
|
```
|
||||||
|
|
||||||
|
- fixture contract 使用 `metadata.json + field.f32`,校验 schema、id、WIND/GLOBAL、EPSG:4326、bounds、grid registration/order、interleaved UV、little-endian float32、NoData 声明、长度和 SHA-256。
|
||||||
|
- `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 每帧粒子循环。
|
||||||
|
- 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。
|
||||||
|
|
||||||
|
## Codex 可执行检查
|
||||||
|
|
||||||
|
| 检查 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| `ninja -C build-linux-opengl` 受影响 Core 对象 | PASS:`offline_fixture_particle_adapter.cpp`、`render_particle_validation_layer.cpp`、`shader_info.cpp`、`particle_validation_layer.cpp` |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
首次整库构建尝试被 sandbox 默认 ccache 目录只读中断;以 `CCACHE_DIR=/tmp/maplibre-particle-ccache` 重新执行上述受影响对象检查后通过。这不是代码编译失败。
|
||||||
|
|
||||||
|
## 用户手工 Android OpenGL 验收
|
||||||
|
|
||||||
|
未执行。已核对当前模块提供 `opengl` flavor,用户应在提交后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd platform/android
|
||||||
|
./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
随后按任务书执行 T1-T12,尤其验证 STATIC + OCEAN A/B rotation 无按钮恢复、east/north 方向差异、field/mask generation 在 Camera 操作中稳定,以及 disable/enable、world wrap 和双向 antimeridian。日志筛选关键字:`ParticleField|ParticleGPU|ParticleMask|ParticleRender|ParticleValidation`。
|
||||||
|
|
||||||
|
## 未覆盖与风险
|
||||||
|
|
||||||
|
- 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 OpenGL build 与 T1-T12 实际结果;仅在收到证据后进入任务书定义的 Stage B。
|
||||||
40
docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md
Normal file
40
docs/报告/40_01_Android粒子最小正式闭环_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 40-01 Android 粒子最小正式闭环 Codex 完成报告 v1
|
||||||
|
|
||||||
|
## 1. 执行结论
|
||||||
|
|
||||||
|
Stage A 代码与静态检查完成,任务状态为 `WAITING_MANUAL_VALIDATION`。本报告不声明 40-01 PASS。
|
||||||
|
|
||||||
|
## 2. Git 基线
|
||||||
|
|
||||||
|
- branch:`particle/main`
|
||||||
|
- start commit:`1ec44701146925dec9a87618e3f90bee9a12073f`(`docs: record 30-03 final push metadata`)
|
||||||
|
- 工作树:保留三份既有未跟踪任务书,不纳入本任务提交。
|
||||||
|
- submodule:已记录,未修改。
|
||||||
|
- `git rerere.enabled`:`true`。
|
||||||
|
|
||||||
|
最终 commit、push 结果和远端 HEAD 由本报告随后的 Git 收尾提交记录;目标为 `origin/particle/main`。
|
||||||
|
|
||||||
|
## 3. 实际修改
|
||||||
|
|
||||||
|
- 新增隔离的 `mbgl::particle::OfflineFixtureParticleAdapter` 与 backend-neutral `ParticleFieldSnapshot`。
|
||||||
|
- 新增确定性 `uniform-east`、`uniform-north`、`vortex` fixture 及生成脚本;metadata 遵循架构文档 v1 contract。
|
||||||
|
- 在既有 Core RenderLayer 的 State A/B、Trail A/B、shared mask 和 canonical world composite 路线中接入 GPU field texture;没有新建 Android 侧粒子引擎。
|
||||||
|
- 修复 STATIC/context recreation 的资源 bootstrap:field、mask、trail、state 和首帧 composite 均可在 STATIC 中建立,bootstrap 为零 simulation step。
|
||||||
|
- Test App 保存 fixture selection,并增加 east/north 最小切换控制。
|
||||||
|
|
||||||
|
## 4. 构建与测试记录
|
||||||
|
|
||||||
|
- Linux OpenGL 受影响对象编译:PASS。
|
||||||
|
- C++ 格式、`git diff --check`、fixture JSON/生成/sha256:PASS。
|
||||||
|
- adapter smoke test:PASS,三份 fixture 均成功 decode/validate。
|
||||||
|
- 未运行 Gradle、APK 安装、ADB 或 Android 实机验证,遵守任务书限制。
|
||||||
|
|
||||||
|
## 5. 已知问题与未完成事项
|
||||||
|
|
||||||
|
- Android OpenGL 的 T1-T12 仍待用户执行,T1 rotation 无按钮恢复是硬门槛。
|
||||||
|
- 未实现 iOS/Vulkan/真实在线数据/生产 coastline/性能结论。
|
||||||
|
- NoData cell 的实际 fixture 覆盖留待后续数据 fixture Step;本任务的 metadata/data validator 已拒绝无效结构、长度和 checksum。
|
||||||
|
|
||||||
|
## 6. 与指令书差异
|
||||||
|
|
||||||
|
无。Stage A 结束时保持 `WAITING_MANUAL_VALIDATION`,未新建或修改 `docs/50_Step记录/`。
|
||||||
@@ -41,6 +41,8 @@ void main() {
|
|||||||
|
|
||||||
uniform sampler2D u_previous;
|
uniform sampler2D u_previous;
|
||||||
uniform sampler2D u_mask_texture;
|
uniform sampler2D u_mask_texture;
|
||||||
|
uniform sampler2D u_field_texture;
|
||||||
|
uniform sampler2D u_state_texture;
|
||||||
in vec2 v_uv;
|
in vec2 v_uv;
|
||||||
in vec2 v_world;
|
in vec2 v_world;
|
||||||
|
|
||||||
@@ -49,15 +51,27 @@ void main() {
|
|||||||
float mode = u_pass.x;
|
float mode = u_pass.x;
|
||||||
|
|
||||||
if (mode < 0.5) {
|
if (mode < 0.5) {
|
||||||
// State pass: deterministic GPU-only state evolution.
|
// 40-01: State A/B samples the uploaded OfflineFixture Particle Field.
|
||||||
fragColor = vec4(fract(previous.r + 0.03125), v_uv, 1.0);
|
// RG stores field-normalized position, B stores age, and the Field
|
||||||
|
// 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;
|
||||||
|
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);
|
||||||
} else if (mode < 1.5) {
|
} else if (mode < 1.5) {
|
||||||
// Trail pass: fade the previous trail and add a moving validation marker.
|
// The trail consumes State B/A written by the real field-sampling pass.
|
||||||
float marker = 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, u_marker.xy));
|
// Four deterministic texels are enough for the Stage A visual proof.
|
||||||
|
float marker = 0.0;
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.125, 0.125)).rg)));
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.375, 0.375)).rg)));
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.625, 0.625)).rg)));
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.875, 0.875)).rg)));
|
||||||
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
||||||
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
||||||
} else {
|
} else {
|
||||||
// VALIDATION ONLY: canonical world-space pattern and shared mask.
|
// Current-frame canonical world composite and shared Mask.
|
||||||
float intensity = max(previous.r, max(previous.g, previous.b));
|
float intensity = max(previous.r, max(previous.g, previous.b));
|
||||||
float ocean = texture(u_mask_texture, v_world).a;
|
float ocean = texture(u_mask_texture, v_world).a;
|
||||||
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
||||||
|
|||||||
@@ -341,6 +341,8 @@ enum {
|
|||||||
enum {
|
enum {
|
||||||
idParticleGpuValidationPreviousTexture,
|
idParticleGpuValidationPreviousTexture,
|
||||||
idParticleGpuValidationMaskTexture,
|
idParticleGpuValidationMaskTexture,
|
||||||
|
idParticleGpuValidationFieldTexture,
|
||||||
|
idParticleGpuValidationStateTexture,
|
||||||
particleGpuValidationTextureCount
|
particleGpuValidationTextureCount
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public:
|
|||||||
void setActive(bool);
|
void setActive(bool);
|
||||||
uint8_t getMaskMode() const;
|
uint8_t getMaskMode() const;
|
||||||
void setMaskMode(uint8_t);
|
void setMaskMode(uint8_t);
|
||||||
|
uint8_t getFixtureMode() const;
|
||||||
|
void setFixtureMode(uint8_t);
|
||||||
|
|
||||||
class Impl;
|
class Impl;
|
||||||
const Impl& impl() const;
|
const Impl& impl() const;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# Particle 固定离线 Fixtures
|
# Particle 固定离线 Fixtures
|
||||||
|
|
||||||
第一阶段禁止依赖真实天气服务器。具体数据格式在设计冻结后确定。
|
第一阶段禁止依赖真实天气服务器。每个 velocity fixture 遵循冻结的
|
||||||
|
`maplibre-particle-fixture/v1` contract:`metadata.json` 和 little-endian
|
||||||
|
interleaved U/V `field.f32`,其长度与 SHA-256 必须通过 adapter 校验。
|
||||||
|
|||||||
BIN
particle-fixtures/uniform-east/field.f32
Normal file
BIN
particle-fixtures/uniform-east/field.f32
Normal file
Binary file not shown.
15
particle-fixtures/uniform-east/metadata.json
Normal file
15
particle-fixtures/uniform-east/metadata.json
Normal 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"}
|
||||||
|
}
|
||||||
BIN
particle-fixtures/uniform-north/field.f32
Normal file
BIN
particle-fixtures/uniform-north/field.f32
Normal file
Binary file not shown.
15
particle-fixtures/uniform-north/metadata.json
Normal file
15
particle-fixtures/uniform-north/metadata.json
Normal 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"}
|
||||||
|
}
|
||||||
BIN
particle-fixtures/vortex/field.f32
Normal file
BIN
particle-fixtures/vortex/field.f32
Normal file
Binary file not shown.
15
particle-fixtures/vortex/metadata.json
Normal file
15
particle-fixtures/vortex/metadata.json
Normal 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"}
|
||||||
|
}
|
||||||
@@ -21,15 +21,17 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
private var active = true
|
private var active = true
|
||||||
private var enabled = true
|
private var enabled = true
|
||||||
private var maskMode = MASK_GLOBAL
|
private var maskMode = MASK_GLOBAL
|
||||||
|
private var fixtureMode = FIXTURE_UNIFORM_EAST
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true
|
active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true
|
||||||
enabled = savedInstanceState?.getBoolean(STATE_ENABLED, true) ?: true
|
enabled = savedInstanceState?.getBoolean(STATE_ENABLED, true) ?: true
|
||||||
maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL
|
maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL
|
||||||
|
fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
"ParticleValidation30_03: Activity state restored active=$active enabled=$enabled maskMode=$maskMode " +
|
"ParticleRender: Activity state restored active=$active enabled=$enabled maskMode=$maskMode fixtureMode=$fixtureMode " +
|
||||||
"recreated=${savedInstanceState != null}"
|
"recreated=${savedInstanceState != null}"
|
||||||
)
|
)
|
||||||
setContentView(R.layout.activity_particle_core_layer_validation)
|
setContentView(R.layout.activity_particle_core_layer_validation)
|
||||||
@@ -69,6 +71,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
findViewById<Button>(R.id.validationOceanB).setOnClickListener {
|
findViewById<Button>(R.id.validationOceanB).setOnClickListener {
|
||||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_OCEAN_B)
|
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)
|
||||||
|
}
|
||||||
|
findViewById<Button>(R.id.validationUniformNorth).setOnClickListener {
|
||||||
|
if (this::maplibreMap.isInitialized) setValidationProperties(active, maskMode, FIXTURE_UNIFORM_NORTH)
|
||||||
|
}
|
||||||
findViewById<Button>(R.id.validationDateline).setOnClickListener {
|
findViewById<Button>(R.id.validationDateline).setOnClickListener {
|
||||||
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
|
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
|
||||||
Log.i(TAG, "ParticleValidation30_03: DATELINE preset center=0,179 zoom=2")
|
Log.i(TAG, "ParticleValidation30_03: DATELINE preset center=0,179 zoom=2")
|
||||||
@@ -89,9 +97,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setValidationProperties(active: Boolean, maskMode: Int) {
|
private fun setValidationProperties(active: Boolean, maskMode: Int, fixtureMode: Int = this.fixtureMode) {
|
||||||
this.active = active
|
this.active = active
|
||||||
this.maskMode = maskMode
|
this.maskMode = maskMode
|
||||||
|
this.fixtureMode = fixtureMode
|
||||||
val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID)
|
val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID)
|
||||||
if (layer == null || !enabled) {
|
if (layer == null || !enabled) {
|
||||||
loadValidationStyle(active, enabled)
|
loadValidationStyle(active, enabled)
|
||||||
@@ -99,10 +108,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
layer.setProperties(
|
layer.setProperties(
|
||||||
PaintPropertyValue("particle-validation-active", active),
|
PaintPropertyValue("particle-validation-active", active),
|
||||||
PaintPropertyValue("particle-validation-mask-mode", maskMode)
|
PaintPropertyValue("particle-validation-mask-mode", maskMode),
|
||||||
|
PaintPropertyValue("particle-fixture-mode", fixtureMode)
|
||||||
)
|
)
|
||||||
status.text = statusText()
|
status.text = statusText()
|
||||||
Log.i(TAG, "ParticleValidation30_03: property update active=$active maskMode=$maskMode resourceReuseExpected=true")
|
Log.i(TAG, "ParticleRender: property update active=$active maskMode=$maskMode fixtureMode=$fixtureMode")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun statusText(): String = when {
|
private fun statusText(): String = when {
|
||||||
@@ -116,7 +126,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
private fun validationStyle(active: Boolean, enabled: Boolean): String {
|
private fun validationStyle(active: Boolean, enabled: Boolean): String {
|
||||||
val particleLayer = if (enabled) {
|
val particleLayer = if (enabled) {
|
||||||
"""{"id":"particle-validation-core","type":"particle-validation",
|
"""{"id":"particle-validation-core","type":"particle-validation",
|
||||||
"paint":{"particle-validation-active":$active,"particle-validation-mask-mode":$maskMode}},"""
|
"paint":{"particle-validation-active":$active,"particle-validation-mask-mode":$maskMode,"particle-fixture-mode":$fixtureMode}},"""
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -153,6 +163,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
outState.putBoolean(STATE_ACTIVE, active)
|
outState.putBoolean(STATE_ACTIVE, active)
|
||||||
outState.putBoolean(STATE_ENABLED, enabled)
|
outState.putBoolean(STATE_ENABLED, enabled)
|
||||||
outState.putInt(STATE_MASK_MODE, maskMode)
|
outState.putInt(STATE_MASK_MODE, maskMode)
|
||||||
|
outState.putInt(STATE_FIXTURE_MODE, fixtureMode)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
mapView.onSaveInstanceState(outState)
|
mapView.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
@@ -162,9 +173,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
private const val STATE_ACTIVE = "particle_validation.active"
|
private const val STATE_ACTIVE = "particle_validation.active"
|
||||||
private const val STATE_ENABLED = "particle_validation.enabled"
|
private const val STATE_ENABLED = "particle_validation.enabled"
|
||||||
private const val STATE_MASK_MODE = "particle_validation.mask_mode"
|
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-validation-core"
|
||||||
private const val MASK_GLOBAL = 0
|
private const val MASK_GLOBAL = 0
|
||||||
private const val MASK_OCEAN_A = 1
|
private const val MASK_OCEAN_A = 1
|
||||||
private const val MASK_OCEAN_B = 2
|
private const val MASK_OCEAN_B = 2
|
||||||
|
private const val FIXTURE_UNIFORM_EAST = 0
|
||||||
|
private const val FIXTURE_UNIFORM_NORTH = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,26 @@
|
|||||||
android:text="@string/particle_validation_active_status"
|
android:text="@string/particle_validation_active_status"
|
||||||
android:textColor="@android:color/white" />
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/validationUniformEast"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/particle_validation_uniform_east" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/validationUniformNorth"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/particle_validation_uniform_north" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -22,8 +22,10 @@
|
|||||||
<string name="particle_validation_ocean_b">OCEAN B</string>
|
<string name="particle_validation_ocean_b">OCEAN B</string>
|
||||||
<string name="particle_validation_dateline">DATELINE</string>
|
<string name="particle_validation_dateline">DATELINE</string>
|
||||||
<string name="particle_validation_world_wrap">WORLD WRAP</string>
|
<string name="particle_validation_world_wrap">WORLD WRAP</string>
|
||||||
<string name="particle_validation_global_active_status">30-03 GLOBAL ACTIVE: current-frame Camera/world-wrap validation and GPU simulation.</string>
|
<string name="particle_validation_uniform_east">UNIFORM EAST</string>
|
||||||
<string name="particle_validation_global_static_status">30-03 GLOBAL STATIC: simulation frozen; Camera redraw must keep the world mask aligned.</string>
|
<string name="particle_validation_uniform_north">UNIFORM NORTH</string>
|
||||||
<string name="particle_validation_ocean_a_status">30-03 OCEAN_ONLY_A: blue shared coastal-mask consumer.</string>
|
<string name="particle_validation_global_active_status">40-01 GLOBAL ACTIVE: offline field simulation and current-frame composite.</string>
|
||||||
<string name="particle_validation_ocean_b_status">30-03 OCEAN_ONLY_B: green shared coastal-mask consumer.</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>
|
||||||
|
<string name="particle_validation_ocean_b_status">40-01 OCEAN_ONLY_B: shared coastal-mask consumer.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
|||||||
|
|
||||||
uniform sampler2D u_previous;
|
uniform sampler2D u_previous;
|
||||||
uniform sampler2D u_mask_texture;
|
uniform sampler2D u_mask_texture;
|
||||||
|
uniform sampler2D u_field_texture;
|
||||||
|
uniform sampler2D u_state_texture;
|
||||||
in vec2 v_uv;
|
in vec2 v_uv;
|
||||||
in vec2 v_world;
|
in vec2 v_world;
|
||||||
|
|
||||||
@@ -15,15 +17,27 @@ void main() {
|
|||||||
float mode = u_pass.x;
|
float mode = u_pass.x;
|
||||||
|
|
||||||
if (mode < 0.5) {
|
if (mode < 0.5) {
|
||||||
// State pass: deterministic GPU-only state evolution.
|
// 40-01: State A/B samples the uploaded OfflineFixture Particle Field.
|
||||||
fragColor = vec4(fract(previous.r + 0.03125), v_uv, 1.0);
|
// RG stores field-normalized position, B stores age, and the Field
|
||||||
|
// 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;
|
||||||
|
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);
|
||||||
} else if (mode < 1.5) {
|
} else if (mode < 1.5) {
|
||||||
// Trail pass: fade the previous trail and add a moving validation marker.
|
// The trail consumes State B/A written by the real field-sampling pass.
|
||||||
float marker = 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, u_marker.xy));
|
// Four deterministic texels are enough for the Stage A visual proof.
|
||||||
|
float marker = 0.0;
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.125, 0.125)).rg)));
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.375, 0.375)).rg)));
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.625, 0.625)).rg)));
|
||||||
|
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.875, 0.875)).rg)));
|
||||||
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
||||||
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
||||||
} else {
|
} else {
|
||||||
// VALIDATION ONLY: canonical world-space pattern and shared mask.
|
// Current-frame canonical world composite and shared Mask.
|
||||||
float intensity = max(previous.r, max(previous.g, previous.b));
|
float intensity = max(previous.r, max(previous.g, previous.b));
|
||||||
float ocean = texture(u_mask_texture, v_world).a;
|
float ocean = texture(u_mask_texture, v_world).a;
|
||||||
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
||||||
|
|||||||
195
src/mbgl/particle/field/offline_fixture_particle_adapter.cpp
Normal file
195
src/mbgl/particle/field/offline_fixture_particle_adapter.cpp
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
|
||||||
|
|
||||||
|
#include <rapidjson/document.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace mbgl::particle {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool knownFixture(const std::string& fixtureID) {
|
||||||
|
return fixtureID == "uniform-east" || fixtureID == "uniform-north" || fixtureID == "vortex";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string readText(const std::filesystem::path& path) {
|
||||||
|
std::ifstream input(path);
|
||||||
|
if (!input) throw std::invalid_argument("fixture metadata unavailable: " + path.string());
|
||||||
|
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> readBinary(const std::filesystem::path& path) {
|
||||||
|
std::ifstream input(path, std::ios::binary);
|
||||||
|
if (!input) throw std::invalid_argument("fixture field unavailable: " + path.string());
|
||||||
|
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rapidjson::Value& member(const rapidjson::Value& value, const char* name) {
|
||||||
|
if (!value.HasMember(name)) throw std::invalid_argument(std::string("fixture metadata missing: ") + name);
|
||||||
|
return value[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sha256(const std::vector<char>& input) {
|
||||||
|
constexpr std::array<uint32_t, 64> constants = {
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
|
||||||
|
const auto rotateRight = [](uint32_t value, uint32_t bits) {
|
||||||
|
return (value >> bits) | (value << (32 - bits));
|
||||||
|
};
|
||||||
|
std::vector<uint8_t> bytes(input.begin(), input.end());
|
||||||
|
const uint64_t bitLength = static_cast<uint64_t>(bytes.size()) * 8;
|
||||||
|
bytes.push_back(0x80);
|
||||||
|
while ((bytes.size() % 64) != 56) bytes.push_back(0);
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8) bytes.push_back(static_cast<uint8_t>(bitLength >> shift));
|
||||||
|
|
||||||
|
std::array<uint32_t, 8> hash = {
|
||||||
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
|
||||||
|
for (size_t offset = 0; offset < bytes.size(); offset += 64) {
|
||||||
|
std::array<uint32_t, 64> words{};
|
||||||
|
for (size_t i = 0; i < 16; ++i) {
|
||||||
|
words[i] = (static_cast<uint32_t>(bytes[offset + i * 4]) << 24) |
|
||||||
|
(static_cast<uint32_t>(bytes[offset + i * 4 + 1]) << 16) |
|
||||||
|
(static_cast<uint32_t>(bytes[offset + i * 4 + 2]) << 8) |
|
||||||
|
static_cast<uint32_t>(bytes[offset + i * 4 + 3]);
|
||||||
|
}
|
||||||
|
for (size_t i = 16; i < words.size(); ++i) {
|
||||||
|
const uint32_t a = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ (words[i - 15] >> 3);
|
||||||
|
const uint32_t b = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ (words[i - 2] >> 10);
|
||||||
|
words[i] = words[i - 16] + a + words[i - 7] + b;
|
||||||
|
}
|
||||||
|
uint32_t a = hash[0], b = hash[1], c = hash[2], d = hash[3];
|
||||||
|
uint32_t e = hash[4], f = hash[5], g = hash[6], h = hash[7];
|
||||||
|
for (size_t i = 0; i < words.size(); ++i) {
|
||||||
|
const uint32_t sigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
||||||
|
const uint32_t choice = (e & f) ^ (~e & g);
|
||||||
|
const uint32_t temp1 = h + sigma1 + choice + constants[i] + words[i];
|
||||||
|
const uint32_t sigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
||||||
|
const uint32_t majority = (a & b) ^ (a & c) ^ (b & c);
|
||||||
|
const uint32_t temp2 = sigma0 + majority;
|
||||||
|
h = g;
|
||||||
|
g = f;
|
||||||
|
f = e;
|
||||||
|
e = d + temp1;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = a;
|
||||||
|
a = temp1 + temp2;
|
||||||
|
}
|
||||||
|
hash[0] += a;
|
||||||
|
hash[1] += b;
|
||||||
|
hash[2] += c;
|
||||||
|
hash[3] += d;
|
||||||
|
hash[4] += e;
|
||||||
|
hash[5] += f;
|
||||||
|
hash[6] += g;
|
||||||
|
hash[7] += h;
|
||||||
|
}
|
||||||
|
static constexpr char digits[] = "0123456789abcdef";
|
||||||
|
std::string result;
|
||||||
|
result.reserve(64);
|
||||||
|
for (const uint32_t word : hash) {
|
||||||
|
for (int shift = 28; shift >= 0; shift -= 4) result.push_back(digits[(word >> shift) & 0x0f]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const float component : velocity) {
|
||||||
|
if (!std::isfinite(component) || component == noData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
rapidjson::Document metadata;
|
||||||
|
metadata.Parse(readText(directory / "metadata.json").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() ||
|
||||||
|
std::string(member(metadata, "fixtureId").GetString()) != fixtureID ||
|
||||||
|
!member(metadata, "fieldType").IsString() || !member(metadata, "displayDomain").IsString()) {
|
||||||
|
throw std::invalid_argument("invalid fixture metadata: " + fixtureID);
|
||||||
|
}
|
||||||
|
const auto& grid = member(metadata, "grid");
|
||||||
|
const auto& bounds = member(metadata, "bounds");
|
||||||
|
const auto& vector = member(metadata, "vector");
|
||||||
|
const auto& integrity = member(metadata, "integrity");
|
||||||
|
if (!grid.IsObject() || !bounds.IsObject() || !vector.IsObject() || !integrity.IsObject() ||
|
||||||
|
!member(metadata, "timeIndex").IsUint64() || !member(metadata, "crs").IsString() ||
|
||||||
|
std::string(member(metadata, "crs").GetString()) != "EPSG:4326" || !member(grid, "width").IsUint() ||
|
||||||
|
!member(grid, "height").IsUint() || !member(bounds, "west").IsNumber() || !member(bounds, "south").IsNumber() ||
|
||||||
|
!member(bounds, "east").IsNumber() || !member(bounds, "north").IsNumber() ||
|
||||||
|
!member(grid, "registration").IsString() || !member(grid, "origin").IsString() ||
|
||||||
|
!member(grid, "rowOrder").IsString() || !member(grid, "columnOrder").IsString() ||
|
||||||
|
!member(vector, "uPositive").IsString() || !member(vector, "vPositive").IsString() ||
|
||||||
|
!member(vector, "unit").IsString() || !member(vector, "layout").IsString() ||
|
||||||
|
!member(vector, "encoding").IsString() || !member(vector, "noData").IsNumber() ||
|
||||||
|
!member(vector, "file").IsString() || !member(integrity, "fieldByteLength").IsUint64() ||
|
||||||
|
!member(integrity, "fieldSha256").IsString()) {
|
||||||
|
throw std::invalid_argument("invalid fixture grid/bounds/encoding: " + fixtureID);
|
||||||
|
}
|
||||||
|
if (std::string(grid["registration"].GetString()) != "node" ||
|
||||||
|
std::string(grid["origin"].GetString()) != "north-west" ||
|
||||||
|
std::string(grid["rowOrder"].GetString()) != "north-to-south" ||
|
||||||
|
std::string(grid["columnOrder"].GetString()) != "west-to-east" ||
|
||||||
|
std::string(vector["uPositive"].GetString()) != "east" ||
|
||||||
|
std::string(vector["vPositive"].GetString()) != "north" ||
|
||||||
|
std::string(vector["layout"].GetString()) != "interleaved-uv" ||
|
||||||
|
std::string(vector["encoding"].GetString()) != "float32-le" ||
|
||||||
|
std::string(vector["file"].GetString()) != "field.f32") {
|
||||||
|
throw std::invalid_argument("unsupported fixture v1 layout: " + fixtureID);
|
||||||
|
}
|
||||||
|
ParticleFieldSnapshot field;
|
||||||
|
field.fixtureID = fixtureID;
|
||||||
|
field.fieldType = member(metadata, "fieldType").GetString();
|
||||||
|
field.displayDomain = member(metadata, "displayDomain").GetString();
|
||||||
|
field.width = grid["width"].GetUint();
|
||||||
|
field.height = grid["height"].GetUint();
|
||||||
|
field.west = bounds["west"].GetDouble();
|
||||||
|
field.south = bounds["south"].GetDouble();
|
||||||
|
field.east = bounds["east"].GetDouble();
|
||||||
|
field.north = bounds["north"].GetDouble();
|
||||||
|
field.noData = static_cast<float>(vector["noData"].GetDouble());
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (sha256(bytes) != member(integrity, "fieldSha256").GetString()) {
|
||||||
|
throw std::invalid_argument("fixture field checksum mismatch: " + fixtureID);
|
||||||
|
}
|
||||||
|
field.velocity.resize(expected / sizeof(float));
|
||||||
|
std::memcpy(field.velocity.data(), bytes.data(), expected);
|
||||||
|
if (!field.isValid()) {
|
||||||
|
throw std::invalid_argument("invalid offline particle fixture: " + fixtureID);
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mbgl::particle
|
||||||
39
src/mbgl/particle/field/offline_fixture_particle_adapter.hpp
Normal file
39
src/mbgl/particle/field/offline_fixture_particle_adapter.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace mbgl::particle {
|
||||||
|
|
||||||
|
// Immutable, backend-neutral Field snapshot used by the first offline particle
|
||||||
|
// implementation. The renderer owns only a GPU representation of this value.
|
||||||
|
struct ParticleFieldSnapshot {
|
||||||
|
std::string fixtureID;
|
||||||
|
std::string fieldType;
|
||||||
|
std::string displayDomain;
|
||||||
|
uint32_t width = 0;
|
||||||
|
uint32_t height = 0;
|
||||||
|
double west = -180.0;
|
||||||
|
double south = -85.0;
|
||||||
|
double east = 180.0;
|
||||||
|
double north = 85.0;
|
||||||
|
float noData = -9999.0f;
|
||||||
|
uint64_t dataVersion = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
bool isValid() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Phase-one OfflineFixtureParticleAdapter. Its public contract deliberately
|
||||||
|
// contains no Android/UI/GPU objects, allowing the same decoded snapshot to be
|
||||||
|
// used by future Android Vulkan and iOS renderers.
|
||||||
|
class OfflineFixtureParticleAdapter final {
|
||||||
|
public:
|
||||||
|
static ParticleFieldSnapshot load(const std::string& fixtureID, uint64_t generation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mbgl::particle
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <mbgl/gfx/shader_registry.hpp>
|
#include <mbgl/gfx/shader_registry.hpp>
|
||||||
#include <mbgl/gfx/texture2d.hpp>
|
#include <mbgl/gfx/texture2d.hpp>
|
||||||
#include <mbgl/map/transform_state.hpp>
|
#include <mbgl/map/transform_state.hpp>
|
||||||
|
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
|
||||||
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
|
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
|
||||||
#include <mbgl/renderer/change_request.hpp>
|
#include <mbgl/renderer/change_request.hpp>
|
||||||
#include <mbgl/renderer/layer_group.hpp>
|
#include <mbgl/renderer/layer_group.hpp>
|
||||||
@@ -42,6 +43,7 @@ constexpr Size MaskSize{16, 16};
|
|||||||
|
|
||||||
std::atomic_uint64_t nextGpuGeneration{0};
|
std::atomic_uint64_t nextGpuGeneration{0};
|
||||||
std::atomic_uint64_t nextMaskGeneration{0};
|
std::atomic_uint64_t nextMaskGeneration{0};
|
||||||
|
std::atomic_uint64_t nextFieldGeneration{0};
|
||||||
|
|
||||||
const style::ParticleValidationLayer::Impl& impl(const Immutable<style::Layer::Impl>& value) {
|
const style::ParticleValidationLayer::Impl& impl(const Immutable<style::Layer::Impl>& value) {
|
||||||
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
||||||
@@ -63,6 +65,34 @@ Size validViewportSize(const Size size) {
|
|||||||
return {std::max<uint32_t>(1, size.width), std::max<uint32_t>(1, size.height)};
|
return {std::max<uint32_t>(1, size.width), std::max<uint32_t>(1, size.height)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* fixtureID(const uint8_t mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case 0:
|
||||||
|
return "uniform-east";
|
||||||
|
case 1:
|
||||||
|
return "uniform-north";
|
||||||
|
default:
|
||||||
|
return "vortex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<PremultipliedImage> makeVelocityFieldImage(const particle::ParticleFieldSnapshot& field) {
|
||||||
|
auto image = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
|
||||||
|
for (uint32_t y = 0; y < field.height; ++y) {
|
||||||
|
for (uint32_t x = 0; x < field.width; ++x) {
|
||||||
|
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2;
|
||||||
|
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
|
||||||
|
image->data[pixel + 0] = static_cast<uint8_t>(std::clamp(field.velocity[cell] * 0.5f + 0.5f, 0.0f, 1.0f) *
|
||||||
|
255.0f);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
// VALIDATION ONLY: deterministic global fixture with a diagonal coast. Alpha
|
// VALIDATION ONLY: deterministic global fixture with a diagonal coast. Alpha
|
||||||
// 255 means OCEAN, alpha 0 means LAND. It is intentionally tiny and is not a
|
// 255 means OCEAN, alpha 0 means LAND. It is intentionally tiny and is not a
|
||||||
// production coastline source.
|
// production coastline source.
|
||||||
@@ -104,6 +134,7 @@ public:
|
|||||||
bool stateSeeded = false;
|
bool stateSeeded = false;
|
||||||
bool trailSeeded = false;
|
bool trailSeeded = false;
|
||||||
bool pending = false;
|
bool pending = false;
|
||||||
|
bool bootstrapPending = false;
|
||||||
bool contextDestroyed = false;
|
bool contextDestroyed = false;
|
||||||
bool staticFreezeLogged = false;
|
bool staticFreezeLogged = false;
|
||||||
bool cameraDrawLogged = false;
|
bool cameraDrawLogged = false;
|
||||||
@@ -111,6 +142,9 @@ public:
|
|||||||
bool antimeridianLogged = false;
|
bool antimeridianLogged = false;
|
||||||
uint64_t lastCameraLogFrame = 0;
|
uint64_t lastCameraLogFrame = 0;
|
||||||
uint64_t maskGeneration = 0;
|
uint64_t maskGeneration = 0;
|
||||||
|
uint64_t fieldGeneration = 0;
|
||||||
|
uint64_t gpuFieldGeneration = 0;
|
||||||
|
uint8_t fixtureMode = 255;
|
||||||
uint8_t loggedMaskMode = 255;
|
uint8_t loggedMaskMode = 255;
|
||||||
Size trailSize{};
|
Size trailSize{};
|
||||||
|
|
||||||
@@ -119,6 +153,10 @@ public:
|
|||||||
std::vector<RenderTargetPtr> registeredTargets;
|
std::vector<RenderTargetPtr> registeredTargets;
|
||||||
gfx::ShaderProgramBasePtr shader;
|
gfx::ShaderProgramBasePtr shader;
|
||||||
gfx::Texture2DPtr maskTexture;
|
gfx::Texture2DPtr maskTexture;
|
||||||
|
gfx::Texture2DPtr fieldTexture;
|
||||||
|
gfx::Texture2DPtr stateTexture;
|
||||||
|
particle::ParticleFieldSnapshot field;
|
||||||
|
std::shared_ptr<PremultipliedImage> fieldImage;
|
||||||
std::shared_ptr<PremultipliedImage> maskFixture;
|
std::shared_ptr<PremultipliedImage> maskFixture;
|
||||||
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
|
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
|
||||||
SegmentVector segments;
|
SegmentVector segments;
|
||||||
@@ -153,7 +191,10 @@ public:
|
|||||||
trailRead = trailWrite();
|
trailRead = trailWrite();
|
||||||
stateSeeded = true;
|
stateSeeded = true;
|
||||||
trailSeeded = true;
|
trailSeeded = true;
|
||||||
|
if (!bootstrapPending) {
|
||||||
++committedFrame;
|
++committedFrame;
|
||||||
|
}
|
||||||
|
bootstrapPending = false;
|
||||||
pending = false;
|
pending = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -169,6 +210,8 @@ public:
|
|||||||
gfx::Texture2DPtr source_,
|
gfx::Texture2DPtr source_,
|
||||||
gfx::Texture2DPtr target_,
|
gfx::Texture2DPtr target_,
|
||||||
gfx::Texture2DPtr mask_,
|
gfx::Texture2DPtr mask_,
|
||||||
|
gfx::Texture2DPtr stateTexture_,
|
||||||
|
float simulationStep_,
|
||||||
int16_t worldWrap_,
|
int16_t worldWrap_,
|
||||||
uint8_t maskMode_)
|
uint8_t maskMode_)
|
||||||
: state(std::move(state_)),
|
: state(std::move(state_)),
|
||||||
@@ -178,6 +221,8 @@ public:
|
|||||||
source(std::move(source_)),
|
source(std::move(source_)),
|
||||||
target(std::move(target_)),
|
target(std::move(target_)),
|
||||||
mask(std::move(mask_)),
|
mask(std::move(mask_)),
|
||||||
|
stateTexture(std::move(stateTexture_)),
|
||||||
|
simulationStep(simulationStep_),
|
||||||
worldWrap(worldWrap_),
|
worldWrap(worldWrap_),
|
||||||
maskMode(maskMode_) {}
|
maskMode(maskMode_) {}
|
||||||
|
|
||||||
@@ -193,9 +238,14 @@ public:
|
|||||||
if (mask) {
|
if (mask) {
|
||||||
drawable.setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
|
drawable.setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
|
||||||
}
|
}
|
||||||
|
if (resources->fieldTexture) {
|
||||||
|
drawable.setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture);
|
||||||
|
}
|
||||||
|
if (stateTexture) {
|
||||||
|
drawable.setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
|
||||||
|
}
|
||||||
|
|
||||||
const bool frozen = pass == ValidationPass::FrozenComposite;
|
const float frame = simulationStep;
|
||||||
const float frame = static_cast<float>(resources->committedFrame + (frozen ? 0 : 1));
|
|
||||||
const float markerX = 0.5f + 0.32f * std::sin(frame * 0.071f);
|
const float markerX = 0.5f + 0.32f * std::sin(frame * 0.071f);
|
||||||
const float markerY = 0.5f + 0.32f * std::cos(frame * 0.053f);
|
const float markerY = 0.5f + 0.32f * std::cos(frame * 0.053f);
|
||||||
const shaders::ParticleGpuValidationUBO ubo = {
|
const shaders::ParticleGpuValidationUBO ubo = {
|
||||||
@@ -308,6 +358,8 @@ private:
|
|||||||
gfx::Texture2DPtr source;
|
gfx::Texture2DPtr source;
|
||||||
gfx::Texture2DPtr target;
|
gfx::Texture2DPtr target;
|
||||||
gfx::Texture2DPtr mask;
|
gfx::Texture2DPtr mask;
|
||||||
|
gfx::Texture2DPtr stateTexture;
|
||||||
|
float simulationStep;
|
||||||
int16_t worldWrap;
|
int16_t worldWrap;
|
||||||
uint8_t maskMode;
|
uint8_t maskMode;
|
||||||
};
|
};
|
||||||
@@ -336,6 +388,8 @@ bool buildQuad(gfx::Context& context,
|
|||||||
const gfx::Texture2DPtr& source,
|
const gfx::Texture2DPtr& source,
|
||||||
const gfx::Texture2DPtr& target,
|
const gfx::Texture2DPtr& target,
|
||||||
const gfx::Texture2DPtr& mask,
|
const gfx::Texture2DPtr& mask,
|
||||||
|
const gfx::Texture2DPtr& stateTexture,
|
||||||
|
const float simulationStep,
|
||||||
const ValidationPass pass,
|
const ValidationPass pass,
|
||||||
const uint8_t readSlot,
|
const uint8_t readSlot,
|
||||||
const uint8_t writeSlot,
|
const uint8_t writeSlot,
|
||||||
@@ -372,8 +426,14 @@ bool buildQuad(gfx::Context& context,
|
|||||||
if (mask) {
|
if (mask) {
|
||||||
builder->setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
|
builder->setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
|
||||||
}
|
}
|
||||||
|
if (resources->fieldTexture) {
|
||||||
|
builder->setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture);
|
||||||
|
}
|
||||||
|
if (stateTexture) {
|
||||||
|
builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
|
||||||
|
}
|
||||||
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(
|
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(
|
||||||
resources, pass, readSlot, writeSlot, source, target, mask, worldWrap, maskMode));
|
resources, pass, readSlot, writeSlot, source, target, mask, stateTexture, simulationStep, worldWrap, maskMode));
|
||||||
|
|
||||||
// DrawableBuilder owns the current drawable until flush finalizes it.
|
// DrawableBuilder owns the current drawable until flush finalizes it.
|
||||||
builder->flush(context);
|
builder->flush(context);
|
||||||
@@ -389,7 +449,8 @@ bool buildQuad(gfx::Context& context,
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
RenderParticleValidationLayer::RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl> impl_)
|
RenderParticleValidationLayer::RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl> impl_)
|
||||||
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(impl_, impl_->active, impl_->maskMode)) {
|
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(
|
||||||
|
impl_, impl_->active, impl_->maskMode, impl_->fixtureMode)) {
|
||||||
Log::Info(
|
Log::Info(
|
||||||
Event::Render,
|
Event::Render,
|
||||||
"ParticleValidation: RenderLayer create id=" + getID() + " source-less=true pass=translucent stage=30-02");
|
"ParticleValidation: RenderLayer create id=" + getID() + " source-less=true pass=translucent stage=30-02");
|
||||||
@@ -407,16 +468,17 @@ RenderParticleValidationLayer::~RenderParticleValidationLayer() {
|
|||||||
void RenderParticleValidationLayer::evaluate(const PropertyEvaluationParameters&) {
|
void RenderParticleValidationLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||||
const bool active = impl(baseImpl).active;
|
const bool active = impl(baseImpl).active;
|
||||||
const uint8_t maskMode = impl(baseImpl).maskMode;
|
const uint8_t maskMode = impl(baseImpl).maskMode;
|
||||||
|
const uint8_t fixtureMode = impl(baseImpl).fixtureMode;
|
||||||
auto properties = makeMutable<style::ParticleValidationLayerProperties>(
|
auto properties = makeMutable<style::ParticleValidationLayerProperties>(
|
||||||
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active, maskMode);
|
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active, maskMode, fixtureMode);
|
||||||
passes = RenderPass::Translucent;
|
passes = RenderPass::Translucent;
|
||||||
properties->renderPasses = mbgl::underlying_type(passes);
|
properties->renderPasses = mbgl::underlying_type(passes);
|
||||||
evaluatedProperties = std::move(properties);
|
evaluatedProperties = std::move(properties);
|
||||||
|
|
||||||
Log::Info(Event::Render,
|
Log::Info(Event::Render,
|
||||||
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") +
|
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") +
|
||||||
" maskMode=" + std::to_string(maskMode) + " id=" + getID() +
|
" maskMode=" + std::to_string(maskMode) + " id=" + getID() + " fixture=" + fixtureID(fixtureMode) +
|
||||||
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=30-03");
|
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=40-01");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderParticleValidationLayer::needsRepaint() const {
|
bool RenderParticleValidationLayer::needsRepaint() const {
|
||||||
@@ -471,6 +533,7 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
|
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
|
||||||
const bool active = properties.active;
|
const bool active = properties.active;
|
||||||
const uint8_t maskMode = properties.maskMode;
|
const uint8_t maskMode = properties.maskMode;
|
||||||
|
const uint8_t fixtureMode = properties.fixtureMode;
|
||||||
auto resources = gpuValidation;
|
auto resources = gpuValidation;
|
||||||
if (resources && resources->contextDestroyed) {
|
if (resources && resources->contextDestroyed) {
|
||||||
// VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale
|
// VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale
|
||||||
@@ -479,13 +542,6 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
resources.reset();
|
resources.reset();
|
||||||
}
|
}
|
||||||
if (!resources) {
|
if (!resources) {
|
||||||
if (!active) {
|
|
||||||
// STATIC without a preceding ACTIVE frame has no trail to show. Do not create,
|
|
||||||
// clear, or advance offscreen resources merely because the map redraws.
|
|
||||||
static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto owned = std::make_shared<ParticleGpuValidationResources>();
|
auto owned = std::make_shared<ParticleGpuValidationResources>();
|
||||||
owned->generation = ++nextGpuGeneration;
|
owned->generation = ++nextGpuGeneration;
|
||||||
owned->shader = context.getGenericShader(shaders, ShaderName);
|
owned->shader = context.getGenericShader(shaders, ShaderName);
|
||||||
@@ -511,11 +567,51 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
.wrapU = gfx::TextureWrapType::Clamp,
|
.wrapU = gfx::TextureWrapType::Clamp,
|
||||||
.wrapV = gfx::TextureWrapType::Clamp});
|
.wrapV = gfx::TextureWrapType::Clamp});
|
||||||
owned->maskGeneration = ++nextMaskGeneration;
|
owned->maskGeneration = ++nextMaskGeneration;
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
Log::Error(Event::Render, "ParticleGPU: field resource creation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
owned->fieldTexture->setImage(owned->fieldImage);
|
||||||
|
owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||||
|
.wrapU = gfx::TextureWrapType::Repeat,
|
||||||
|
.wrapV = gfx::TextureWrapType::Clamp});
|
||||||
|
owned->gpuFieldGeneration = owned->fieldGeneration;
|
||||||
|
owned->fixtureMode = fixtureMode;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
Log::Error(Event::Render, "ParticleField: fixture load failed: " + std::string(error.what()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
gpuValidation = std::move(owned);
|
gpuValidation = std::move(owned);
|
||||||
resources = gpuValidation;
|
resources = gpuValidation;
|
||||||
Log::Info(Event::Render,
|
Log::Info(Event::Render,
|
||||||
"ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) +
|
"ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) +
|
||||||
" size=16x16 independent=true");
|
" size=16x16 independent=true");
|
||||||
|
Log::Info(
|
||||||
|
Event::Render,
|
||||||
|
"ParticleField: fixture loaded id=" + resources->field.fixtureID + " type=" + resources->field.fieldType +
|
||||||
|
" bounds=[" + std::to_string(resources->field.west) + ',' + std::to_string(resources->field.east) +
|
||||||
|
"]x[" + std::to_string(resources->field.south) + ',' + std::to_string(resources->field.north) +
|
||||||
|
"] grid=" + std::to_string(resources->field.width) + 'x' + std::to_string(resources->field.height) +
|
||||||
|
" dataVersion=" + std::to_string(resources->field.dataVersion) +
|
||||||
|
" fieldGeneration=" + std::to_string(resources->fieldGeneration));
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleField: fixture validated id=" + resources->field.fixtureID +
|
||||||
|
" noData=" + std::to_string(resources->field.noData) +
|
||||||
|
" velocityCount=" + std::to_string(resources->field.velocity.size()));
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleField: created id=" + resources->field.fixtureID +
|
||||||
|
" generation=" + std::to_string(resources->fieldGeneration));
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleGPU: field upload reason=RESOURCE_RECREATE fixture=" + resources->field.fixtureID +
|
||||||
|
" fieldGeneration=" + std::to_string(resources->fieldGeneration) +
|
||||||
|
" gpuFieldGeneration=" + std::to_string(resources->gpuFieldGeneration) +
|
||||||
|
" resourceGeneration=" + std::to_string(resources->generation));
|
||||||
Log::Info(
|
Log::Info(
|
||||||
Event::Render,
|
Event::Render,
|
||||||
"ParticleValidation30_03: mask fixture ready id=coastal-mask-validation-v1 bounds=[-180,180]x[-85,85] "
|
"ParticleValidation30_03: mask fixture ready id=coastal-mask-validation-v1 bounds=[-180,180]x[-85,85] "
|
||||||
@@ -525,6 +621,41 @@ 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");
|
"ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resources->fixtureMode != fixtureMode) {
|
||||||
|
try {
|
||||||
|
const auto fieldGeneration = ++nextFieldGeneration;
|
||||||
|
auto field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), fieldGeneration);
|
||||||
|
auto image = makeVelocityFieldImage(field);
|
||||||
|
auto texture = context.createTexture2D();
|
||||||
|
if (!image || !texture) {
|
||||||
|
Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
texture->setImage(image);
|
||||||
|
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||||
|
.wrapU = gfx::TextureWrapType::Repeat,
|
||||||
|
.wrapV = gfx::TextureWrapType::Clamp});
|
||||||
|
resources->field = std::move(field);
|
||||||
|
resources->fieldImage = std::move(image);
|
||||||
|
resources->fieldTexture = std::move(texture);
|
||||||
|
resources->fieldGeneration = fieldGeneration;
|
||||||
|
resources->gpuFieldGeneration = fieldGeneration;
|
||||||
|
resources->fixtureMode = fixtureMode;
|
||||||
|
resources->resetSequence();
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleRender: fixture switched id=" + resources->field.fixtureID +
|
||||||
|
" fieldGeneration=" + std::to_string(resources->fieldGeneration) + " gpuFieldGeneration=" +
|
||||||
|
std::to_string(resources->gpuFieldGeneration) + " upload reason=FIELD_CHANGED trail=cleared");
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleField: fixture validated id=" + resources->field.fixtureID +
|
||||||
|
" noData=" + std::to_string(resources->field.noData) +
|
||||||
|
" velocityCount=" + std::to_string(resources->field.velocity.size()));
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
Log::Error(Event::Render, "ParticleField: fixture switch failed: " + std::string(error.what()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (resources->loggedMaskMode != maskMode) {
|
if (resources->loggedMaskMode != maskMode) {
|
||||||
Log::Info(Event::Render,
|
Log::Info(Event::Render,
|
||||||
"ParticleValidation30_03: mask mode=" + std::to_string(maskMode) + " consumer=" +
|
"ParticleValidation30_03: mask mode=" + std::to_string(maskMode) + " consumer=" +
|
||||||
@@ -536,13 +667,119 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
removeTargets(resources->registeredTargets, changes);
|
removeTargets(resources->registeredTargets, changes);
|
||||||
resources->registeredTargets.clear();
|
resources->registeredTargets.clear();
|
||||||
|
|
||||||
|
const auto viewport = validViewportSize(transformState.getSize());
|
||||||
|
const auto ensureTrailTargets = [&] {
|
||||||
|
if (resources->trailTargets[0] && resources->trailTargets[1] && resources->trailSize == viewport) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto previousSize = resources->trailSize;
|
||||||
|
resources->trailTargets = {};
|
||||||
|
for (uint8_t i = 0; i < 2; ++i) {
|
||||||
|
resources->trailTargets[i] = createValidationTarget(
|
||||||
|
context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear);
|
||||||
|
}
|
||||||
|
if (!resources->trailTargets[0] || !resources->trailTargets[1]) {
|
||||||
|
Log::Error(Event::Render, "ParticleGPU: Trail resource creation failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resources->trailSize = viewport;
|
||||||
|
resources->trailRead = 0;
|
||||||
|
resources->trailSeeded = false;
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleGPU: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") +
|
||||||
|
" resourceGeneration=" + std::to_string(resources->generation) +
|
||||||
|
" old=" + std::to_string(previousSize.width) + "x" + std::to_string(previousSize.height) +
|
||||||
|
" new=" + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) +
|
||||||
|
" StatePreserved=true");
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
// Map gestures must still redraw the main pass, but STATIC is a simulation freeze:
|
// STATIC freezes simulation only. A recreated activity still needs the
|
||||||
// no State/Trail RenderTarget is registered, no pending swap survives, and the
|
// resource bootstrap and first current-frame composite without a button tap.
|
||||||
// composite samples the last committed Trail slot with the current map frame.
|
if (!ensureTrailTargets()) {
|
||||||
|
static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
||||||
|
return;
|
||||||
|
}
|
||||||
resources->pending = false;
|
resources->pending = false;
|
||||||
auto& compositeGroup = static_cast<LayerGroup&>(*layerGroup);
|
auto& compositeGroup = static_cast<LayerGroup&>(*layerGroup);
|
||||||
if (resources->trailSeeded && resources->trailTargets[resources->trailRead]) {
|
if (!resources->stateSeeded || !resources->trailSeeded) {
|
||||||
|
const auto stateWrite = resources->stateWrite();
|
||||||
|
const auto trailWrite = resources->trailWrite();
|
||||||
|
auto& stateGroup = static_cast<LayerGroup&>(*resources->stateTargets[stateWrite]->getLayerGroup(0));
|
||||||
|
auto& trailGroup = static_cast<LayerGroup&>(*resources->trailTargets[trailWrite]->getLayerGroup(0));
|
||||||
|
const bool stateBuilt = buildQuad(context,
|
||||||
|
resources,
|
||||||
|
stateGroup,
|
||||||
|
getID() + "-static-bootstrap-state-pass",
|
||||||
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
||||||
|
resources->stateTargets[stateWrite]->getTexture(),
|
||||||
|
resources->maskTexture,
|
||||||
|
{},
|
||||||
|
0.0f,
|
||||||
|
ValidationPass::State,
|
||||||
|
resources->stateRead,
|
||||||
|
stateWrite,
|
||||||
|
0,
|
||||||
|
maskMode,
|
||||||
|
true);
|
||||||
|
const bool trailBuilt = buildQuad(context,
|
||||||
|
resources,
|
||||||
|
trailGroup,
|
||||||
|
getID() + "-static-bootstrap-trail-pass",
|
||||||
|
resources->trailTargets[resources->trailRead]->getTexture(),
|
||||||
|
resources->trailTargets[trailWrite]->getTexture(),
|
||||||
|
resources->maskTexture,
|
||||||
|
resources->stateTargets[stateWrite]->getTexture(),
|
||||||
|
0.0f,
|
||||||
|
ValidationPass::Trail,
|
||||||
|
resources->trailRead,
|
||||||
|
trailWrite,
|
||||||
|
0,
|
||||||
|
maskMode,
|
||||||
|
true);
|
||||||
|
compositeGroup.clearDrawables();
|
||||||
|
bool compositeBuilt = true;
|
||||||
|
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
||||||
|
const auto centerWrap = wrapForLongitude(center.longitude());
|
||||||
|
for (int16_t wrap = static_cast<int16_t>(centerWrap - 1); wrap <= centerWrap + 1; ++wrap) {
|
||||||
|
compositeBuilt = buildQuad(context,
|
||||||
|
resources,
|
||||||
|
compositeGroup,
|
||||||
|
getID() + "-static-bootstrap-composite-pass-" + std::to_string(wrap),
|
||||||
|
resources->trailTargets[trailWrite]->getTexture(),
|
||||||
|
{},
|
||||||
|
resources->maskTexture,
|
||||||
|
{},
|
||||||
|
0.0f,
|
||||||
|
ValidationPass::Composite,
|
||||||
|
resources->trailRead,
|
||||||
|
trailWrite,
|
||||||
|
wrap,
|
||||||
|
maskMode,
|
||||||
|
false) &&
|
||||||
|
compositeBuilt;
|
||||||
|
}
|
||||||
|
if (!stateBuilt || !trailBuilt || !compositeBuilt) {
|
||||||
|
compositeGroup.clearDrawables();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resources->registeredTargets.emplace_back(resources->stateTargets[resources->stateRead]);
|
||||||
|
resources->registeredTargets.emplace_back(resources->stateTargets[stateWrite]);
|
||||||
|
resources->registeredTargets.emplace_back(resources->trailTargets[resources->trailRead]);
|
||||||
|
resources->registeredTargets.emplace_back(resources->trailTargets[trailWrite]);
|
||||||
|
for (const auto& target : resources->registeredTargets) {
|
||||||
|
changes.emplace_back(std::make_unique<AddRenderTargetRequest>(target));
|
||||||
|
}
|
||||||
|
resources->bootstrapPending = true;
|
||||||
|
resources->pending = true;
|
||||||
|
Log::Info(Event::Render,
|
||||||
|
"ParticleRender: STATIC bootstrap scheduled resourceGeneration=" +
|
||||||
|
std::to_string(resources->generation) + " fixture=" + resources->field.fixtureID +
|
||||||
|
" simulationFrame=" + std::to_string(resources->committedFrame));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resources->trailTargets[resources->trailRead]) {
|
||||||
compositeGroup.clearDrawables();
|
compositeGroup.clearDrawables();
|
||||||
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
||||||
const auto centerWrap = wrapForLongitude(center.longitude());
|
const auto centerWrap = wrapForLongitude(center.longitude());
|
||||||
@@ -554,6 +791,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
resources->trailTargets[resources->trailRead]->getTexture(),
|
resources->trailTargets[resources->trailRead]->getTexture(),
|
||||||
{},
|
{},
|
||||||
resources->maskTexture,
|
resources->maskTexture,
|
||||||
|
{},
|
||||||
|
0.0f,
|
||||||
ValidationPass::FrozenComposite,
|
ValidationPass::FrozenComposite,
|
||||||
resources->trailRead,
|
resources->trailRead,
|
||||||
resources->trailRead,
|
resources->trailRead,
|
||||||
@@ -567,29 +806,9 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resources->staticFreezeLogged = false;
|
resources->staticFreezeLogged = false;
|
||||||
|
if (!ensureTrailTargets()) {
|
||||||
const auto viewport = validViewportSize(transformState.getSize());
|
|
||||||
if (!resources->trailTargets[0] || !resources->trailTargets[1] || resources->trailSize != viewport) {
|
|
||||||
const auto previousSize = resources->trailSize;
|
|
||||||
resources->trailTargets = {};
|
|
||||||
for (uint8_t i = 0; i < 2; ++i) {
|
|
||||||
resources->trailTargets[i] = createValidationTarget(
|
|
||||||
context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear);
|
|
||||||
}
|
|
||||||
if (!resources->trailTargets[0] || !resources->trailTargets[1]) {
|
|
||||||
Log::Error(Event::Render, "ParticleValidation30_02: Trail resource creation failed");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resources->trailSize = viewport;
|
|
||||||
resources->trailRead = 0;
|
|
||||||
resources->trailSeeded = false;
|
|
||||||
Log::Info(Event::Render,
|
|
||||||
"ParticleValidation30_02: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") +
|
|
||||||
" generation=" + std::to_string(resources->generation) +
|
|
||||||
" old=" + std::to_string(previousSize.width) + "x" + std::to_string(previousSize.height) +
|
|
||||||
" new=" + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) +
|
|
||||||
" StatePreserved=true");
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto stateWrite = resources->stateWrite();
|
const auto stateWrite = resources->stateWrite();
|
||||||
const auto trailWrite = resources->trailWrite();
|
const auto trailWrite = resources->trailWrite();
|
||||||
@@ -604,6 +823,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
resources->stateTargets[resources->stateRead]->getTexture(),
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
||||||
resources->stateTargets[stateWrite]->getTexture(),
|
resources->stateTargets[stateWrite]->getTexture(),
|
||||||
resources->maskTexture,
|
resources->maskTexture,
|
||||||
|
{},
|
||||||
|
1.0f,
|
||||||
ValidationPass::State,
|
ValidationPass::State,
|
||||||
resources->stateRead,
|
resources->stateRead,
|
||||||
stateWrite,
|
stateWrite,
|
||||||
@@ -617,6 +838,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
resources->trailTargets[resources->trailRead]->getTexture(),
|
resources->trailTargets[resources->trailRead]->getTexture(),
|
||||||
resources->trailTargets[trailWrite]->getTexture(),
|
resources->trailTargets[trailWrite]->getTexture(),
|
||||||
resources->maskTexture,
|
resources->maskTexture,
|
||||||
|
resources->stateTargets[stateWrite]->getTexture(),
|
||||||
|
1.0f,
|
||||||
ValidationPass::Trail,
|
ValidationPass::Trail,
|
||||||
resources->trailRead,
|
resources->trailRead,
|
||||||
trailWrite,
|
trailWrite,
|
||||||
@@ -635,6 +858,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
resources->trailTargets[trailWrite]->getTexture(),
|
resources->trailTargets[trailWrite]->getTexture(),
|
||||||
{},
|
{},
|
||||||
resources->maskTexture,
|
resources->maskTexture,
|
||||||
|
{},
|
||||||
|
0.0f,
|
||||||
ValidationPass::Composite,
|
ValidationPass::Composite,
|
||||||
resources->trailRead,
|
resources->trailRead,
|
||||||
trailWrite,
|
trailWrite,
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ const std::vector<AttributeInfo> ParticleGpuValidationShaderInfo::attributes = {
|
|||||||
const std::vector<TextureInfo> ParticleGpuValidationShaderInfo::textures = {
|
const std::vector<TextureInfo> ParticleGpuValidationShaderInfo::textures = {
|
||||||
TextureInfo{"u_previous", idParticleGpuValidationPreviousTexture},
|
TextureInfo{"u_previous", idParticleGpuValidationPreviousTexture},
|
||||||
TextureInfo{"u_mask_texture", idParticleGpuValidationMaskTexture},
|
TextureInfo{"u_mask_texture", idParticleGpuValidationMaskTexture},
|
||||||
|
TextureInfo{"u_field_texture", idParticleGpuValidationFieldTexture},
|
||||||
|
TextureInfo{"u_state_texture", idParticleGpuValidationStateTexture},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hillshade Prepare
|
// Hillshade Prepare
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace style {
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr const char* ActiveProperty = "particle-validation-active";
|
constexpr const char* ActiveProperty = "particle-validation-active";
|
||||||
constexpr const char* MaskModeProperty = "particle-validation-mask-mode";
|
constexpr const char* MaskModeProperty = "particle-validation-mask-mode";
|
||||||
|
constexpr const char* FixtureModeProperty = "particle-fixture-mode";
|
||||||
|
|
||||||
const LayerTypeInfo typeInfo{.type = "particle-validation",
|
const LayerTypeInfo typeInfo{.type = "particle-validation",
|
||||||
.source = LayerTypeInfo::Source::NotRequired,
|
.source = LayerTypeInfo::Source::NotRequired,
|
||||||
@@ -66,6 +67,10 @@ uint8_t ParticleValidationLayer::getMaskMode() const {
|
|||||||
return impl().maskMode;
|
return impl().maskMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t ParticleValidationLayer::getFixtureMode() const {
|
||||||
|
return impl().fixtureMode;
|
||||||
|
}
|
||||||
|
|
||||||
void ParticleValidationLayer::setActive(bool active) {
|
void ParticleValidationLayer::setActive(bool active) {
|
||||||
if (active == isActive()) {
|
if (active == isActive()) {
|
||||||
return;
|
return;
|
||||||
@@ -86,6 +91,16 @@ void ParticleValidationLayer::setMaskMode(uint8_t mode) {
|
|||||||
observer->onLayerChanged(*this);
|
observer->onLayerChanged(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ParticleValidationLayer::setFixtureMode(uint8_t mode) {
|
||||||
|
if (mode > 2 || mode == getFixtureMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto impl_ = mutableImpl();
|
||||||
|
impl_->fixtureMode = mode;
|
||||||
|
baseImpl = std::move(impl_);
|
||||||
|
observer->onLayerChanged(*this);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(const std::string& name,
|
std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(const std::string& name,
|
||||||
const conversion::Convertible& value) {
|
const conversion::Convertible& value) {
|
||||||
if (name == MaskModeProperty) {
|
if (name == MaskModeProperty) {
|
||||||
@@ -97,6 +112,15 @@ std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(co
|
|||||||
setMaskMode(static_cast<uint8_t>(*converted));
|
setMaskMode(static_cast<uint8_t>(*converted));
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
if (name == FixtureModeProperty) {
|
||||||
|
conversion::Error error;
|
||||||
|
auto converted = conversion::convert<float>(value, error);
|
||||||
|
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted) {
|
||||||
|
return conversion::Error{"particle-fixture-mode must be 0, 1, or 2"};
|
||||||
|
}
|
||||||
|
setFixtureMode(static_cast<uint8_t>(*converted));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
if (name != ActiveProperty) {
|
if (name != ActiveProperty) {
|
||||||
return conversion::Error{"layer doesn't support this property"};
|
return conversion::Error{"layer doesn't support this property"};
|
||||||
}
|
}
|
||||||
@@ -116,6 +140,9 @@ StyleProperty ParticleValidationLayer::getProperty(const std::string& name) cons
|
|||||||
if (name == MaskModeProperty) {
|
if (name == MaskModeProperty) {
|
||||||
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
|
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
|
||||||
}
|
}
|
||||||
|
if (name == FixtureModeProperty) {
|
||||||
|
return StyleProperty(Value{static_cast<uint64_t>(getFixtureMode())}, StyleProperty::Kind::Constant);
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +150,7 @@ Value ParticleValidationLayer::serialize() const {
|
|||||||
auto result = Layer::serialize();
|
auto result = Layer::serialize();
|
||||||
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
|
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
|
||||||
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
|
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
|
||||||
|
serializeProperty(result, getProperty(FixtureModeProperty), FixtureModeProperty, true);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,21 +19,28 @@ public:
|
|||||||
bool active = true;
|
bool active = true;
|
||||||
// VALIDATION ONLY: 0=GLOBAL, 1=OCEAN_ONLY_A, 2=OCEAN_ONLY_B.
|
// VALIDATION ONLY: 0=GLOBAL, 1=OCEAN_ONLY_A, 2=OCEAN_ONLY_B.
|
||||||
uint8_t maskMode = 0;
|
uint8_t maskMode = 0;
|
||||||
|
// 40-01 offline Field selection: 0=uniform-east, 1=uniform-north, 2=vortex.
|
||||||
|
uint8_t fixtureMode = 0;
|
||||||
|
|
||||||
DECLARE_LAYER_TYPE_INFO;
|
DECLARE_LAYER_TYPE_INFO;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ParticleValidationLayerProperties final : public LayerProperties {
|
class ParticleValidationLayerProperties final : public LayerProperties {
|
||||||
public:
|
public:
|
||||||
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl, bool active_, uint8_t maskMode_)
|
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl,
|
||||||
|
bool active_,
|
||||||
|
uint8_t maskMode_,
|
||||||
|
uint8_t fixtureMode_)
|
||||||
: LayerProperties(std::move(impl)),
|
: LayerProperties(std::move(impl)),
|
||||||
active(active_),
|
active(active_),
|
||||||
maskMode(maskMode_) {}
|
maskMode(maskMode_),
|
||||||
|
fixtureMode(fixtureMode_) {}
|
||||||
|
|
||||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||||
|
|
||||||
bool active;
|
bool active;
|
||||||
uint8_t maskMode;
|
uint8_t maskMode;
|
||||||
|
uint8_t fixtureMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace style
|
} // namespace style
|
||||||
|
|||||||
27
tools/generate_particle_fixture_f32.py
Normal file
27
tools/generate_particle_fixture_f32.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate deterministic v1 little-endian OfflineFixture field payloads."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
GRID = 16
|
||||||
|
ROOT = Path(__file__).resolve().parents[1] / "particle-fixtures"
|
||||||
|
|
||||||
|
|
||||||
|
def velocity(fixture: str, x: int, y: int) -> tuple[float, float]:
|
||||||
|
if fixture == "uniform-east":
|
||||||
|
return 1.0, 0.0
|
||||||
|
if fixture == "uniform-north":
|
||||||
|
return 0.0, 1.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"):
|
||||||
|
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)
|
||||||
@@ -148,3 +148,12 @@
|
|||||||
- `Rotation/Viewport 自动恢复问题` 为已知问题;用户接受延期,明确在下一个任务修复。本轮不声称该问题已修复,也不将其伪造为已验证功能。
|
- `Rotation/Viewport 自动恢复问题` 为已知问题;用户接受延期,明确在下一个任务修复。本轮不声称该问题已修复,也不将其伪造为已验证功能。
|
||||||
- 真实 EGL context recreation 的独立设备证据、正式 fixture adapter、正式 NoData fail-closed 和生产 coastline 数据仍未完成;EGL 风险沿用 30-02 的 `Accepted Risk / 未验证` 边界。
|
- 真实 EGL context recreation 的独立设备证据、正式 fixture adapter、正式 NoData fail-closed 和生产 coastline 数据仍未完成;EGL 风险沿用 30-02 的 `Accepted Risk / 未验证` 边界。
|
||||||
- 30-03 已允许进入下一阶段;下一任务优先处理 Rotation/Viewport 自动恢复延期项。
|
- 30-03 已允许进入下一阶段;下一任务优先处理 Rotation/Viewport 自动恢复延期项。
|
||||||
|
|
||||||
|
## 2026-08-31 — 40-01 Android 离线 Fixture 到正式粒子渲染最小闭环(Stage A)
|
||||||
|
|
||||||
|
- 30-03 后进入 Android 第一条离线 fixture 粒子闭环;Rotation/Viewport 自动恢复技术债已纳入本任务。
|
||||||
|
- 新增 backend-neutral `OfflineFixtureParticleAdapter` 和 `ParticleFieldSnapshot`,以 v1 metadata、binary length 与 SHA-256 把 uniform-east、uniform-north、vortex 解码为正式 Field;不使用在线数据或 Android UI 依赖。
|
||||||
|
- GPU field texture 接入既有 State A/B;Trail A/B 改为读取 State write texture,fixture 切换递增 field/gpu field generation 并清理旧 trail;Mask 继续走既有 shared GLOBAL/OCEAN_ONLY 路线。
|
||||||
|
- STATIC 不再阻止 resource bootstrap:Activity/context recreation 后会建立 field/mask/trail/state 并做零步 first composite,`simulationFrame` 不增加;Activity 保存 active/enabled/mask/fixture 选择。
|
||||||
|
- 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 子任务。
|
||||||
|
|||||||
Reference in New Issue
Block a user