test: validate particle camera wrap and ocean mask

This commit is contained in:
OpenAI Codex
2026-08-30 10:27:24 +08:00
parent 3b5a147bba
commit 8b4c5c6d71
16 changed files with 572 additions and 55 deletions

View File

@@ -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 最小验证。

View File

@@ -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。

View File

@@ -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

View File

@@ -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

View File

@@ -340,6 +340,7 @@ enum {
enum {
idParticleGpuValidationPreviousTexture,
idParticleGpuValidationMaskTexture,
particleGpuValidationTextureCount
};

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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`。