fix: audit STEP-057 weather runtime facade
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:
@@ -14,8 +14,8 @@
|
|||||||
- `setWeatherColorVisible` / `setParticleVisible`:仅 visibility,`hide != unload`。
|
- `setWeatherColorVisible` / `setParticleVisible`:仅 visibility,`hide != unload`。
|
||||||
- `setForecastTime(timestampMs)`:Core 按 timestamp 选相邻 T0/T1 并计算 MIX。恰好命中末快照为 MIX=1;中间线性插值;超范围、未 READY、Gap/Invalid 返回 `REJECTED`,不会沿用错误旧时间。正常 seek 不 submit field、不 reset particle、不 clear trail;仅跨窗口时沿用既有 OLD_T1→NEW_T0 reuse。
|
- `setForecastTime(timestampMs)`:Core 按 timestamp 选相邻 T0/T1 并计算 MIX。恰好命中末快照为 MIX=1;中间线性插值;超范围、未 READY、Gap/Invalid 返回 `REJECTED`,不会沿用错误旧时间。正常 seek 不 submit field、不 reset particle、不 clear trail;仅跨窗口时沿用既有 OLD_T1→NEW_T0 reuse。
|
||||||
- `setPlaybackState(PLAY/PAUSE)`、`setPlaybackSpeed(HALF/NORMAL/DOUBLE)`:复用 Source Timeline,不创建 Android Timer。
|
- `setPlaybackState(PLAY/PAUSE)`、`setPlaybackSpeed(HALF/NORMAL/DOUBLE)`:复用 Source Timeline,不创建 Android Timer。
|
||||||
- `setWeatherColorOpacity`、`setParticleVisual`:低频 visual 更新。
|
- `setWeatherColorOpacity`、`setParticleCount`、`setParticleLineWidth`、`setParticleLength`、`setParticleOpacity`、`setTrailOpacity`、`setTrailFade`、`setParticleSpeedColorEnabled`:明确 typed 的低频 visual 更新;正式 Host API 不接收 property string。
|
||||||
- `setWeatherColorStops(List<ColorStop>)`:2 至 8 个 stop,speed finite 严格递增,RGBA 均为有限 [0,1]。低于/高于边界分别钳到首/末颜色,中间线性插值。无效输入 reject,并保留上一套有效 ramp。
|
- `setWeatherColorStops(List<ColorStop>)`:2 至 Core `MaxWeatherColorStops` 个 stop(v1 为 8,Java 经 JNI `getWeatherColorStopCapacity()` 获取),speed finite 严格递增,RGBA 均为有限 [0,1]。低于/高于边界分别钳到首/末颜色,中间线性插值。无效输入 reject,并保留上一套有效 ramp。
|
||||||
- `setParticleColors(...)`:独立 particle/trail RGBA;不依赖 WeatherColor ramp。legacy `speed-color-enabled=true` 时仅当前 segment 可使用旧 speed 色,不改变 WeatherColor stops。
|
- `setParticleColors(...)`:独立 particle/trail RGBA;不依赖 WeatherColor ramp。legacy `speed-color-enabled=true` 时仅当前 segment 可使用旧 speed 色,不改变 WeatherColor stops。
|
||||||
|
|
||||||
状态为 `DISABLED`、`WAITING_FOR_FIELDS`、`READY`、`ERROR`,仅由低频命令和 submit result 更新。v1 提供同步 typed result/state query;没有 listener,未来 RN 薄层可在这些调用完成后读取结果。
|
状态为 `DISABLED`、`WAITING_FOR_FIELDS`、`READY`、`ERROR`,仅由低频命令和 submit result 更新。v1 提供同步 typed result/state query;没有 listener,未来 RN 薄层可在这些调用完成后读取结果。
|
||||||
|
|||||||
@@ -13,3 +13,15 @@ OFF 会停止 Timeline、clear Host ownership,并在 Render 线程的空 Sourc
|
|||||||
本地验证:shader generator、Linux OpenGL `mbgl-core`、`style_layer.test.cpp` 与 `source.test.cpp` 精确编译;`git diff --check`。Android Gradle/APK/ADB/设备测试和 Jest 未运行。
|
本地验证:shader generator、Linux OpenGL `mbgl-core`、`style_layer.test.cpp` 与 `source.test.cpp` 精确编译;`git diff --check`。Android Gradle/APK/ADB/设备测试和 Jest 未运行。
|
||||||
|
|
||||||
未完成:Android 手工 T1–T13 验收,特别是 OFF resource log、ON 后重新 T0/T1、dynamic ramp/colour 无 field upload/particle reset、timestamp mid-point 和窗口边界回归。
|
未完成:Android 手工 T1–T13 验收,特别是 OFF resource log、ON 后重新 T0/T1、dynamic ramp/colour 无 field upload/particle reset、timestamp mid-point 和窗口边界回归。
|
||||||
|
|
||||||
|
## STEP-057A 审计修正(2026-09-07)
|
||||||
|
|
||||||
|
状态保持 `IMPLEMENTED / WAITING_ANDROID_VALIDATION`。基于已推送 `efaa3e61c` 审计修正,不进入 STEP-058。
|
||||||
|
|
||||||
|
- 删除正式 `WeatherRuntime.setParticleVisual(String, float)`;改为 particle count、line width、length、particle/trail opacity、trail fade、speed-color enabled 的明确 typed 方法。Test App 的 fixture-only generic property 调试仍只留在低层路径。
|
||||||
|
- 修正 facade 状态:READY 重复 enable 幂等;DISABLED enable 进入 WAITING;ERROR 收到 pending T0 恢复 WAITING;initial window ready 进入 READY;clear/disable 只在对应 Core 成功时转入 WAITING/DISABLED。新增 Android 既有 JUnit state transition 定向测试源码,未按本轮限制执行 Gradle。
|
||||||
|
- `RenderOrchestrator` 不再对所有 hidden RasterArray layer 执行额外 prepare/update。`RenderLayer` 默认无 cleanup;仅仍持有资源、Host-field Source 已空的 RasterParticle/WeatherColor renderer 显式 opt-in,完成一次空场 GPU cleanup。普通 hidden layer 语义不变。
|
||||||
|
- Host stop capacity 改为由 JNI 读取 Core `MaxWeatherColorStops`;Host API 继续接受 `ColorStop` list,未暴露 UBO。严格验证与 particle/trail 独立性保持。
|
||||||
|
- timestamp 测试改为 epoch milliseconds(2024-01-01 UTC 起的 64-bit ms),覆盖 exact T0、midpoint、exact T1、上下界 out-of-range 与 clear 后 fail-closed。
|
||||||
|
|
||||||
|
验证:output-throttle 下 shader generator、Linux OpenGL `mbgl-core`、`mbgl-render`、`mbgl-test-runner`、相关精确 Source/Style compile 和定向非 Jest tests 均 PASS;`git diff --check` PASS。Android Gradle/APK/ADB/设备/Jest 未运行。
|
||||||
|
|||||||
34
docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v2.md
Normal file
34
docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v2.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# STEP-057A Android Host 天气运行时接口审计修正 — Codex 完成报告
|
||||||
|
|
||||||
|
状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。本轮仅修正 STEP-057,不进入 STEP-058、React Native bridge 或性能任务。
|
||||||
|
|
||||||
|
## 实际修正
|
||||||
|
|
||||||
|
1. `WeatherRuntime` 删除公开 `setParticleVisual(String, float)`。正式 API 改为 `setParticleCount`、`setParticleLineWidth`、`setParticleLength`、`setParticleOpacity`、`setTrailOpacity`、`setTrailFade`、`setParticleSpeedColorEnabled`;Core property 语义未改。Test App 的未映射 fixture diagnostic 才可使用低层 generic setter。
|
||||||
|
2. 状态转换被收敛为可单测函数:READY enable 幂等;DISABLED 成功 enable → WAITING;ERROR 成功 pending field → WAITING;initial window ready/next snapshot → READY;成功 clear → WAITING;成功 disable → DISABLED。新增 `WeatherRuntimeStateTest`,使用现有 Android JUnit 体系;遵从任务限制未运行 Gradle。
|
||||||
|
3. 取消 `RenderOrchestrator` 的所有 hidden RasterArray 层全局 prepare/update。新增默认关闭的 `RenderLayer::needsHiddenLayerSourceCleanup`;只有持有 GPU resource 且 Host-field source 已清空的 RasterParticle/WeatherColor layers opt-in,才在 hidden 时接收一轮 cleanup。普通 hidden MapLibre layer 生命周期不变。
|
||||||
|
4. stop 上限继续由 C++ `MaxWeatherColorStops=8` 定义。Java 经 JNI 获取 capacity,facade 以 `ColorStop` list 验证,不暴露 UBO 数组。严格 speed/RGBA 校验和独立 particle/trail color 保持。
|
||||||
|
5. Source timestamp tests 现在使用 epoch milliseconds:T0=`1704067200000`、T1=`1704070800000`,验证 exact T0、midpoint、exact T1、lower/upper out-of-range 与 clear 后 fail-closed。
|
||||||
|
|
||||||
|
既有 clear/unload、JNI、Core-owned T0/T1/MIX、dynamic ramp、独立颜色、field/window reuse、无 style reload 均保留。正常 window boundary 的 OLD_T1→NEW_T0 / only NEW_T1 upload 路径未改;本轮没有触及 simulation/trail reset 条件。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
所有编译、codegen 和测试均经 `build-output-throttle` wrapper,日志仅存 `/tmp/step057a-logs/`,未提交。
|
||||||
|
|
||||||
|
- shader generator:PASS。
|
||||||
|
- Linux OpenGL `mbgl-core`:PASS。
|
||||||
|
- Linux OpenGL `mbgl-render`:PASS。
|
||||||
|
- Linux OpenGL `mbgl-test-runner`:PASS。
|
||||||
|
- `style_layer.test.cpp` / `source.test.cpp` 精确编译:PASS。
|
||||||
|
- 定向非 Jest tests(timestamp seek、clear、WeatherColor stop validation、particle/trail properties):PASS。
|
||||||
|
- `git diff --check`:PASS。
|
||||||
|
|
||||||
|
未执行:Android Gradle、APK build、ADB、Android device validation、Jest。
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- 审计起点:`efaa3e61c94e056bf06a955eb250cdfdc1633ecd`
|
||||||
|
- 功能提交、文档提交、push 和 Final HEAD:在完成提交后以最终交付结果为准。
|
||||||
|
|
||||||
|
Android 后续人工验收仍需重点检查 OFF hidden cleanup logs、READY 重复 ON、ERROR→pending→WAITING、dynamic stop/colour 无 field upload/reset、epoch-ms seek 与正常 window boundary `simulationRebuild=false` / `trailCleared=false`。
|
||||||
@@ -1214,6 +1214,10 @@ jni::jboolean NativeMapView::setWeatherColorStops(JNIEnv& env,
|
|||||||
return layer->setColorStops(std::move(stops));
|
return layer->setColorStops(std::move(stops));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jni::jint NativeMapView::getWeatherColorStopCapacity(JNIEnv&) {
|
||||||
|
return static_cast<jni::jint>(mbgl::style::MaxWeatherColorStops);
|
||||||
|
}
|
||||||
|
|
||||||
jni::jboolean NativeMapView::advanceRasterArraySourceFieldWindow(JNIEnv& env, const jni::String& sourceId) {
|
jni::jboolean NativeMapView::advanceRasterArraySourceFieldWindow(JNIEnv& env, const jni::String& sourceId) {
|
||||||
const auto id = jni::Make<std::string>(env, sourceId);
|
const auto id = jni::Make<std::string>(env, sourceId);
|
||||||
auto* source = map->getStyle().getSource(id);
|
auto* source = map->getStyle().getSource(id);
|
||||||
@@ -1692,6 +1696,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
|
|||||||
METHOD(&NativeMapView::setStyleLayerVisibility, "nativeSetStyleLayerVisibility"),
|
METHOD(&NativeMapView::setStyleLayerVisibility, "nativeSetStyleLayerVisibility"),
|
||||||
METHOD(&NativeMapView::setRasterParticleColors, "nativeSetRasterParticleColors"),
|
METHOD(&NativeMapView::setRasterParticleColors, "nativeSetRasterParticleColors"),
|
||||||
METHOD(&NativeMapView::setWeatherColorStops, "nativeSetWeatherColorStops"),
|
METHOD(&NativeMapView::setWeatherColorStops, "nativeSetWeatherColorStops"),
|
||||||
|
METHOD(&NativeMapView::getWeatherColorStopCapacity, "nativeGetWeatherColorStopCapacity"),
|
||||||
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),
|
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),
|
||||||
METHOD(&NativeMapView::repeatRasterArraySourceFieldWindow, "nativeRepeatRasterArraySourceFieldWindow"),
|
METHOD(&NativeMapView::repeatRasterArraySourceFieldWindow, "nativeRepeatRasterArraySourceFieldWindow"),
|
||||||
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
|
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ public:
|
|||||||
jni::jboolean setStyleLayerVisibility(JNIEnv&, const jni::String&, jni::jboolean);
|
jni::jboolean setStyleLayerVisibility(JNIEnv&, const jni::String&, jni::jboolean);
|
||||||
jni::jboolean setRasterParticleColors(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
|
jni::jboolean setRasterParticleColors(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
|
||||||
jni::jboolean setWeatherColorStops(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
|
jni::jboolean setWeatherColorStops(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
|
||||||
|
jni::jint getWeatherColorStopCapacity(JNIEnv&);
|
||||||
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
||||||
jni::jboolean repeatRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
jni::jboolean repeatRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
||||||
jni::jint submitRasterArraySourceHostField(JNIEnv&,
|
jni::jint submitRasterArraySourceHostField(JNIEnv&,
|
||||||
|
|||||||
@@ -306,6 +306,11 @@ public final class MapLibreMap {
|
|||||||
return nativeMapView.setWeatherColorStops(layerId, speedsMps, rgbaStops);
|
return nativeMapView.setWeatherColorStops(layerId, speedsMps, rgbaStops);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the Core-owned WeatherColor stop capacity for typed Host validation. */
|
||||||
|
public int getWeatherColorStopCapacity() {
|
||||||
|
return nativeMapView.getWeatherColorStopCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
/** Advances a Source-owned raster-array runtime Field window without reloading the Style. */
|
/** Advances a Source-owned raster-array runtime Field window without reloading the Style. */
|
||||||
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
|
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
|
||||||
return nativeMapView.advanceRasterArraySourceFieldWindow(sourceId);
|
return nativeMapView.advanceRasterArraySourceFieldWindow(sourceId);
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ interface NativeMap {
|
|||||||
|
|
||||||
boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
|
boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
|
||||||
|
|
||||||
|
int getWeatherColorStopCapacity();
|
||||||
|
|
||||||
boolean advanceRasterArraySourceFieldWindow(String sourceId);
|
boolean advanceRasterArraySourceFieldWindow(String sourceId);
|
||||||
|
|
||||||
boolean repeatRasterArraySourceFieldWindow(String sourceId);
|
boolean repeatRasterArraySourceFieldWindow(String sourceId);
|
||||||
|
|||||||
@@ -968,6 +968,10 @@ final class NativeMapView implements NativeMap {
|
|||||||
return !checkState("setWeatherColorStops") && nativeSetWeatherColorStops(layerId, speedsMps, rgbaStops);
|
return !checkState("setWeatherColorStops") && nativeSetWeatherColorStops(layerId, speedsMps, rgbaStops);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getWeatherColorStopCapacity() {
|
||||||
|
return checkState("getWeatherColorStopCapacity") ? 0 : nativeGetWeatherColorStopCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
|
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
|
||||||
return !checkState("advanceRasterArraySourceFieldWindow")
|
return !checkState("advanceRasterArraySourceFieldWindow")
|
||||||
@@ -1742,6 +1746,7 @@ final class NativeMapView implements NativeMap {
|
|||||||
@Keep private native boolean nativeSetStyleLayerVisibility(String layerId, boolean visible);
|
@Keep private native boolean nativeSetStyleLayerVisibility(String layerId, boolean visible);
|
||||||
@Keep private native boolean nativeSetRasterParticleColors(String layerId, float[] particleColor, float[] trailColor);
|
@Keep private native boolean nativeSetRasterParticleColors(String layerId, float[] particleColor, float[] trailColor);
|
||||||
@Keep private native boolean nativeSetWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
|
@Keep private native boolean nativeSetWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
|
||||||
|
@Keep private native int nativeGetWeatherColorStopCapacity();
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
private native boolean nativeAdvanceRasterArraySourceFieldWindow(String sourceId);
|
private native boolean nativeAdvanceRasterArraySourceFieldWindow(String sourceId);
|
||||||
|
|||||||
@@ -40,35 +40,41 @@ public final class WeatherRuntime {
|
|||||||
|
|
||||||
private final MapLibreMap map;
|
private final MapLibreMap map;
|
||||||
private final Config config;
|
private final Config config;
|
||||||
|
private final int weatherColorStopCapacity;
|
||||||
private State state = State.WAITING_FOR_FIELDS;
|
private State state = State.WAITING_FOR_FIELDS;
|
||||||
private boolean weatherColorVisible = true;
|
private boolean weatherColorVisible = true;
|
||||||
private boolean particleVisible = true;
|
private boolean particleVisible = true;
|
||||||
|
|
||||||
public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) { this.map = map; this.config = config; }
|
public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) {
|
||||||
|
this.map = map; this.config = config; this.weatherColorStopCapacity = map.getWeatherColorStopCapacity();
|
||||||
|
}
|
||||||
public State getState() { return state; }
|
public State getState() { return state; }
|
||||||
public Config getConfig() { return config; }
|
public Config getConfig() { return config; }
|
||||||
|
/** Core-owned capacity; Host never sees the shader's UBO layout. */
|
||||||
|
public int getWeatherColorStopCapacity() { return weatherColorStopCapacity; }
|
||||||
|
|
||||||
public FieldSubmitResult submitWindField(@NonNull String metadataJson, @NonNull byte[] payloadBytes) {
|
public FieldSubmitResult submitWindField(@NonNull String metadataJson, @NonNull byte[] payloadBytes) {
|
||||||
requireEnabled();
|
requireEnabled();
|
||||||
if (metadataJson.isEmpty() || payloadBytes.length == 0) { state = State.ERROR; return FieldSubmitResult.REJECTED_ADAPTER; }
|
if (metadataJson.isEmpty() || payloadBytes.length == 0) { state = State.ERROR; return FieldSubmitResult.REJECTED_ADAPTER; }
|
||||||
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes));
|
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes));
|
||||||
if (result == FieldSubmitResult.INITIAL_WINDOW_READY || result == FieldSubmitResult.ACCEPTED_NEXT_SNAPSHOT || result == FieldSubmitResult.DUPLICATE) state = State.READY;
|
state = stateAfterSubmit(state, result);
|
||||||
else if (!result.accepted()) state = State.ERROR;
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
public CommandResult clearWindFields() { if (state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); if (ok) state = State.WAITING_FOR_FIELDS; return result(ok); }
|
public CommandResult clearWindFields() { if (state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterClear(state, ok); return result(ok); }
|
||||||
public CommandResult setWeatherEnabled(boolean enabled) {
|
public CommandResult setWeatherEnabled(boolean enabled) {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
if (state == State.DISABLED) return CommandResult.ACCEPTED;
|
||||||
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
|
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
|
||||||
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, false);
|
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, false);
|
||||||
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
|
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
|
||||||
boolean clear = map.clearRasterArraySourceHostFields(config.sourceId);
|
boolean clear = map.clearRasterArraySourceHostFields(config.sourceId);
|
||||||
state = (color && particle && clear) ? State.DISABLED : State.ERROR;
|
state = stateAfterDisable(color && particle && clear);
|
||||||
return result(color && particle && clear);
|
return result(color && particle && clear);
|
||||||
}
|
}
|
||||||
|
if (state != State.DISABLED) return CommandResult.ACCEPTED;
|
||||||
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible);
|
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible);
|
||||||
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible);
|
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible);
|
||||||
if (color && particle) state = State.WAITING_FOR_FIELDS; else state = State.ERROR;
|
state = stateAfterEnable(state, color && particle);
|
||||||
return result(color && particle);
|
return result(color && particle);
|
||||||
}
|
}
|
||||||
public CommandResult setWeatherColorVisible(boolean visible) { weatherColorVisible = visible; return result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); }
|
public CommandResult setWeatherColorVisible(boolean visible) { weatherColorVisible = visible; return result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); }
|
||||||
@@ -77,16 +83,35 @@ public final class WeatherRuntime {
|
|||||||
public CommandResult setPlaybackState(@NonNull PlaybackState playback) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); }
|
public CommandResult setPlaybackState(@NonNull PlaybackState playback) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); }
|
||||||
public CommandResult setPlaybackSpeed(@NonNull PlaybackSpeed speed) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, 2, speed.value)); }
|
public CommandResult setPlaybackSpeed(@NonNull PlaybackSpeed speed) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, 2, speed.value)); }
|
||||||
public CommandResult setWeatherColorOpacity(float opacity) { return result(Float.isFinite(opacity) && map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-opacity", opacity)); }
|
public CommandResult setWeatherColorOpacity(float opacity) { return result(Float.isFinite(opacity) && map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-opacity", opacity)); }
|
||||||
public CommandResult setParticleVisual(String property, float value) { return result(Float.isFinite(value) && map.setStyleLayerFloatProperty(config.particleLayerId, property, value)); }
|
public CommandResult setParticleCount(int count) { return setParticleFloat("raster-particle-particle-count", count); }
|
||||||
|
public CommandResult setParticleLineWidth(float width) { return setParticleFloat("raster-particle-line-width", width); }
|
||||||
|
public CommandResult setParticleLength(float length) { return setParticleFloat("raster-particle-particle-length", length); }
|
||||||
|
public CommandResult setParticleOpacity(float opacity) { return setParticleFloat("raster-particle-particle-opacity", opacity); }
|
||||||
|
public CommandResult setTrailOpacity(float opacity) { return setParticleFloat("raster-particle-trail-opacity", opacity); }
|
||||||
|
public CommandResult setTrailFade(float fade) { return setParticleFloat("raster-particle-trail-fade", fade); }
|
||||||
|
public CommandResult setParticleSpeedColorEnabled(boolean enabled) { return setParticleFloat("raster-particle-speed-color-enabled", enabled ? 1f : 0f); }
|
||||||
public CommandResult setParticleColors(float red, float green, float blue, float alpha, float trailRed, float trailGreen, float trailBlue, float trailAlpha) {
|
public CommandResult setParticleColors(float red, float green, float blue, float alpha, float trailRed, float trailGreen, float trailBlue, float trailAlpha) {
|
||||||
return result(map.setRasterParticleColors(config.particleLayerId, new float[]{red, green, blue, alpha}, new float[]{trailRed, trailGreen, trailBlue, trailAlpha}));
|
return result(map.setRasterParticleColors(config.particleLayerId, new float[]{red, green, blue, alpha}, new float[]{trailRed, trailGreen, trailBlue, trailAlpha}));
|
||||||
}
|
}
|
||||||
public CommandResult setWeatherColorStops(@NonNull List<ColorStop> stops) {
|
public CommandResult setWeatherColorStops(@NonNull List<ColorStop> stops) {
|
||||||
if (stops.size() < 2 || stops.size() > 8) return CommandResult.REJECTED;
|
if (weatherColorStopCapacity < 2 || stops.size() < 2 || stops.size() > weatherColorStopCapacity) return CommandResult.REJECTED;
|
||||||
float[] speeds = new float[stops.size()]; float[] colors = new float[stops.size() * 4];
|
float[] speeds = new float[stops.size()]; float[] colors = new float[stops.size() * 4];
|
||||||
for (int i = 0; i < stops.size(); i++) { ColorStop stop = stops.get(i); speeds[i] = stop.speedMps; colors[i * 4] = stop.red; colors[i * 4 + 1] = stop.green; colors[i * 4 + 2] = stop.blue; colors[i * 4 + 3] = stop.alpha; }
|
for (int i = 0; i < stops.size(); i++) { ColorStop stop = stops.get(i); speeds[i] = stop.speedMps; colors[i * 4] = stop.red; colors[i * 4 + 1] = stop.green; colors[i * 4 + 2] = stop.blue; colors[i * 4 + 3] = stop.alpha; }
|
||||||
return result(map.setWeatherColorStops(config.weatherColorLayerId, speeds, colors));
|
return result(map.setWeatherColorStops(config.weatherColorLayerId, speeds, colors));
|
||||||
}
|
}
|
||||||
private static CommandResult result(boolean accepted) { return accepted ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
|
private static CommandResult result(boolean accepted) { return accepted ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
|
||||||
|
private CommandResult setParticleFloat(String property, float value) { return result(Float.isFinite(value) && map.setStyleLayerFloatProperty(config.particleLayerId, property, value)); }
|
||||||
|
static State stateAfterSubmit(State current, FieldSubmitResult submit) {
|
||||||
|
if (submit == FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW) return State.WAITING_FOR_FIELDS;
|
||||||
|
if (submit == FieldSubmitResult.INITIAL_WINDOW_READY || submit == FieldSubmitResult.ACCEPTED_NEXT_SNAPSHOT) return State.READY;
|
||||||
|
if (submit == FieldSubmitResult.DUPLICATE) return current;
|
||||||
|
return State.ERROR;
|
||||||
|
}
|
||||||
|
static State stateAfterClear(State current, boolean accepted) { return accepted ? State.WAITING_FOR_FIELDS : current; }
|
||||||
|
static State stateAfterDisable(boolean accepted) { return accepted ? State.DISABLED : State.ERROR; }
|
||||||
|
static State stateAfterEnable(State current, boolean accepted) {
|
||||||
|
if (!accepted) return State.ERROR;
|
||||||
|
return current == State.DISABLED ? State.WAITING_FOR_FIELDS : current;
|
||||||
|
}
|
||||||
private void requireEnabled() { if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); }
|
private void requireEnabled() { if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.maplibre.android.maps;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
/** Unit coverage for facade-only state transitions; no renderer frame callback is involved. */
|
||||||
|
public class WeatherRuntimeStateTest {
|
||||||
|
@Test public void enableIsIdempotentAndDisabledEnableWaitsForFields() {
|
||||||
|
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.READY, true));
|
||||||
|
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.DISABLED, true));
|
||||||
|
}
|
||||||
|
@Test public void submitRecoversErrorAndReachesReadyOnlyWithWindow() {
|
||||||
|
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.ERROR, WeatherRuntime.FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW));
|
||||||
|
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY));
|
||||||
|
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.DUPLICATE));
|
||||||
|
}
|
||||||
|
@Test public void clearAndDisableUseSuccessfulCoreResult() {
|
||||||
|
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterClear(WeatherRuntime.State.READY, true));
|
||||||
|
assertEquals(WeatherRuntime.State.DISABLED, WeatherRuntime.stateAfterDisable(true));
|
||||||
|
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterDisable(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -534,8 +534,20 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setParticleFloat(property: String, value: Float): Boolean {
|
private fun setParticleFloat(property: String, value: Float): Boolean {
|
||||||
return weatherRuntime?.setParticleVisual(property, value) == WeatherRuntime.CommandResult.ACCEPTED ||
|
val runtimeResult = weatherRuntime?.let { runtime ->
|
||||||
(weatherRuntime == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value))
|
when (property) {
|
||||||
|
"raster-particle-particle-count" -> runtime.setParticleCount(value.toInt())
|
||||||
|
"raster-particle-line-width" -> runtime.setParticleLineWidth(value)
|
||||||
|
"raster-particle-particle-length" -> runtime.setParticleLength(value)
|
||||||
|
"raster-particle-particle-opacity" -> runtime.setParticleOpacity(value)
|
||||||
|
"raster-particle-trail-opacity" -> runtime.setTrailOpacity(value)
|
||||||
|
"raster-particle-trail-fade" -> runtime.setTrailFade(value)
|
||||||
|
"raster-particle-speed-color-enabled" -> runtime.setParticleSpeedColorEnabled(value > 0.5f)
|
||||||
|
else -> null // Test-only direct entry retains fixture diagnostics such as time-mix.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runtimeResult == WeatherRuntime.CommandResult.ACCEPTED ||
|
||||||
|
(runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetVisualDefaults() {
|
private fun resetVisualDefaults() {
|
||||||
|
|||||||
@@ -999,6 +999,13 @@ bool RenderRasterParticleLayer::needsRepaint() const {
|
|||||||
baseImpl->visibility != style::VisibilityType::None;
|
baseImpl->visibility != style::VisibilityType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RenderRasterParticleLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const {
|
||||||
|
if (!formalSource || !gpuResources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false;
|
||||||
|
const auto& source = static_cast<const RenderRasterArraySource&>(renderSource);
|
||||||
|
const auto& impl = source.impl();
|
||||||
|
return impl.hostFieldSubmission && !impl.field && !impl.nextField && !impl.timeline;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderRasterParticleLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) {
|
void RenderRasterParticleLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) {
|
||||||
// RenderOrchestrator calls this while visiting each source. In a style with more
|
// RenderOrchestrator calls this while visiting each source. In a style with more
|
||||||
// than one source, false can therefore be a transient per-source result, not a
|
// than one source, false can therefore be a transient per-source result, not a
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public:
|
|||||||
const std::shared_ptr<UpdateParameters>&,
|
const std::shared_ptr<UpdateParameters>&,
|
||||||
const RenderTree&,
|
const RenderTree&,
|
||||||
UniqueChangeRequestVec&) override;
|
UniqueChangeRequestVec&) override;
|
||||||
|
bool needsHiddenLayerSourceCleanup(const RenderSource&) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void transition(const TransitionParameters&) override {}
|
void transition(const TransitionParameters&) override {}
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ void RenderWeatherColorLayer::evaluate(const PropertyEvaluationParameters&) {
|
|||||||
evaluatedProperties = std::move(properties);
|
evaluatedProperties = std::move(properties);
|
||||||
}
|
}
|
||||||
bool RenderWeatherColorLayer::needsRepaint() const { return isLayerRenderable() && sourceField && sourceNextField; }
|
bool RenderWeatherColorLayer::needsRepaint() const { return isLayerRenderable() && sourceField && sourceNextField; }
|
||||||
|
bool RenderWeatherColorLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const {
|
||||||
|
if (!resources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false;
|
||||||
|
const auto& source = static_cast<const RenderRasterArraySource&>(renderSource);
|
||||||
|
const auto& impl = source.impl();
|
||||||
|
return impl.hostFieldSubmission && !impl.field && !impl.nextField && !impl.timeline;
|
||||||
|
}
|
||||||
void RenderWeatherColorLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) { RenderLayer::markLayerRenderable(willRender, changes); }
|
void RenderWeatherColorLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) { RenderLayer::markLayerRenderable(willRender, changes); }
|
||||||
void RenderWeatherColorLayer::layerRemoved(UniqueChangeRequestVec& changes) { resources.reset(); RenderLayer::layerRemoved(changes); }
|
void RenderWeatherColorLayer::layerRemoved(UniqueChangeRequestVec& changes) { resources.reset(); RenderLayer::layerRemoved(changes); }
|
||||||
void RenderWeatherColorLayer::markContextDestroyed() { resources.reset(); }
|
void RenderWeatherColorLayer::markContextDestroyed() { resources.reset(); }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public:
|
|||||||
~RenderWeatherColorLayer() override;
|
~RenderWeatherColorLayer() override;
|
||||||
void update(gfx::ShaderRegistry&, gfx::Context&, const TransformState&, const std::shared_ptr<UpdateParameters>&,
|
void update(gfx::ShaderRegistry&, gfx::Context&, const TransformState&, const std::shared_ptr<UpdateParameters>&,
|
||||||
const RenderTree&, UniqueChangeRequestVec&) override;
|
const RenderTree&, UniqueChangeRequestVec&) override;
|
||||||
|
bool needsHiddenLayerSourceCleanup(const RenderSource&) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void transition(const TransitionParameters&) override {}
|
void transition(const TransitionParameters&) override {}
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ public:
|
|||||||
// Returns true when an active animated layer needs another MapLibre frame.
|
// Returns true when an active animated layer needs another MapLibre frame.
|
||||||
virtual bool needsRepaint() const { return false; }
|
virtual bool needsRepaint() const { return false; }
|
||||||
|
|
||||||
|
// Exceptional source-driven cleanup for a hidden layer. The default keeps
|
||||||
|
// normal MapLibre hidden-layer lifecycle unchanged.
|
||||||
|
virtual bool needsHiddenLayerSourceCleanup(const RenderSource&) const { return false; }
|
||||||
|
|
||||||
// Returns true if layer writes to depth buffer by drawing using PaintParameters::depthModeFor3D().
|
// Returns true if layer writes to depth buffer by drawing using PaintParameters::depthModeFor3D().
|
||||||
virtual bool is3D() const { return false; }
|
virtual bool is3D() const { return false; }
|
||||||
|
|
||||||
|
|||||||
@@ -443,13 +443,13 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source-driven weather unload must reach hidden layers too. Normal
|
// Only weather layers that explicitly opt in may clean up after Host clear.
|
||||||
// visibility only suspends drawing; an empty raster-array releases resources.
|
// Ordinary hidden layers retain MapLibre's existing lifecycle.
|
||||||
for (auto& entry : orderedLayers) {
|
for (auto& entry : orderedLayers) {
|
||||||
auto& layer = entry.get();
|
auto& layer = entry.get();
|
||||||
const auto source = renderSources.find(layer.baseImpl->source);
|
const auto source = renderSources.find(layer.baseImpl->source);
|
||||||
if (!layer.isLayerRenderable() && source != renderSources.end() &&
|
if (!layer.isLayerRenderable() && source != renderSources.end() &&
|
||||||
source->second->baseImpl->type == style::SourceType::RasterArray) {
|
layer.needsHiddenLayerSourceCleanup(*source->second)) {
|
||||||
layer.prepare({.source = source->second.get(), .imageManager = *imageManager,
|
layer.prepare({.source = source->second.get(), .imageManager = *imageManager,
|
||||||
.patternAtlas = *patternAtlas, .lineAtlas = *lineAtlas,
|
.patternAtlas = *patternAtlas, .lineAtlas = *lineAtlas,
|
||||||
.state = updateParameters->transformState});
|
.state = updateParameters->transformState});
|
||||||
@@ -985,7 +985,7 @@ void RenderOrchestrator::updateLayers(gfx::ShaderRegistry& shaders,
|
|||||||
auto& layer = entry.get();
|
auto& layer = entry.get();
|
||||||
const auto source = renderSources.find(layer.baseImpl->source);
|
const auto source = renderSources.find(layer.baseImpl->source);
|
||||||
if (!layer.isLayerRenderable() && source != renderSources.end() &&
|
if (!layer.isLayerRenderable() && source != renderSources.end() &&
|
||||||
source->second->baseImpl->type == style::SourceType::RasterArray) {
|
layer.needsHiddenLayerSourceCleanup(*source->second)) {
|
||||||
layer.update(shaders, context, state, updateParameters, renderTree, changes);
|
layer.update(shaders, context, state, updateParameters, renderTree, changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ std::string fixtureBytes(const std::string& fixtureID, const std::string& file)
|
|||||||
std::string localMetadata(const int index) {
|
std::string localMetadata(const int index) {
|
||||||
return "{\"schema\":\"maplibre-particle-field/v1\",\"fieldId\":\"production-t" +
|
return "{\"schema\":\"maplibre-particle-field/v1\",\"fieldId\":\"production-t" +
|
||||||
std::to_string(index) + "\",\"fieldType\":\"WIND\",\"displayDomain\":\"GLOBAL\",\"generation\":" +
|
std::to_string(index) + "\",\"fieldType\":\"WIND\",\"displayDomain\":\"GLOBAL\",\"generation\":" +
|
||||||
std::to_string(index + 1) + ",\"validTime\":" + std::to_string(202609040000ULL + index * 100) +
|
std::to_string(index + 1) + ",\"validTime\":" + std::to_string(1704067200000ULL + index * 3600000ULL) +
|
||||||
",\"dataVersion\":" + std::to_string(index + 1) +
|
",\"dataVersion\":" + std::to_string(index + 1) +
|
||||||
",\"bounds\":{\"west\":-180,\"south\":-85,\"east\":180,\"north\":85},\"grid\":{\"width\":16,\"height\":16},\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"noData\":-9999},\"payload\":{\"byteLength\":2048}}";
|
",\"bounds\":{\"west\":-180,\"south\":-85,\"east\":180,\"north\":85},\"grid\":{\"width\":16,\"height\":16},\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"noData\":-9999},\"payload\":{\"byteLength\":2048}}";
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ TEST(Source, LocalProductionFeedSubmitsSnapshotsWithProducerTime) {
|
|||||||
ASSERT_TRUE(result) << result.message;
|
ASSERT_TRUE(result) << result.message;
|
||||||
ASSERT_TRUE(result.snapshot);
|
ASSERT_TRUE(result.snapshot);
|
||||||
EXPECT_EQ(result.snapshot->generation, static_cast<uint64_t>(index + 1));
|
EXPECT_EQ(result.snapshot->generation, static_cast<uint64_t>(index + 1));
|
||||||
EXPECT_EQ(result.snapshot->validTime, 202609040000ULL + static_cast<uint64_t>(index) * 100);
|
EXPECT_EQ(result.snapshot->validTime, 1704067200000ULL + static_cast<uint64_t>(index) * 3600000ULL);
|
||||||
snapshots.push_back(std::move(*result.snapshot));
|
snapshots.push_back(std::move(*result.snapshot));
|
||||||
}
|
}
|
||||||
for (size_t index = 1; index < snapshots.size(); ++index) {
|
for (size_t index = 1; index < snapshots.size(); ++index) {
|
||||||
@@ -441,11 +441,18 @@ TEST(Source, HostFieldClearAndTimestampSeekAreFailClosed) {
|
|||||||
EXPECT_FALSE(source.impl().timeline);
|
EXPECT_FALSE(source.impl().timeline);
|
||||||
EXPECT_EQ(source.submitHostField(localMetadata(0), eastPayload), style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
EXPECT_EQ(source.submitHostField(localMetadata(0), eastPayload), style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||||
EXPECT_EQ(source.submitHostField(localMetadata(1), northPayload), style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
EXPECT_EQ(source.submitHostField(localMetadata(1), northPayload), style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||||
EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(202609040050ULL));
|
const uint64_t t0 = 1704067200000ULL;
|
||||||
|
const uint64_t t1 = 1704070800000ULL;
|
||||||
|
EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(t0));
|
||||||
|
EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 0.0f);
|
||||||
|
EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(t0 + 1800000ULL));
|
||||||
EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 0.5f);
|
EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 0.5f);
|
||||||
EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(202609039999ULL));
|
EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(t1));
|
||||||
|
EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 1.0f);
|
||||||
|
EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(t0 - 1));
|
||||||
|
EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(t1 + 1));
|
||||||
EXPECT_TRUE(source.clearHostFields());
|
EXPECT_TRUE(source.clearHostFields());
|
||||||
EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(202609040050ULL));
|
EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(t0 + 1800000ULL));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Source, HostFieldSubmissionStyleContractCreatesEmptyIngress) {
|
TEST(Source, HostFieldSubmissionStyleContractCreatesEmptyIngress) {
|
||||||
@@ -515,9 +522,9 @@ TEST(Source, HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField) {
|
|||||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||||
|
|
||||||
auto nonMonotonic = localMetadata(2);
|
auto nonMonotonic = localMetadata(2);
|
||||||
const auto validTime = nonMonotonic.find("202609040200");
|
const auto validTime = nonMonotonic.find("1704074400000");
|
||||||
ASSERT_NE(validTime, std::string::npos);
|
ASSERT_NE(validTime, std::string::npos);
|
||||||
nonMonotonic.replace(validTime, std::string("202609040200").size(), "202609040050");
|
nonMonotonic.replace(validTime, std::string("1704074400000").size(), "1704069000000");
|
||||||
EXPECT_EQ(source.submitHostField(nonMonotonic, payload),
|
EXPECT_EQ(source.submitHostField(nonMonotonic, payload),
|
||||||
style::RasterArrayHostFieldSubmitResult::RejectedNonMonotonicSnapshot);
|
style::RasterArrayHostFieldSubmitResult::RejectedNonMonotonicSnapshot);
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
|
|||||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}}));
|
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}}));
|
||||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {0.0f, {1, 0, 0, 1}}}));
|
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {0.0f, {1, 0, 0, 1}}}));
|
||||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {10.0f, {std::numeric_limits<float>::quiet_NaN(), 0, 0, 1}}}));
|
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {10.0f, {std::numeric_limits<float>::quiet_NaN(), 0, 0, 1}}}));
|
||||||
|
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {1.0f, {0, 0, 1, 1}}, {2.0f, {0, 0, 1, 1}},
|
||||||
|
{3.0f, {0, 0, 1, 1}}, {4.0f, {0, 0, 1, 1}}, {5.0f, {0, 0, 1, 1}},
|
||||||
|
{6.0f, {0, 0, 1, 1}}, {7.0f, {0, 0, 1, 1}}, {8.0f, {0, 0, 1, 1}}}));
|
||||||
EXPECT_EQ(layer.getColorStops(), valid);
|
EXPECT_EQ(layer.getColorStops(), valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -456,3 +456,10 @@ WeatherColorLayer 的 `ParticleGpuValidationUBO` designated initializer 补齐 `
|
|||||||
- clear 后 Source 保持可复用但无 field;Render 线程空场路径释放 Particle State/Trail/draw、field texture 与 WeatherColor resources。OFF 不 reload style,ON 后要求重新 submit T0/T1。
|
- clear 后 Source 保持可复用但无 field;Render 线程空场路径释放 Particle State/Trail/draw、field texture 与 WeatherColor resources。OFF 不 reload style,ON 后要求重新 submit T0/T1。
|
||||||
- WeatherColor 支持最多 8 个 validated dynamic stops;particleColor/trailColor 独立 UBO 数据通路,不依赖 WeatherColor ramp。正常 time seek 和 visual/ramp/color 更新均不重新提交 field 或 reset simulation。
|
- WeatherColor 支持最多 8 个 validated dynamic stops;particleColor/trailColor 独立 UBO 数据通路,不依赖 WeatherColor ramp。正常 time seek 和 visual/ramp/color 更新均不重新提交 field 或 reset simulation。
|
||||||
- Linux shader generator、OpenGL Core 及相关 Source/Style test translation unit 已验证;Android Gradle/APK/ADB/设备/Jest 留待后续人工验收。
|
- Linux shader generator、OpenGL Core 及相关 Source/Style test translation unit 已验证;Android Gradle/APK/ADB/设备/Jest 留待后续人工验收。
|
||||||
|
|
||||||
|
## 2026-09-07 — STEP-057A 审计修正
|
||||||
|
|
||||||
|
- `WeatherRuntime` 正式 Host API 改为明确 typed 粒子外观方法,删除 public string property setter;Test App 的通用 fixture 调试仍为低层路径。
|
||||||
|
- enable/clear/disable/submit 状态转换已幂等并可恢复,另增加既有 Android JUnit state transition 测试源码;未运行 Gradle。
|
||||||
|
- hidden unload 改成 RasterParticle/WeatherColor renderer 的 Host-empty-source 显式 opt-in cleanup,移除对所有 hidden RasterArray layer 的全局 prepare/update。
|
||||||
|
- timestamp 测试改用真实 epoch milliseconds,覆盖 T0、midpoint、T1、上下边界和 clear 后拒绝;WeatherColor capacity 由 Java 通过 JNI 读取 C++ `MaxWeatherColorStops`。
|
||||||
|
|||||||
Reference in New Issue
Block a user