test: validate particle camera wrap and ocean mask
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 30-03 Camera / World Wrap 与 Land/Ocean Mask 最小技术验证报告 v1
|
||||
|
||||
日期:2026-08-30
|
||||
|
||||
当前状态:`WAITING_MANUAL_VALIDATION`
|
||||
|
||||
## 1. 前置与结论边界
|
||||
|
||||
30-02 最新正式报告 v5 为 `PASS(EGL Context Recreation:Accepted Risk / 未验证;用户明确决定不阻塞)`,并明确允许进入 30-03。本轮是 30-03 阶段 A 的验证代码准备。
|
||||
|
||||
本轮没有运行 Android 编译、Gradle、安装、ADB 或设备操作,也没有用户实际运行证据。因此不得写 PASS;本报告只记录源码和 Linux Core 精确编译可确认的事实。
|
||||
|
||||
## 2. 当前源码链
|
||||
|
||||
- Camera:`TransformState` 由 render frame 建立 `TransformParameters`;`PaintParameters::matrixForTile()` 调用当前 `TransformState::matrixFor()`,再乘当前 frame projection matrix。
|
||||
- World wrap:`UnwrappedTileID` 用 `wrap + CanonicalTileID` 表达绘制 world copy;`TransformState::matrixFor()` 将 `tileID.wrap * tileScale` 纳入 world translation。
|
||||
- 本验证没有独立 Camera、Overlay 或第二套 world 状态机。Composite Tweaker 直接从当前 `PaintParameters` 取得 `matrixForTile({wrap,{0,0,0}})`。
|
||||
|
||||
## 3. 本次 validation-only 实现
|
||||
|
||||
`ParticleGpuValidationShader` 的 State / Trail pass 仍是 30-02 的全屏 RenderTarget pass。主 composite 改为 canonical z0 world quad:vertex shader 以当前 frame world-copy matrix 投影,而不是固定 screen quad。
|
||||
|
||||
每帧按当前未 wrap Camera center 选择中心 wrap 和相邻 wrap (`centerWrap - 1..+1`) 绘制同一 canonical quad;只有中心 copy 可以提交 30-02 slot/frame,其他 copy 仅绘制,故不会把 world copy 数量变成 simulation 推进次数。
|
||||
|
||||
确定性地理锚点为:A `(0, 0)`、B `(0, -90)`、C `(0, -172.8)`、D `(0, 172.8)`。C/D 位于反经线两侧。它们是 shader 中的 canonical Mercator z0 world fraction,不由 Android UI 计算。
|
||||
|
||||
ACTIVE 继续运行 30-02 State/Trail A/B;STATIC 保留既有冻结语义:不注册离屏 target、不 update/fade/marker、不 slot swap、不增 simulation frame,但用当前 `PaintParameters` matrix 重画最后 Trail 的地理 composite 与 mask。
|
||||
|
||||
## 4. Antimeridian 表示
|
||||
|
||||
Mask fixture 使用正式设计中允许的全球 canonical bounds `west=-180, east=180, south=-85, north=85`,不把单一数据文件表示成跨反经线区域。C/D 两个 canonical anchor 跨越该边界;世界连续性由 `UnwrappedTileID.wrap` 与同一 canonical z0 quad 完成。
|
||||
|
||||
GPU sampling 采用 canonical world UV `[0,1]`,不在 shader 以 `if lon > 180` 进行局部纠正。每个 world copy 使用同一 canonical mask UV、各自 MapLibre matrix,因此 geometry 与 mask 不会分别选择不同 copy。
|
||||
|
||||
## 5. Shared Land/Ocean Mask
|
||||
|
||||
当前 `particle-fixtures/coastal-mask/` 在开始时只有占位文件,仓库也尚未具备正式 fixture adapter/upload 链。本轮未伪造 production fixture pipeline;增加的是 validation-only deterministic CPU fixture generator:16×16 RGBA,alpha `0=LAND`、`255=OCEAN`,包含斜向 coast 和非轴对齐缺口。
|
||||
|
||||
它只创建一次 `Texture2D`,采用 nearest + clamp,`maskGeneration` 单调记录。Camera pan/zoom/bearing/pitch 和 `GLOBAL ↔ OCEAN_ONLY_A ↔ OCEAN_ONLY_B` 只改变 drawable uniform/style property,不重新上传 mask。A/B 都绑定同一 `maskTexture`;GLOBAL 不 discard,A 显示蓝色、B 显示绿色且均在 land discard。NoData 在本 synthetic fixture 中不存在;正式 OCEAN_ONLY contract 的 NoData fail-closed 仍未实现。
|
||||
|
||||
`markContextDestroyed()` 后下一次 ACTIVE update 丢弃整套 validation GPU resources(包括 mask texture)并以新 `maskGeneration` 从 CPU fixture 重建,不复用 stale handle。真实 EGL context recreation 仍不在本轮被声称验证。
|
||||
|
||||
## 6. Test App 与日志
|
||||
|
||||
复用 `Particle Core Layer Validation`,新增最小控制:`GLOBAL`、`OCEAN A`、`OCEAN B`、`DATELINE`、`WORLD WRAP`。A/B/GLOBAL 通过既有 Layer 的 paint properties 更新,预期资源复用,不重载 Style;ACTIVE/STATIC/DISABLE/ENABLE 保持原有入口。Activity Bundle 还保存 mask mode。
|
||||
|
||||
主要日志标签为 `ParticleValidation30_03`:fixture/mask create、maskGeneration、mode/consumer reuse、first camera-locked draw、每 30 frame 节流 camera 摘要、world wrap、antimeridian 与 30-02 STATIC frozen composite。没有每 frame 输出完整 matrix。
|
||||
|
||||
## 7. Codex 静态验证
|
||||
|
||||
- `CCACHE_DISABLE=1 cmake --build build-linux-opengl --target CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o CMakeFiles/mbgl-core.dir/src/mbgl/style/layers/particle_validation_layer.cpp.o CMakeFiles/mbgl-core.dir/src/mbgl/shaders/gl/shader_info.cpp.o -- -j3`:PASS。
|
||||
- `clang-format --dry-run --Werror`(受影响 C++/header):PASS。
|
||||
- `git diff --check`:PASS。
|
||||
- Android 编译/运行、Gradle、ADB、GPU visual result:未执行,遵守任务书。
|
||||
|
||||
## 8. 用户手工 Android OpenGL 验证
|
||||
|
||||
编译(由用户手工执行):
|
||||
|
||||
```bash
|
||||
cd /mnt/sda1/MapLibreParticleLab
|
||||
source /root/.maplibre-particle-env
|
||||
cd platform/android
|
||||
RENDERER=opengl BUILDTYPE=Debug JOBS=3 make android-x86-64
|
||||
```
|
||||
|
||||
打开 `Custom Layer → Particle Core Layer Validation`,日志可使用:
|
||||
|
||||
```bash
|
||||
adb -s 192.168.1.146 logcat | grep -E 'ParticleValidation30_02|ParticleValidation30_03|ParticleValidation'
|
||||
```
|
||||
|
||||
1. T1–T5:先 ACTIVE 数秒后切 STATIC,在 GLOBAL 下 pan、zoom、bearing、pitch;锚点/mask 必须跟地理底图移动,STATIC frame 不推进且 `maskGeneration` 不变。再切 ACTIVE,确认 simulation 恢复。
|
||||
2. T6:点 `WORLD WRAP`,水平连续 pan 至非零 wrap;确认相邻 copy 皆显示、无重复 slot commit,并记录 center/zoom/wrap 日志。
|
||||
3. T7/T8:点 `DATELINE`,分别东→西、西→东跨越 ±180°;C/D、pattern 与 mask 不得断裂、翻转、重复或消失。
|
||||
4. T9/T10:在 coastal fixture 区域依次选 `OCEAN A`、`GLOBAL`;A 的 land 必须抑制,GLOBAL 的 land 必须显示。
|
||||
5. T11:`OCEAN A → OCEAN B → OCEAN A`;A/B land/ocean 判断一致、颜色不同,日志 `maskGeneration` 保持不变并显示 `reuse=true`。
|
||||
6. T12:在 DATELINE/WORLD WRAP 场景启用 OCEAN A/B,确认 mask 与 geometry 使用同一 copy,无 ±180° seam。
|
||||
7. T13:进行可稳定触发的 rotation/viewport 变化;确认 mode 恢复、地理对齐和新 resource generation。此项不替代 30-02 EGL context-loss 证据。
|
||||
|
||||
## 9. 未验证与判定
|
||||
|
||||
未验证:Android OpenGL 编译、所有 T1–T13 视觉/日志结果、真实 EGL context loss、正式 fixture adapter、正式 NoData fail-closed 与生产 coastline 数据。
|
||||
|
||||
当前唯一合法结论:`WAITING_MANUAL_VALIDATION`。在用户提供 Android OpenGL 实际运行结果前,不允许进入下一项 iOS 最小验证。
|
||||
@@ -0,0 +1,54 @@
|
||||
# 30-03 Camera / World Wrap 与 Land/Ocean Mask 验证 Codex 完成报告 v1
|
||||
|
||||
日期:2026-08-30
|
||||
|
||||
执行结论:`WAITING_MANUAL_VALIDATION`
|
||||
|
||||
## 1. Git 与前置
|
||||
|
||||
- 分支:`particle/main`。
|
||||
- 开始 HEAD:`3b5a147bbaa3e3d967ee89b16745a3ab8f62c72d`。
|
||||
- 公开远端:`origin`,目标分支 `origin/particle/main`。
|
||||
- 30-02 前置:v5 正式 `PASS`,允许进入 30-03;EGL context recreation 是用户接受且不阻塞的未验证风险。
|
||||
- 开始时有两份用户提供的未跟踪任务指令书(30-02 rotation、30-03);均未修改、未提交。
|
||||
|
||||
已读取 AGENTS、项目总则、项目规范、基线、STEP-000、调查/架构设计/前序报告、开发记录、30-01/30-02 实际 validation Core 与 Android Test App,以及 Transform/Paint/world-wrap/gfx texture 相关源码。
|
||||
|
||||
## 2. 已完成
|
||||
|
||||
- Camera:Composite Tweaker 使用当前 `PaintParameters::matrixForTile()`;没有 UI Camera、独立 timer 或 Overlay。
|
||||
- World wrap:通过当前 `UnwrappedTileID{wrap, CanonicalTileID{0,0,0}}` 与 `TransformState::matrixFor()` 绘制中心及相邻 world copy;仅中心 copy commit simulation。
|
||||
- Antimeridian:使用全球 canonical bounds 与两侧 C/D anchor,未引入局部 longitude clamp 补丁或第二套 wrap 状态机。
|
||||
- Mask:创建一次 validation-only deterministic 16×16 CPU fixture 和一个 shared `Texture2D`;GLOBAL、OCEAN_ONLY_A、OCEAN_ONLY_B 复用同一 texture。
|
||||
- 生命周期:Camera 和 consumer 切换不会重传 mask;通知 context destroyed 后下一个 ACTIVE update 放弃旧资源并创建新 mask generation。
|
||||
- Test App:在已有页面增加 mask consumer、DATELINE 和 WORLD WRAP 控制;A/B/GLOBAL property update 不 reload Style。
|
||||
- 日志:新增 fixture/create/generation/reuse/camera/world-wrap/antimeridian/anchor 字段,camera 摘要节流至每 30 frame。
|
||||
- Linux Core 精确对象编译、clang-format、diff check:PASS。
|
||||
|
||||
## 3. 实际修改
|
||||
|
||||
- `render_particle_validation_layer.cpp`:当前 frame matrix、world-copy composite、shared mask lifecycle 与日志。
|
||||
- `particle_validation_layer.*`:validation-only `particle-validation-mask-mode`(0/1/2)。
|
||||
- validation shader/UBO/shader info:world z0 projection、mask sampling与 anchor。
|
||||
- `ParticleCoreLayerValidationActivity.kt`、布局、strings:最小人工验证控制与 Bundle 保存。
|
||||
|
||||
未修改通用 Transform、Projection、RenderTree、gfx API、正式 Field/Mask pipeline、Vulkan、iOS、依赖或项目总则;没有新增第三方依赖。
|
||||
|
||||
## 4. 未完成与无法验证
|
||||
|
||||
- 未完成:Android OpenGL 手工编译和 T1–T13 运行验证。
|
||||
- 无法验证:真实设备的 pan/zoom/bearing/pitch 对齐、world wrap/antimeridian continuity、A/B visual crop、rotation 后真实 resource generation、真实 EGL context recreation。
|
||||
- `particle-fixtures/coastal-mask/` 尚无 production fixture adapter,因此使用最小 deterministic validation generator;它不是生产 coastline pipeline,正式 `mask.u8`/NoData contract 仍未实现。
|
||||
|
||||
## 5. Android 手工步骤
|
||||
|
||||
技术报告 v1 第 8 节给出了当前 Makefile 的准确 OpenGL x86_64 command、带 `adb -s 192.168.1.146` 的日志命令和 T1–T13。Codex 未执行这些命令。
|
||||
|
||||
## 6. Git 收尾
|
||||
|
||||
- 本报告及代码提交 hash、提交消息、push 结果、最终远端 HEAD 将在提交后按事实补记于最终交付。
|
||||
- 本任务只会 stage 30-03 validation 代码、v1 报告和开发记录;两份用户未跟踪指令书保留。
|
||||
|
||||
## 7. 下一步
|
||||
|
||||
当前为 `WAITING_MANUAL_VALIDATION`,不允许进入 iOS Core Layer 薄接入。用户完成 Android OpenGL T1–T13 并提供日志/现象后,才可更新为 PASS、FAIL 或 BLOCKED。
|
||||
@@ -11,19 +11,38 @@ struct ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Op
|
||||
static constexpr const char* vertex = R"(layout (location = 0) in vec2 a_pos;
|
||||
|
||||
out vec2 v_uv;
|
||||
out vec2 v_world;
|
||||
|
||||
layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
v_uv = a_pos;
|
||||
v_world = a_pos;
|
||||
if (u_pass.x < 1.5) {
|
||||
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
} else {
|
||||
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix is
|
||||
// PaintParameters::matrixForTile() for the selected MapLibre world copy.
|
||||
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
)";
|
||||
static constexpr const char* fragment = R"(layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_mask_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
|
||||
void main() {
|
||||
vec4 previous = texture(u_previous, v_uv);
|
||||
@@ -38,9 +57,20 @@ void main() {
|
||||
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
||||
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
||||
} else {
|
||||
// Main pass: composite the latest trail without hiding the validation map.
|
||||
// VALIDATION ONLY: canonical world-space pattern and shared mask.
|
||||
float intensity = max(previous.r, max(previous.g, previous.b));
|
||||
fragColor = vec4(previous.rgb * 0.72, min(intensity * 0.72, 0.72));
|
||||
float ocean = texture(u_mask_texture, v_world).a;
|
||||
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
||||
float stripe = 0.55 + 0.45 * sin((v_world.x + v_world.y) * 100.0 + u_pass.y * 0.07);
|
||||
// A=(0°,0°), B=(-90°,approximately 0°), C/D straddle ±180°.
|
||||
float anchors = 0.0;
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.50, 0.50))));
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.25, 0.50))));
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.02, 0.50))));
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.98, 0.50))));
|
||||
vec3 color = u_pass.w < 1.5 ? vec3(0.10, 0.70, 1.00) : vec3(0.35, 1.00, 0.45);
|
||||
fragColor = vec4(mix(color * stripe, vec3(1.00, 0.82, 0.15), anchors),
|
||||
0.28 + min(intensity, 0.35) * 0.35 + anchors * 0.35);
|
||||
}
|
||||
|
||||
#ifdef OVERDRAW_INSPECTOR
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
namespace mbgl {
|
||||
namespace shaders {
|
||||
|
||||
// VALIDATION ONLY: parameters for the 30-02 State/Trail ping-pong proof.
|
||||
// VALIDATION ONLY: 30-02 State/Trail and 30-03 Camera/Mask proof parameters.
|
||||
struct alignas(16) ParticleGpuValidationUBO {
|
||||
std::array<float, 4> pass;
|
||||
std::array<float, 4> marker;
|
||||
std::array<float, 4> mask;
|
||||
std::array<float, 16> matrix;
|
||||
};
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 2 * 16);
|
||||
static_assert(sizeof(ParticleGpuValidationUBO) == 7 * 16);
|
||||
|
||||
} // namespace shaders
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -340,6 +340,7 @@ enum {
|
||||
|
||||
enum {
|
||||
idParticleGpuValidationPreviousTexture,
|
||||
idParticleGpuValidationMaskTexture,
|
||||
particleGpuValidationTextureCount
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ public:
|
||||
|
||||
bool isActive() const;
|
||||
void setActive(bool);
|
||||
uint8_t getMaskMode() const;
|
||||
void setMaskMode(uint8_t);
|
||||
|
||||
class Impl;
|
||||
const Impl& impl() const;
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.android.maps.MapView
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
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. */
|
||||
@@ -19,14 +20,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private lateinit var status: TextView
|
||||
private var active = true
|
||||
private var enabled = true
|
||||
private var maskMode = MASK_GLOBAL
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true
|
||||
enabled = savedInstanceState?.getBoolean(STATE_ENABLED, true) ?: true
|
||||
maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.i(
|
||||
TAG,
|
||||
"ParticleValidation30_02: Activity state restored active=$active enabled=$enabled " +
|
||||
"ParticleValidation30_03: Activity state restored active=$active enabled=$enabled maskMode=$maskMode " +
|
||||
"recreated=${savedInstanceState != null}"
|
||||
)
|
||||
setContentView(R.layout.activity_particle_core_layer_validation)
|
||||
@@ -46,10 +49,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.validationActive).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active = true, enabled = enabled)
|
||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = true, maskMode = maskMode)
|
||||
}
|
||||
findViewById<Button>(R.id.validationStatic).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active = false, enabled = enabled)
|
||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = false, maskMode = maskMode)
|
||||
}
|
||||
findViewById<Button>(R.id.validationDisable).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active = active, enabled = false)
|
||||
@@ -57,28 +60,63 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationEnable).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active = active, enabled = true)
|
||||
}
|
||||
findViewById<Button>(R.id.validationGlobal).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_GLOBAL)
|
||||
}
|
||||
findViewById<Button>(R.id.validationOceanA).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_OCEAN_A)
|
||||
}
|
||||
findViewById<Button>(R.id.validationOceanB).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_OCEAN_B)
|
||||
}
|
||||
findViewById<Button>(R.id.validationDateline).setOnClickListener {
|
||||
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")
|
||||
}
|
||||
findViewById<Button>(R.id.validationWorldWrap).setOnClickListener {
|
||||
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 0.0)).zoom(0.0).build()
|
||||
Log.i(TAG, "ParticleValidation30_03: WORLD_WRAP preset center=0,0 zoom=0")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadValidationStyle(active: Boolean, enabled: Boolean) {
|
||||
this.active = active
|
||||
this.enabled = enabled
|
||||
val state = if (!enabled) "DISABLED" else if (active) "ACTIVE" else "STATIC"
|
||||
Log.i(TAG, "ParticleValidation30_02: Test App switching to $state")
|
||||
val state = statusText()
|
||||
Log.i(TAG, "ParticleValidation30_03: Test App switching to $state")
|
||||
maplibreMap.setStyle(Style.Builder().fromJson(validationStyle(active, enabled))) {
|
||||
status.text = getString(
|
||||
when {
|
||||
!enabled -> R.string.particle_validation_disabled_status
|
||||
active -> R.string.particle_validation_active_status
|
||||
else -> R.string.particle_validation_static_status
|
||||
}
|
||||
)
|
||||
status.text = state
|
||||
}
|
||||
}
|
||||
|
||||
private fun setValidationProperties(active: Boolean, maskMode: Int) {
|
||||
this.active = active
|
||||
this.maskMode = maskMode
|
||||
val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID)
|
||||
if (layer == null || !enabled) {
|
||||
loadValidationStyle(active, enabled)
|
||||
return
|
||||
}
|
||||
layer.setProperties(
|
||||
PaintPropertyValue("particle-validation-active", active),
|
||||
PaintPropertyValue("particle-validation-mask-mode", maskMode)
|
||||
)
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleValidation30_03: property update active=$active maskMode=$maskMode resourceReuseExpected=true")
|
||||
}
|
||||
|
||||
private fun statusText(): String = when {
|
||||
!enabled -> getString(R.string.particle_validation_disabled_status)
|
||||
maskMode == MASK_OCEAN_A -> getString(R.string.particle_validation_ocean_a_status)
|
||||
maskMode == MASK_OCEAN_B -> getString(R.string.particle_validation_ocean_b_status)
|
||||
active -> getString(R.string.particle_validation_global_active_status)
|
||||
else -> getString(R.string.particle_validation_global_static_status)
|
||||
}
|
||||
|
||||
private fun validationStyle(active: Boolean, enabled: Boolean): String {
|
||||
val particleLayer = if (enabled) {
|
||||
"""{"id":"particle-validation-core","type":"particle-validation",
|
||||
"paint":{"particle-validation-active":$active}},"""
|
||||
"paint":{"particle-validation-active":$active,"particle-validation-mask-mode":$maskMode}},"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -114,6 +152,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(STATE_ACTIVE, active)
|
||||
outState.putBoolean(STATE_ENABLED, enabled)
|
||||
outState.putInt(STATE_MASK_MODE, maskMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
mapView.onSaveInstanceState(outState)
|
||||
}
|
||||
@@ -122,5 +161,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
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 PARTICLE_LAYER_ID = "particle-validation-core"
|
||||
private const val MASK_GLOBAL = 0
|
||||
private const val MASK_OCEAN_A = 1
|
||||
private const val MASK_OCEAN_B = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,53 @@
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_enable" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationGlobal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_global" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationOceanA"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_ocean_a" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationOceanB"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_ocean_b" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationDateline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_dateline" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationWorldWrap"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_world_wrap" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -17,4 +17,13 @@
|
||||
<string name="particle_validation_active_status">30-02 ACTIVE: GPU State/Trail ping-pong requests frames. Move, zoom, rotate, pitch and resize.</string>
|
||||
<string name="particle_validation_static_status">30-02 STATIC: GPU ping-pong stopped requesting frames. Gestures must still render normally.</string>
|
||||
<string name="particle_validation_disabled_status">30-02 DISABLED: validation layer and its GPU resources were removed. Tap ENABLE to recreate them.</string>
|
||||
<string name="particle_validation_global">GLOBAL</string>
|
||||
<string name="particle_validation_ocean_a">OCEAN A</string>
|
||||
<string name="particle_validation_ocean_b">OCEAN B</string>
|
||||
<string name="particle_validation_dateline">DATELINE</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_global_static_status">30-03 GLOBAL STATIC: simulation frozen; Camera redraw must keep the world mask aligned.</string>
|
||||
<string name="particle_validation_ocean_a_status">30-03 OCEAN_ONLY_A: blue shared coastal-mask consumer.</string>
|
||||
<string name="particle_validation_ocean_b_status">30-03 OCEAN_ONLY_B: green shared coastal-mask consumer.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_mask_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
|
||||
void main() {
|
||||
vec4 previous = texture(u_previous, v_uv);
|
||||
@@ -19,9 +23,20 @@ void main() {
|
||||
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
|
||||
fragColor = vec4(min(color, vec3(1.0)), 1.0);
|
||||
} else {
|
||||
// Main pass: composite the latest trail without hiding the validation map.
|
||||
// VALIDATION ONLY: canonical world-space pattern and shared mask.
|
||||
float intensity = max(previous.r, max(previous.g, previous.b));
|
||||
fragColor = vec4(previous.rgb * 0.72, min(intensity * 0.72, 0.72));
|
||||
float ocean = texture(u_mask_texture, v_world).a;
|
||||
if (u_pass.z > 0.5 && ocean < 0.5) discard;
|
||||
float stripe = 0.55 + 0.45 * sin((v_world.x + v_world.y) * 100.0 + u_pass.y * 0.07);
|
||||
// A=(0°,0°), B=(-90°,approximately 0°), C/D straddle ±180°.
|
||||
float anchors = 0.0;
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.50, 0.50))));
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.25, 0.50))));
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.02, 0.50))));
|
||||
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.98, 0.50))));
|
||||
vec3 color = u_pass.w < 1.5 ? vec3(0.10, 0.70, 1.00) : vec3(0.35, 1.00, 0.45);
|
||||
fragColor = vec4(mix(color * stripe, vec3(1.00, 0.82, 0.15), anchors),
|
||||
0.28 + min(intensity, 0.35) * 0.35 + anchors * 0.35);
|
||||
}
|
||||
|
||||
#ifdef OVERDRAW_INSPECTOR
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
layout (location = 0) in vec2 a_pos;
|
||||
|
||||
out vec2 v_uv;
|
||||
out vec2 v_world;
|
||||
|
||||
layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
highp mat4 u_matrix;
|
||||
};
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
v_uv = a_pos;
|
||||
v_world = a_pos;
|
||||
if (u_pass.x < 1.5) {
|
||||
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
|
||||
} else {
|
||||
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix is
|
||||
// PaintParameters::matrixForTile() for the selected MapLibre world copy.
|
||||
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <mbgl/gfx/drawable_builder.hpp>
|
||||
#include <mbgl/gfx/drawable_tweaker.hpp>
|
||||
#include <mbgl/gfx/shader_registry.hpp>
|
||||
#include <mbgl/gfx/texture2d.hpp>
|
||||
#include <mbgl/map/transform_state.hpp>
|
||||
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
|
||||
#include <mbgl/renderer/change_request.hpp>
|
||||
@@ -19,7 +20,10 @@
|
||||
#include <mbgl/shaders/shader_defines.hpp>
|
||||
#include <mbgl/shaders/shader_program_base.hpp>
|
||||
#include <mbgl/style/types.hpp>
|
||||
#include <mbgl/tile/tile_id.hpp>
|
||||
#include <mbgl/util/image.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
#include <mbgl/util/mat4.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -34,8 +38,10 @@ namespace {
|
||||
|
||||
constexpr auto ShaderName = "ParticleGpuValidationShader";
|
||||
constexpr Size StateSize{16, 16};
|
||||
constexpr Size MaskSize{16, 16};
|
||||
|
||||
std::atomic_uint64_t nextGpuGeneration{0};
|
||||
std::atomic_uint64_t nextMaskGeneration{0};
|
||||
|
||||
const style::ParticleValidationLayer::Impl& impl(const Immutable<style::Layer::Impl>& value) {
|
||||
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
||||
@@ -57,6 +63,28 @@ Size validViewportSize(const Size size) {
|
||||
return {std::max<uint32_t>(1, size.width), std::max<uint32_t>(1, size.height)};
|
||||
}
|
||||
|
||||
// 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
|
||||
// production coastline source.
|
||||
std::shared_ptr<PremultipliedImage> makeCoastalMaskFixture() {
|
||||
auto image = std::make_shared<PremultipliedImage>(MaskSize);
|
||||
for (uint32_t y = 0; y < MaskSize.height; ++y) {
|
||||
for (uint32_t x = 0; x < MaskSize.width; ++x) {
|
||||
const bool ocean = x > (y / 2 + 3) && !(x > 11 && y < 4);
|
||||
const auto offset = (y * MaskSize.width + x) * PremultipliedImage::channels;
|
||||
image->data[offset + 0] = ocean ? 36 : 90;
|
||||
image->data[offset + 1] = ocean ? 128 : 66;
|
||||
image->data[offset + 2] = ocean ? 188 : 42;
|
||||
image->data[offset + 3] = ocean ? 255 : 0;
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
int16_t wrapForLongitude(const double longitude) {
|
||||
return static_cast<int16_t>(std::floor((longitude + 180.0) / 360.0));
|
||||
}
|
||||
|
||||
void removeTargets(const std::vector<RenderTargetPtr>& targets, UniqueChangeRequestVec& changes) {
|
||||
for (const auto& target : targets) {
|
||||
if (target) {
|
||||
@@ -78,12 +106,20 @@ public:
|
||||
bool pending = false;
|
||||
bool contextDestroyed = false;
|
||||
bool staticFreezeLogged = false;
|
||||
bool cameraDrawLogged = false;
|
||||
bool worldWrapLogged = false;
|
||||
bool antimeridianLogged = false;
|
||||
uint64_t lastCameraLogFrame = 0;
|
||||
uint64_t maskGeneration = 0;
|
||||
uint8_t loggedMaskMode = 255;
|
||||
Size trailSize{};
|
||||
|
||||
std::array<RenderTargetPtr, 2> stateTargets;
|
||||
std::array<RenderTargetPtr, 2> trailTargets;
|
||||
std::vector<RenderTargetPtr> registeredTargets;
|
||||
gfx::ShaderProgramBasePtr shader;
|
||||
gfx::Texture2DPtr maskTexture;
|
||||
std::shared_ptr<PremultipliedImage> maskFixture;
|
||||
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
|
||||
SegmentVector segments;
|
||||
|
||||
@@ -131,13 +167,19 @@ public:
|
||||
uint8_t readSlot_,
|
||||
uint8_t writeSlot_,
|
||||
gfx::Texture2DPtr source_,
|
||||
gfx::Texture2DPtr target_)
|
||||
gfx::Texture2DPtr target_,
|
||||
gfx::Texture2DPtr mask_,
|
||||
int16_t worldWrap_,
|
||||
uint8_t maskMode_)
|
||||
: state(std::move(state_)),
|
||||
pass(pass_),
|
||||
readSlot(readSlot_),
|
||||
writeSlot(writeSlot_),
|
||||
source(std::move(source_)),
|
||||
target(std::move(target_)) {}
|
||||
target(std::move(target_)),
|
||||
mask(std::move(mask_)),
|
||||
worldWrap(worldWrap_),
|
||||
maskMode(maskMode_) {}
|
||||
|
||||
void init(gfx::Drawable&) override {}
|
||||
|
||||
@@ -148,18 +190,58 @@ public:
|
||||
}
|
||||
|
||||
drawable.setTexture(source, shaders::idParticleGpuValidationPreviousTexture);
|
||||
if (mask) {
|
||||
drawable.setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
|
||||
}
|
||||
|
||||
const bool frozen = pass == ValidationPass::FrozenComposite;
|
||||
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 markerY = 0.5f + 0.32f * std::cos(frame * 0.053f);
|
||||
const shaders::ParticleGpuValidationUBO ubo = {
|
||||
.pass = {static_cast<float>(pass), frame, 0.0f, 0.0f},
|
||||
.pass = {static_cast<float>(pass), frame, maskMode == 0 ? 0.0f : 1.0f, static_cast<float>(maskMode)},
|
||||
.marker = {markerX, markerY, 0.035f, 0.965f},
|
||||
.mask = {static_cast<float>(resources->maskGeneration), static_cast<float>(worldWrap), 0.0f, 0.0f},
|
||||
.matrix = util::cast<float>(parameters.matrixForTile({worldWrap, {0, 0, 0}})),
|
||||
};
|
||||
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
|
||||
|
||||
if (pass == ValidationPass::Composite || pass == ValidationPass::FrozenComposite) {
|
||||
const auto center = parameters.state.getLatLng(LatLng::Unwrapped);
|
||||
const bool primaryWorldCopy = worldWrap == wrapForLongitude(center.longitude());
|
||||
if (primaryWorldCopy && !resources->cameraDrawLogged) {
|
||||
std::ostringstream message;
|
||||
message << "ParticleValidation30_03: first camera-locked draw maskGeneration="
|
||||
<< resources->maskGeneration << " maskMode=" << static_cast<uint32_t>(maskMode)
|
||||
<< " center=" << center.latitude() << ',' << center.longitude() << " worldCopy=" << worldWrap
|
||||
<< " zoom=" << parameters.state.getZoom() << " bearing=" << parameters.state.getBearing()
|
||||
<< " pitch=" << parameters.state.getPitch() << " pixelRatio=" << parameters.pixelRatio;
|
||||
Log::Info(Event::Render, message.str());
|
||||
resources->cameraDrawLogged = true;
|
||||
}
|
||||
if (primaryWorldCopy && parameters.frameCount >= resources->lastCameraLogFrame + 30) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_03: camera frame=" + std::to_string(parameters.frameCount) +
|
||||
" center=" + std::to_string(center.latitude()) + ',' +
|
||||
std::to_string(center.longitude()) +
|
||||
" zoom=" + std::to_string(parameters.state.getZoom()) +
|
||||
" bearing=" + std::to_string(parameters.state.getBearing()) +
|
||||
" pitch=" + std::to_string(parameters.state.getPitch()) +
|
||||
" viewport=" + std::to_string(parameters.state.getSize().width) + 'x' +
|
||||
std::to_string(parameters.state.getSize().height));
|
||||
resources->lastCameraLogFrame = parameters.frameCount;
|
||||
}
|
||||
if (primaryWorldCopy && worldWrap != 0 && !resources->worldWrapLogged) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_03: world wrap draw canonical=0/0/0 wrap=" + std::to_string(worldWrap));
|
||||
resources->worldWrapLogged = true;
|
||||
}
|
||||
if (primaryWorldCopy && std::abs(center.longitude()) >= 170.0 && !resources->antimeridianLogged) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_03: antimeridian camera centerLon=" +
|
||||
std::to_string(center.longitude()) + " canonicalMask=[-180,180]");
|
||||
resources->antimeridianLogged = true;
|
||||
}
|
||||
if (pass == ValidationPass::FrozenComposite) {
|
||||
if (!resources->staticFreezeLogged) {
|
||||
const auto size = parameters.state.getSize();
|
||||
@@ -176,6 +258,13 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
// The same canonical quad is drawn for adjacent MapLibre world
|
||||
// copies. Only the center copy owns the 30-02 slot commit; copies
|
||||
// are draw-only and must never advance simulation a second time.
|
||||
if (!primaryWorldCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto beforeState = resources->stateRead;
|
||||
const auto beforeTrail = resources->trailRead;
|
||||
resources->commit();
|
||||
@@ -218,6 +307,9 @@ private:
|
||||
uint8_t writeSlot;
|
||||
gfx::Texture2DPtr source;
|
||||
gfx::Texture2DPtr target;
|
||||
gfx::Texture2DPtr mask;
|
||||
int16_t worldWrap;
|
||||
uint8_t maskMode;
|
||||
};
|
||||
|
||||
RenderTargetPtr createValidationTarget(gfx::Context& context,
|
||||
@@ -243,10 +335,16 @@ bool buildQuad(gfx::Context& context,
|
||||
const std::string& name,
|
||||
const gfx::Texture2DPtr& source,
|
||||
const gfx::Texture2DPtr& target,
|
||||
const gfx::Texture2DPtr& mask,
|
||||
const ValidationPass pass,
|
||||
const uint8_t readSlot,
|
||||
const uint8_t writeSlot) {
|
||||
group.clearDrawables();
|
||||
const uint8_t writeSlot,
|
||||
const int16_t worldWrap,
|
||||
const uint8_t maskMode,
|
||||
const bool clearGroup) {
|
||||
if (clearGroup) {
|
||||
group.clearDrawables();
|
||||
}
|
||||
auto attributes = context.createVertexAttributeArray();
|
||||
if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute)) {
|
||||
attribute->setSharedRawData(resources->vertices,
|
||||
@@ -271,8 +369,11 @@ bool buildQuad(gfx::Context& context,
|
||||
resources->segments.data(),
|
||||
resources->segments.size());
|
||||
builder->setTexture(source, shaders::idParticleGpuValidationPreviousTexture);
|
||||
builder->addTweaker(
|
||||
std::make_shared<ParticleGpuValidationTweaker>(resources, pass, readSlot, writeSlot, source, target));
|
||||
if (mask) {
|
||||
builder->setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
|
||||
}
|
||||
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(
|
||||
resources, pass, readSlot, writeSlot, source, target, mask, worldWrap, maskMode));
|
||||
|
||||
// DrawableBuilder owns the current drawable until flush finalizes it.
|
||||
builder->flush(context);
|
||||
@@ -288,7 +389,7 @@ bool buildQuad(gfx::Context& context,
|
||||
} // namespace
|
||||
|
||||
RenderParticleValidationLayer::RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl> impl_)
|
||||
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(impl_, impl_->active)) {
|
||||
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(impl_, impl_->active, impl_->maskMode)) {
|
||||
Log::Info(
|
||||
Event::Render,
|
||||
"ParticleValidation: RenderLayer create id=" + getID() + " source-less=true pass=translucent stage=30-02");
|
||||
@@ -305,15 +406,17 @@ RenderParticleValidationLayer::~RenderParticleValidationLayer() {
|
||||
|
||||
void RenderParticleValidationLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
const bool active = impl(baseImpl).active;
|
||||
const uint8_t maskMode = impl(baseImpl).maskMode;
|
||||
auto properties = makeMutable<style::ParticleValidationLayerProperties>(
|
||||
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active);
|
||||
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active, maskMode);
|
||||
passes = RenderPass::Translucent;
|
||||
properties->renderPasses = mbgl::underlying_type(passes);
|
||||
evaluatedProperties = std::move(properties);
|
||||
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") + " id=" + getID() +
|
||||
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=30-02");
|
||||
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") +
|
||||
" maskMode=" + std::to_string(maskMode) + " id=" + getID() +
|
||||
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=30-03");
|
||||
}
|
||||
|
||||
bool RenderParticleValidationLayer::needsRepaint() const {
|
||||
@@ -365,8 +468,16 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
||||
return;
|
||||
}
|
||||
|
||||
const bool active = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties).active;
|
||||
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
|
||||
const bool active = properties.active;
|
||||
const uint8_t maskMode = properties.maskMode;
|
||||
auto resources = gpuValidation;
|
||||
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();
|
||||
resources.reset();
|
||||
}
|
||||
if (!resources) {
|
||||
if (!active) {
|
||||
// STATIC without a preceding ACTIVE frame has no trail to show. Do not create,
|
||||
@@ -389,11 +500,37 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
||||
Log::Error(Event::Render, "ParticleValidation30_02: State resource creation failed");
|
||||
return;
|
||||
}
|
||||
owned->maskFixture = makeCoastalMaskFixture();
|
||||
owned->maskTexture = context.createTexture2D();
|
||||
if (!owned->maskFixture || !owned->maskTexture) {
|
||||
Log::Error(Event::Render, "ParticleValidation30_03: mask fixture/resource creation failed");
|
||||
return;
|
||||
}
|
||||
owned->maskTexture->setImage(owned->maskFixture);
|
||||
owned->maskTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
owned->maskGeneration = ++nextMaskGeneration;
|
||||
gpuValidation = std::move(owned);
|
||||
resources = gpuValidation;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) +
|
||||
" size=16x16 independent=true");
|
||||
Log::Info(
|
||||
Event::Render,
|
||||
"ParticleValidation30_03: mask fixture ready id=coastal-mask-validation-v1 bounds=[-180,180]x[-85,85] "
|
||||
"encoding=uint8-land-ocean land=0 ocean=255 maskGeneration=" +
|
||||
std::to_string(resources->maskGeneration));
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0");
|
||||
}
|
||||
|
||||
if (resources->loggedMaskMode != maskMode) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleValidation30_03: mask mode=" + std::to_string(maskMode) + " consumer=" +
|
||||
(maskMode == 0 ? std::string("GLOBAL") : (maskMode == 1 ? "OCEAN_ONLY_A" : "OCEAN_ONLY_B")) +
|
||||
" maskGeneration=" + std::to_string(resources->maskGeneration) + " reuse=true");
|
||||
resources->loggedMaskMode = maskMode;
|
||||
}
|
||||
|
||||
removeTargets(resources->registeredTargets, changes);
|
||||
@@ -406,15 +543,24 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
||||
resources->pending = false;
|
||||
auto& compositeGroup = static_cast<LayerGroup&>(*layerGroup);
|
||||
if (resources->trailSeeded && resources->trailTargets[resources->trailRead]) {
|
||||
buildQuad(context,
|
||||
resources,
|
||||
compositeGroup,
|
||||
getID() + "-frozen-composite-pass",
|
||||
resources->trailTargets[resources->trailRead]->getTexture(),
|
||||
{},
|
||||
ValidationPass::FrozenComposite,
|
||||
resources->trailRead,
|
||||
resources->trailRead);
|
||||
compositeGroup.clearDrawables();
|
||||
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) {
|
||||
buildQuad(context,
|
||||
resources,
|
||||
compositeGroup,
|
||||
getID() + "-frozen-composite-pass-" + std::to_string(wrap),
|
||||
resources->trailTargets[resources->trailRead]->getTexture(),
|
||||
{},
|
||||
resources->maskTexture,
|
||||
ValidationPass::FrozenComposite,
|
||||
resources->trailRead,
|
||||
resources->trailRead,
|
||||
wrap,
|
||||
maskMode,
|
||||
false);
|
||||
}
|
||||
} else {
|
||||
compositeGroup.clearDrawables();
|
||||
}
|
||||
@@ -457,27 +603,46 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
|
||||
getID() + "-state-pass",
|
||||
resources->stateTargets[resources->stateRead]->getTexture(),
|
||||
resources->stateTargets[stateWrite]->getTexture(),
|
||||
resources->maskTexture,
|
||||
ValidationPass::State,
|
||||
resources->stateRead,
|
||||
stateWrite);
|
||||
stateWrite,
|
||||
0,
|
||||
maskMode,
|
||||
true);
|
||||
const bool trailBuilt = buildQuad(context,
|
||||
resources,
|
||||
trailGroup,
|
||||
getID() + "-trail-pass",
|
||||
resources->trailTargets[resources->trailRead]->getTexture(),
|
||||
resources->trailTargets[trailWrite]->getTexture(),
|
||||
resources->maskTexture,
|
||||
ValidationPass::Trail,
|
||||
resources->trailRead,
|
||||
trailWrite);
|
||||
const bool compositeBuilt = buildQuad(context,
|
||||
resources,
|
||||
compositeGroup,
|
||||
getID() + "-composite-pass",
|
||||
resources->trailTargets[trailWrite]->getTexture(),
|
||||
{},
|
||||
ValidationPass::Composite,
|
||||
resources->trailRead,
|
||||
trailWrite);
|
||||
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() + "-composite-pass-" + std::to_string(wrap),
|
||||
resources->trailTargets[trailWrite]->getTexture(),
|
||||
{},
|
||||
resources->maskTexture,
|
||||
ValidationPass::Composite,
|
||||
resources->trailRead,
|
||||
trailWrite,
|
||||
wrap,
|
||||
maskMode,
|
||||
false) &&
|
||||
compositeBuilt;
|
||||
}
|
||||
if (!stateBuilt || !trailBuilt || !compositeBuilt) {
|
||||
compositeGroup.clearDrawables();
|
||||
return;
|
||||
|
||||
@@ -298,6 +298,7 @@ const std::vector<AttributeInfo> ParticleGpuValidationShaderInfo::attributes = {
|
||||
};
|
||||
const std::vector<TextureInfo> ParticleGpuValidationShaderInfo::textures = {
|
||||
TextureInfo{"u_previous", idParticleGpuValidationPreviousTexture},
|
||||
TextureInfo{"u_mask_texture", idParticleGpuValidationMaskTexture},
|
||||
};
|
||||
|
||||
// Hillshade Prepare
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace mbgl {
|
||||
namespace style {
|
||||
|
||||
namespace {
|
||||
constexpr const char* ActiveProperty = "particle-validation-active";
|
||||
constexpr const char* MaskModeProperty = "particle-validation-mask-mode";
|
||||
|
||||
const LayerTypeInfo typeInfo{.type = "particle-validation",
|
||||
.source = LayerTypeInfo::Source::NotRequired,
|
||||
@@ -59,6 +62,10 @@ bool ParticleValidationLayer::isActive() const {
|
||||
return impl().active;
|
||||
}
|
||||
|
||||
uint8_t ParticleValidationLayer::getMaskMode() const {
|
||||
return impl().maskMode;
|
||||
}
|
||||
|
||||
void ParticleValidationLayer::setActive(bool active) {
|
||||
if (active == isActive()) {
|
||||
return;
|
||||
@@ -69,8 +76,27 @@ void ParticleValidationLayer::setActive(bool active) {
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
|
||||
void ParticleValidationLayer::setMaskMode(uint8_t mode) {
|
||||
if (mode > 2 || mode == getMaskMode()) {
|
||||
return;
|
||||
}
|
||||
auto impl_ = mutableImpl();
|
||||
impl_->maskMode = mode;
|
||||
baseImpl = std::move(impl_);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
|
||||
std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(const std::string& name,
|
||||
const conversion::Convertible& value) {
|
||||
if (name == MaskModeProperty) {
|
||||
conversion::Error error;
|
||||
auto converted = conversion::convert<float>(value, error);
|
||||
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted) {
|
||||
return conversion::Error{"particle-validation-mask-mode must be 0, 1, or 2"};
|
||||
}
|
||||
setMaskMode(static_cast<uint8_t>(*converted));
|
||||
return std::nullopt;
|
||||
}
|
||||
if (name != ActiveProperty) {
|
||||
return conversion::Error{"layer doesn't support this property"};
|
||||
}
|
||||
@@ -87,12 +113,16 @@ StyleProperty ParticleValidationLayer::getProperty(const std::string& name) cons
|
||||
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 ParticleValidationLayer::serialize() const {
|
||||
auto result = Layer::serialize();
|
||||
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
|
||||
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,19 +17,23 @@ public:
|
||||
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override;
|
||||
|
||||
bool active = true;
|
||||
// VALIDATION ONLY: 0=GLOBAL, 1=OCEAN_ONLY_A, 2=OCEAN_ONLY_B.
|
||||
uint8_t maskMode = 0;
|
||||
|
||||
DECLARE_LAYER_TYPE_INFO;
|
||||
};
|
||||
|
||||
class ParticleValidationLayerProperties final : public LayerProperties {
|
||||
public:
|
||||
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl, bool active_)
|
||||
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl, bool active_, uint8_t maskMode_)
|
||||
: LayerProperties(std::move(impl)),
|
||||
active(active_) {}
|
||||
active(active_),
|
||||
maskMode(maskMode_) {}
|
||||
|
||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||
|
||||
bool active;
|
||||
uint8_t maskMode;
|
||||
};
|
||||
|
||||
} // namespace style
|
||||
|
||||
13
粒子开发记录.md
13
粒子开发记录.md
@@ -127,3 +127,16 @@
|
||||
- 本次只回填报告与开发记录;未修改代码,未重新编译,未运行 Gradle、Android 或 ADB。
|
||||
- 技术报告:`docs/60_测试/30_02_GPU_PingPong与ContextRecreation最小技术验证报告_v5.md`。
|
||||
- Codex 完成报告:`docs/报告/30_02_GPU_PingPong与ContextRecreation验证_Codex完成报告_v5.md`。
|
||||
|
||||
## 2026-08-30 — 30-03 Camera / World Wrap 与 Land/Ocean Mask(阶段 A)
|
||||
|
||||
- 30-02 已正式 PASS,EGL Context recreation 作为用户接受的未验证风险不阻塞,故开始 30-03。
|
||||
- validation composite 改为使用当前 `PaintParameters::matrixForTile()` 的 canonical z0 world quad;ACTIVE/STATIC 继续区分 simulation update 与 Camera redraw。
|
||||
- 通过 `UnwrappedTileID.wrap` 绘制中心和相邻 MapLibre world copy;仅中心 copy commit,避免 world copy 额外推进 30-02 GPU simulation。
|
||||
- 增加反经线两侧 deterministic anchors C/D,并以全球 canonical mask UV 复用 MapLibre wrap,不建立第二套 world 状态机。
|
||||
- 增加 validation-only shared 16×16 synthetic coastal mask,GLOBAL、OCEAN_ONLY_A/B 复用同一 GPU texture;Camera/consumer 切换不重建,context-destroy 通知后按新 generation 重建。
|
||||
- Android Test App 复用既有入口,增加 GLOBAL/OCEAN A/OCEAN B/DATELINE/WORLD WRAP 最小控制;未运行 Android 编译、Gradle 或 ADB。
|
||||
- Linux OpenGL 受影响 Core 对象精确编译、C++ 格式与 diff 检查通过;用户 T1–T13 实机验证仍待执行。
|
||||
- 当前状态:`WAITING_MANUAL_VALIDATION`;不允许进入 iOS 最小验证。
|
||||
- 技术报告:`docs/60_测试/30_03_Camera_WorldWrap与LandOceanMask最小技术验证报告_v1.md`。
|
||||
- Codex 完成报告:`docs/报告/30_03_Camera_WorldWrap与LandOceanMask验证_Codex完成报告_v1.md`。
|
||||
|
||||
Reference in New Issue
Block a user