From b9b3dd22530759129b8c80477409bb7e5df4f4cc Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 7 Sep 2026 14:55:32 +0800 Subject: [PATCH] feat: add weather-local render cadence --- .../style/sources/raster_array_source.hpp | 13 +++++ .../src/cpp/native_map_view.cpp | 14 +++++ .../src/cpp/native_map_view.hpp | 2 + .../maplibre/android/maps/MapLibreMap.java | 9 +++ .../org/maplibre/android/maps/NativeMap.java | 4 ++ .../maplibre/android/maps/NativeMapView.java | 12 ++++ .../maplibre/android/maps/WeatherRuntime.java | 46 +++++++++++++-- .../android/maps/WeatherRuntimeStateTest.java | 10 ++++ .../ParticleCoreLayerValidationActivity.kt | 18 +++++- .../render_particle_validation_layer.cpp | 19 ++++++- .../layers/render_weather_color_layer.cpp | 5 +- .../style/sources/raster_array_source.cpp | 56 +++++++++++++++++++ test/style/source.test.cpp | 24 ++++++++ 13 files changed, 224 insertions(+), 8 deletions(-) diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index 7d049bf73..665fe52df 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -41,6 +41,8 @@ enum class RasterArrayPlaybackState : uint8_t { Ended }; +enum class RasterArrayTargetFps : uint8_t { Auto = 0, Fps30 = 30, Fps60 = 60 }; + struct RasterArrayTimelineFrame { std::shared_ptr t0; std::shared_ptr t1; @@ -72,6 +74,10 @@ public: bool injectGap(); bool injectInvalid(); bool recover(); + bool setTargetFps(RasterArrayTargetFps); + RasterArrayTargetFps targetFps() const; + bool cadenceDue(std::chrono::steady_clock::time_point) const; + void markCadenceTick(std::chrono::steady_clock::time_point); RasterArrayTimelineFrame frame() const; float playbackSpeed() const; size_t size() const; @@ -92,6 +98,10 @@ private: RasterArrayPlaybackState stateBeforeWait = RasterArrayPlaybackState::Paused; uint64_t windowRevision = 1; std::chrono::steady_clock::time_point lastTick{}; + std::chrono::steady_clock::time_point lastCadenceTick{}; + std::chrono::steady_clock::time_point lastCadenceLog{}; + uint64_t cadenceTickCount = 0; + RasterArrayTargetFps targetFps_ = RasterArrayTargetFps::Auto; const size_t cacheCapacity_ = 8; }; @@ -162,6 +172,8 @@ public: bool seekRuntimeTimelineToTimestamp(uint64_t timestamp); // Low-frequency JNI commands for the Source-owned validation timeline. bool controlRuntimeTimeline(int command, float value = 0.0f); + bool setRuntimeTargetFps(uint8_t targetFps); + uint8_t runtimeTargetFps() const; bool supportsLayerType(const LayerTypeInfo*) const override; mapbox::base::WeakPtr makeWeakPtr() override { return weakFactory.makeWeakPtr(); } @@ -207,6 +219,7 @@ private: size_t nextRuntimeField = 0; uint64_t runtimeUpdateSequence = 0; std::shared_ptr timeline; + RasterArrayTargetFps targetFps = RasterArrayTargetFps::Auto; mapbox::base::WeakPtrFactory weakFactory{this}; }; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index ab826b5fa..d32d385bb 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1218,6 +1218,18 @@ jni::jboolean NativeMapView::setRasterParticleTrailColor(JNIEnv& env, return true; } +jni::jboolean NativeMapView::setRasterArraySourceTargetFps(JNIEnv& env, + const jni::String& sourceId, + const jni::jint targetFps) { + auto* source = dynamic_cast(map->getStyle().getSource(jni::Make(env, sourceId))); + return source && source->setRuntimeTargetFps(static_cast(targetFps)); +} + +jni::jint NativeMapView::getRasterArraySourceTargetFps(JNIEnv& env, const jni::String& sourceId) { + auto* source = dynamic_cast(map->getStyle().getSource(jni::Make(env, sourceId))); + return source ? source->runtimeTargetFps() : 0; +} + jni::jboolean NativeMapView::setWeatherColorStops(JNIEnv& env, const jni::String& layerId, const jni::Array& speeds, @@ -1721,6 +1733,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::setRasterParticleColors, "nativeSetRasterParticleColors"), METHOD(&NativeMapView::setRasterParticleColor, "nativeSetRasterParticleColor"), METHOD(&NativeMapView::setRasterParticleTrailColor, "nativeSetRasterParticleTrailColor"), + METHOD(&NativeMapView::setRasterArraySourceTargetFps, "nativeSetRasterArraySourceTargetFps"), + METHOD(&NativeMapView::getRasterArraySourceTargetFps, "nativeGetRasterArraySourceTargetFps"), METHOD(&NativeMapView::setWeatherColorStops, "nativeSetWeatherColorStops"), METHOD(&NativeMapView::getWeatherColorStopCapacity, "nativeGetWeatherColorStopCapacity"), METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"), diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 9f64906f5..57886a699 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -286,6 +286,8 @@ public: jni::jboolean setRasterParticleColors(JNIEnv&, const jni::String&, const jni::Array&, const jni::Array&); jni::jboolean setRasterParticleColor(JNIEnv&, const jni::String&, const jni::Array&); jni::jboolean setRasterParticleTrailColor(JNIEnv&, const jni::String&, const jni::Array&); + jni::jboolean setRasterArraySourceTargetFps(JNIEnv&, const jni::String&, jni::jint); + jni::jint getRasterArraySourceTargetFps(JNIEnv&, const jni::String&); jni::jboolean setWeatherColorStops(JNIEnv&, const jni::String&, const jni::Array&, const jni::Array&); jni::jint getWeatherColorStopCapacity(JNIEnv&); jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java index a4f359810..07f1fdef5 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java @@ -309,6 +309,15 @@ public final class MapLibreMap { return nativeMapView.setRasterParticleTrailColor(layerId, trailColor); } + /** Sets the source-local weather cadence (0=auto, 30 or 60 FPS). */ + public boolean setRasterArraySourceTargetFps(String sourceId, int targetFps) { + return nativeMapView.setRasterArraySourceTargetFps(sourceId, targetFps); + } + + public int getRasterArraySourceTargetFps(String sourceId) { + return nativeMapView.getRasterArraySourceTargetFps(sourceId); + } + /** Installs a validated WeatherColor speed ramp without changing particle style. */ public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) { return nativeMapView.setWeatherColorStops(layerId, speedsMps, rgbaStops); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index b7fe3852e..db988665d 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -149,6 +149,10 @@ interface NativeMap { boolean setRasterParticleTrailColor(String layerId, float[] trailColor); + boolean setRasterArraySourceTargetFps(String sourceId, int targetFps); + + int getRasterArraySourceTargetFps(String sourceId); + boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops); int getWeatherColorStopCapacity(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index f8cf435f8..e1db92001 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -972,6 +972,16 @@ final class NativeMapView implements NativeMap { return !checkState("setRasterParticleTrailColor") && nativeSetRasterParticleTrailColor(layerId, trailColor); } + @Override + public boolean setRasterArraySourceTargetFps(String sourceId, int targetFps) { + return !checkState("setRasterArraySourceTargetFps") && nativeSetRasterArraySourceTargetFps(sourceId, targetFps); + } + + @Override + public int getRasterArraySourceTargetFps(String sourceId) { + return checkState("getRasterArraySourceTargetFps") ? 0 : nativeGetRasterArraySourceTargetFps(sourceId); + } + public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) { return !checkState("setWeatherColorStops") && nativeSetWeatherColorStops(layerId, speedsMps, rgbaStops); } @@ -1755,6 +1765,8 @@ final class NativeMapView implements NativeMap { @Keep private native boolean nativeSetRasterParticleColors(String layerId, float[] particleColor, float[] trailColor); @Keep private native boolean nativeSetRasterParticleColor(String layerId, float[] particleColor); @Keep private native boolean nativeSetRasterParticleTrailColor(String layerId, float[] trailColor); + @Keep private native boolean nativeSetRasterArraySourceTargetFps(String sourceId, int targetFps); + @Keep private native int nativeGetRasterArraySourceTargetFps(String sourceId); @Keep private native boolean nativeSetWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops); @Keep private native int nativeGetWeatherColorStopCapacity(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java index a25a9389d..f489712e6 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java @@ -2,6 +2,9 @@ package org.maplibre.android.maps; import androidx.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; + import java.util.List; /** @@ -12,6 +15,7 @@ public final class WeatherRuntime { public enum State { DISABLED, WAITING_FOR_FIELDS, READY, ERROR } public enum PlaybackState { PLAY, PAUSE } public enum PlaybackSpeed { HALF(0.5f), NORMAL(1.0f), DOUBLE(2.0f); final float value; PlaybackSpeed(float value) { this.value = value; } } + public enum TargetFps { AUTO(0), FPS_30(30), FPS_60(60); final int value; TargetFps(int value) { this.value = value; } } public enum FieldSubmitResult { REJECTED_SOURCE_MODE(-1), REJECTED_ADAPTER(-2), REJECTED_INCOMPATIBLE(-3), REJECTED_NON_MONOTONIC(-4), REJECTED_CAPACITY(-5), ACCEPTED_PENDING_INITIAL_WINDOW(1), INITIAL_WINDOW_READY(2), ACCEPTED_NEXT_SNAPSHOT(3), DUPLICATE(4), UNKNOWN(Integer.MIN_VALUE); @@ -44,6 +48,16 @@ public final class WeatherRuntime { private State state = State.WAITING_FOR_FIELDS; private boolean weatherColorVisible = true; private boolean particleVisible = true; + private PlaybackState playbackState = PlaybackState.PAUSE; + private TargetFps targetFps = TargetFps.AUTO; + private Handler cadenceHandler; + private final Runnable cadenceWake = new Runnable() { + @Override public void run() { + if (!shouldWakeWeather()) return; + map.triggerRepaint(); + scheduleCadenceWake(); + } + }; public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) { this.map = map; this.config = config; this.weatherColorStopCapacity = map.getWeatherColorStopCapacity(); @@ -58,9 +72,10 @@ public final class WeatherRuntime { if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER; FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes)); state = stateAfterSubmit(state, result); + if (state == State.READY && result.accepted()) scheduleCadenceWake(); return result; } - 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 clearWindFields() { if (state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterClear(state, ok); if (ok) cancelCadenceWake(); return result(ok); } public CommandResult setWeatherEnabled(boolean enabled) { if (!enabled) { if (state == State.DISABLED) return CommandResult.ACCEPTED; @@ -69,6 +84,7 @@ public final class WeatherRuntime { boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false); boolean clear = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterDisable(color && particle && clear); + if (state != State.READY) cancelCadenceWake(); return result(color && particle && clear); } if (state == State.ERROR) return CommandResult.REJECTED; @@ -78,10 +94,10 @@ public final class WeatherRuntime { state = stateAfterEnable(state, 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 setParticleVisible(boolean visible) { particleVisible = visible; return result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); } + public CommandResult setWeatherColorVisible(boolean visible) { weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); scheduleCadenceWake(); return result; } + public CommandResult setParticleVisible(boolean visible) { particleVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); scheduleCadenceWake(); return result; } public CommandResult setForecastTime(long timestampMs) { requireEnabled(); return result(timestampMs >= 0 && map.seekRasterArraySourceTimelineToTimestamp(config.sourceId, timestampMs)); } - 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(); CommandResult result = result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); if (result == CommandResult.ACCEPTED) { playbackState = playback; scheduleCadenceWake(); } return result; } 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 setParticleCount(int count) { return setParticleFloat("raster-particle-particle-count", count); } @@ -103,6 +119,14 @@ public final class WeatherRuntime { 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})); } + public CommandResult setTargetFps(@NonNull TargetFps target) { + if (!map.setRasterArraySourceTargetFps(config.sourceId, target.value)) return CommandResult.REJECTED; + targetFps = target; + cancelCadenceWake(); + scheduleCadenceWake(); + return CommandResult.ACCEPTED; + } + public TargetFps getTargetFps() { return targetFps; } public CommandResult setWeatherColorStops(@NonNull List stops) { 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]; @@ -110,6 +134,20 @@ public final class WeatherRuntime { return result(map.setWeatherColorStops(config.weatherColorLayerId, speeds, colors)); } private static CommandResult result(boolean accepted) { return accepted ? CommandResult.ACCEPTED : CommandResult.REJECTED; } + private boolean shouldWakeWeather() { + return targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY)); + } + private void scheduleCadenceWake() { + if (!shouldWakeWeather()) return; + try { + if (cadenceHandler == null) cadenceHandler = new Handler(Looper.getMainLooper()); + cadenceHandler.removeCallbacks(cadenceWake); + cadenceHandler.postDelayed(cadenceWake, targetFps == TargetFps.FPS_30 ? 34L : 17L); + } catch (RuntimeException ignored) { + // Local JVM tests may not provide an Android Looper; Core cadence remains authoritative. + } + } + private void cancelCadenceWake() { if (cadenceHandler != null) cadenceHandler.removeCallbacks(cadenceWake); } 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; diff --git a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java index 73c5e062c..422147112 100644 --- a/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java +++ b/platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java @@ -92,4 +92,14 @@ public class WeatherRuntimeStateTest { assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleColor(Float.NaN, 0f, 0f, 1f)); assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTrailColor(0f, 0f, 0f, 2f)); } + + @Test public void targetFpsIsTypedAndDoesNotSubmitFields() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + Mockito.when(map.setRasterArraySourceTargetFps("source", 30)).thenReturn(true); + WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setTargetFps(WeatherRuntime.TargetFps.FPS_30)); + assertEquals(WeatherRuntime.TargetFps.FPS_30, runtime.getTargetFps()); + Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class)); + } } diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 036afa78e..4e2fb9f27 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -49,6 +49,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var timelineState = "NOT READY" private var timelineSpeed = "1x" private var weatherRuntime: WeatherRuntime? = null + private var realWeatherTargetFps = WeatherRuntime.TargetFps.AUTO override fun onCreate(savedInstanceState: Bundle?) { active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true @@ -283,6 +284,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { popup.menu.add(0, 100, 0, "PARTICLE TUNING").isEnabled = false arrayOf("P -", "P +", "W -", "W +", "L -", "L +", "O -", "O +", "T -", "T +", "F -", "F +", "RESET DEFAULT") .forEachIndexed { index, label -> popup.menu.add(0, index, index, label) } + popup.menu.add(0, 20, 20, "FPS AUTO") + popup.menu.add(0, 21, 21, "FPS 30") + popup.menu.add(0, 22, 22, "FPS 60") popup.setOnMenuItemClickListener { item -> adjustRealWeatherTuning(item.itemId); true } popup.setOnDismissListener { if (realWeatherTuningMenu === popup) realWeatherTuningMenu = null } popup.show() @@ -303,10 +307,21 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 10 -> setVisualFloat("raster-particle-trail-fade", (trailFade - 0.01f).coerceAtLeast(0.80f)) { trailFade = it } 11 -> setVisualFloat("raster-particle-trail-fade", (trailFade + 0.01f).coerceAtMost(0.99f)) { trailFade = it } 12 -> resetRealWeatherDefaults() + 20 -> setRealWeatherTargetFps(WeatherRuntime.TargetFps.AUTO) + 21 -> setRealWeatherTargetFps(WeatherRuntime.TargetFps.FPS_30) + 22 -> setRealWeatherTargetFps(WeatherRuntime.TargetFps.FPS_60) } Log.i(TAG, "ParticleTuning: event=APPLY realWeather=true action=$action values=${realWeatherHudValues()}") } + private fun setRealWeatherTargetFps(target: WeatherRuntime.TargetFps) { + if (weatherRuntime?.setTargetFps(target) == WeatherRuntime.CommandResult.ACCEPTED) { + realWeatherTargetFps = target + status.text = statusText() + Log.i(TAG, "WeatherCadence: event=TARGET_FPS_CHANGED target=${target.name}") + } + } + private fun nextRealWeatherCount(direction: Int): Int { val index = REAL_WEATHER_PARTICLE_COUNTS.indexOf(particleCount).let { if (it < 0) 3 else it } return REAL_WEATHER_PARTICLE_COUNTS[(index + direction).coerceIn(0, REAL_WEATHER_PARTICLE_COUNTS.lastIndex)] @@ -433,6 +448,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { styleReady = true if (realWeatherMode) { weatherRuntime = WeatherRuntime(maplibreMap, WeatherRuntime.Config(PARTICLE_SOURCE_ID, WEATHER_COLOR_LAYER_ID, PARTICLE_LAYER_ID)) + weatherRuntime?.setTargetFps(realWeatherTargetFps) } else { weatherRuntime = null } @@ -706,7 +722,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { return if (realWeatherMode) "$summary\n${realWeatherHudValues()}" else "$summary · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}" } - private fun realWeatherHudValues(): String = "P $particleCount · W ${"%.2f".format(lineWidth)} · L ${"%.4f".format(particleLength)} · O ${"%.2f".format(particleOpacity)} · T ${"%.2f".format(trailOpacity)} · F ${"%.2f".format(trailFade)}" + private fun realWeatherHudValues(): String = "FPS ${realWeatherTargetFps.name.removePrefix("FPS_")} · P $particleCount · W ${"%.2f".format(lineWidth)} · L ${"%.4f".format(particleLength)} · O ${"%.2f".format(particleOpacity)} · T ${"%.2f".format(trailOpacity)} · F ${"%.2f".format(trailFade)}" private fun validationStyle(active: Boolean, enabled: Boolean, reason: String): String { val fixtureId = fixtureId() diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 6504f22c9..cb7f0b61b 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -995,8 +995,9 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) { bool RenderRasterParticleLayer::needsRepaint() const { const auto& properties = static_cast(*evaluatedProperties); - return properties.active && (!formalSource || (sourceField && sourceNextField)) && - baseImpl->visibility != style::VisibilityType::None; + if (!properties.active || (formalSource && (!sourceField || !sourceNextField)) || + baseImpl->visibility == style::VisibilityType::None) return false; + return !formalSource || !sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now()); } bool RenderRasterParticleLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const { @@ -1738,6 +1739,20 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, return; } + // Camera-driven frames may recompose existing weather drawables, but do + // not advance the shared timeline, State, or Trail before the deadline. + if (formalSource && sourceTimeline && !sourceTimeline->cadenceDue(std::chrono::steady_clock::now())) { + auto& compositeGroup = static_cast(*layerGroup); + compositeGroup.clearDrawables(); + const auto center = transformState.getLatLng(LatLng::Unwrapped); + const auto centerWrap = wrapForLongitude(center.longitude()); + const auto wrap = static_cast(centerWrap); + buildQuad(context, resources, compositeGroup, getID() + "-composite-pass-" + std::to_string(wrap), + resources->trailTargets[resources->trailRead]->getTexture(), {}, resources->maskTexture, {}, 0.0f, + ValidationPass::Composite, resources->trailRead, resources->trailRead, wrap, maskMode, false); + return; + } + const auto stateWrite = resources->stateWrite(); const auto trailWrite = resources->trailWrite(); auto& stateGroup = static_cast(*resources->stateTargets[stateWrite]->getLayerGroup(0)); diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index f2464ffa0..2f0648174 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -137,7 +137,10 @@ void RenderWeatherColorLayer::evaluate(const PropertyEvaluationParameters&) { properties->renderPasses = mbgl::underlying_type(passes); evaluatedProperties = std::move(properties); } -bool RenderWeatherColorLayer::needsRepaint() const { return isLayerRenderable() && sourceField && sourceNextField; } +bool RenderWeatherColorLayer::needsRepaint() const { + return isLayerRenderable() && sourceField && sourceNextField && + (!sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now())); +} bool RenderWeatherColorLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const { if (!resources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false; const auto& source = static_cast(renderSource); diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index 3bd36812f..c83201295 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -89,6 +89,17 @@ bool RasterArrayTimelineRuntime::moveToLocked(const size_t nextT0) { RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { std::lock_guard lock(mutex); const auto now = std::chrono::steady_clock::now(); + if (targetFps_ != RasterArrayTargetFps::Auto && lastCadenceTick.time_since_epoch().count() != 0) { + const auto interval = std::chrono::duration(1.0 / static_cast(targetFps_)); + if (now - lastCadenceTick < interval) return frameLocked(); + } + lastCadenceTick = now; + ++cadenceTickCount; + if (lastCadenceLog.time_since_epoch().count() == 0 || now - lastCadenceLog >= std::chrono::seconds(1)) { + lastCadenceLog = now; + Log::Info(Event::Render, "WeatherCadence: targetFps=" + std::to_string(static_cast(targetFps_)) + + " weatherTicks=" + std::to_string(cadenceTickCount)); + } if (lastTick == std::chrono::steady_clock::time_point{}) { lastTick = now; return frameLocked(); @@ -124,6 +135,30 @@ RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { return frameLocked(); } +bool RasterArrayTimelineRuntime::setTargetFps(const RasterArrayTargetFps target) { + std::lock_guard lock(mutex); + targetFps_ = target; + lastCadenceTick = {}; + return true; +} + +RasterArrayTargetFps RasterArrayTimelineRuntime::targetFps() const { + std::lock_guard lock(mutex); + return targetFps_; +} + +bool RasterArrayTimelineRuntime::cadenceDue(const std::chrono::steady_clock::time_point now) const { + std::lock_guard lock(mutex); + if (targetFps_ == RasterArrayTargetFps::Auto || lastCadenceTick.time_since_epoch().count() == 0) return true; + const auto interval = std::chrono::duration(1.0 / static_cast(targetFps_)); + return now - lastCadenceTick >= interval; +} + +void RasterArrayTimelineRuntime::markCadenceTick(const std::chrono::steady_clock::time_point now) { + std::lock_guard lock(mutex); + lastCadenceTick = now; +} + bool RasterArrayTimelineRuntime::play() { std::lock_guard lock(mutex); if (state == RasterArrayPlaybackState::Waiting) { @@ -509,6 +544,7 @@ void RasterArraySource::tryInitializeTimeline() { return; } timeline = std::make_shared(std::move(entries)); + timeline->setTargetFps(targetFps); auto updated = makeMutable(impl()); updated->timeline = timeline; baseImpl = std::move(updated); @@ -537,6 +573,7 @@ void RasterArraySource::tryInitializeTimeline() { } try { timeline = std::make_shared(std::move(entries)); + timeline->setTargetFps(targetFps); auto updated = makeMutable(impl()); updated->timeline = timeline; baseImpl = std::move(updated); @@ -754,6 +791,7 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField(const std::s entries.push_back({std::make_shared(initialT0), initialT0.validTime}); entries.push_back({std::make_shared(initialT1), initialT1.validTime}); timeline = std::make_shared(std::move(entries)); + timeline->setTargetFps(targetFps); auto updated = makeMutable(impl()); updated->timeline = timeline; baseImpl = std::move(updated); @@ -849,6 +887,24 @@ bool RasterArraySource::controlRuntimeTimeline(const int command, const float va } } +bool RasterArraySource::setRuntimeTargetFps(const uint8_t requested) { + RasterArrayTargetFps next; + switch (requested) { + case 0: next = RasterArrayTargetFps::Auto; break; + case 30: next = RasterArrayTargetFps::Fps30; break; + case 60: next = RasterArrayTargetFps::Fps60; break; + default: return false; + } + targetFps = next; + if (timeline) timeline->setTargetFps(next); + observer->onSourceChanged(*this); + return true; +} + +uint8_t RasterArraySource::runtimeTargetFps() const { + return static_cast(targetFps); +} + bool RasterArraySource::supportsLayerType(const LayerTypeInfo* info) const { if (!info) return false; const std::string type{info->type}; diff --git a/test/style/source.test.cpp b/test/style/source.test.cpp index 8dd592e61..42cf81284 100644 --- a/test/style/source.test.cpp +++ b/test/style/source.test.cpp @@ -613,6 +613,30 @@ TEST(Source, RasterArrayTimelineKeepsCoreOwnedWindowAndPlaybackControls) { EXPECT_EQ(timeline.frame().state, style::RasterArrayPlaybackState::Ended); } +TEST(Source, RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline) { + std::vector entries; + for (uint64_t index = 0; index < 3; ++index) { + entries.push_back({std::make_shared( + particle::OfflineFixtureParticleAdapter::load("uniform-east", index + 1)), index * 4000}); + } + style::RasterArrayTimelineRuntime timeline{std::move(entries)}; + const auto start = std::chrono::steady_clock::time_point{} + std::chrono::seconds(1); + EXPECT_TRUE(timeline.cadenceDue(start)); + timeline.markCadenceTick(start); + EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Fps30)); + EXPECT_TRUE(timeline.cadenceDue(start)); + timeline.markCadenceTick(start); + EXPECT_FALSE(timeline.cadenceDue(start + std::chrono::milliseconds(33))); + EXPECT_TRUE(timeline.cadenceDue(start + std::chrono::milliseconds(34))); + EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Fps60)); + EXPECT_TRUE(timeline.cadenceDue(start)); + timeline.markCadenceTick(start); + EXPECT_FALSE(timeline.cadenceDue(start + std::chrono::milliseconds(16))); + EXPECT_TRUE(timeline.cadenceDue(start + std::chrono::milliseconds(17))); + EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Auto)); + EXPECT_TRUE(timeline.cadenceDue(start)); +} + TEST(Source, RasterArrayRuntimeFieldWindowAdvancesWithoutReplacingTheSource) { SourceTest test; const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json");