feat: add weather-local render cadence
This commit is contained in:
@@ -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<const particle::ParticleFieldSnapshot> t0;
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> 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<Source> makeWeakPtr() override { return weakFactory.makeWeakPtr(); }
|
||||
|
||||
@@ -207,6 +219,7 @@ private:
|
||||
size_t nextRuntimeField = 0;
|
||||
uint64_t runtimeUpdateSequence = 0;
|
||||
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
|
||||
RasterArrayTargetFps targetFps = RasterArrayTargetFps::Auto;
|
||||
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -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<mbgl::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env, sourceId)));
|
||||
return source && source->setRuntimeTargetFps(static_cast<uint8_t>(targetFps));
|
||||
}
|
||||
|
||||
jni::jint NativeMapView::getRasterArraySourceTargetFps(JNIEnv& env, const jni::String& sourceId) {
|
||||
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env, sourceId)));
|
||||
return source ? source->runtimeTargetFps() : 0;
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::setWeatherColorStops(JNIEnv& env,
|
||||
const jni::String& layerId,
|
||||
const jni::Array<jni::jfloat>& 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"),
|
||||
|
||||
@@ -286,6 +286,8 @@ public:
|
||||
jni::jboolean setRasterParticleColors(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
|
||||
jni::jboolean setRasterParticleColor(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&);
|
||||
jni::jboolean setRasterParticleTrailColor(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&);
|
||||
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<jni::jfloat>&, const jni::Array<jni::jfloat>&);
|
||||
jni::jint getWeatherColorStopCapacity(JNIEnv&);
|
||||
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<ColorStop> 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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -995,8 +995,9 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
|
||||
bool RenderRasterParticleLayer::needsRepaint() const {
|
||||
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*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&>(*layerGroup);
|
||||
compositeGroup.clearDrawables();
|
||||
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
||||
const auto centerWrap = wrapForLongitude(center.longitude());
|
||||
const auto wrap = static_cast<int16_t>(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<LayerGroup&>(*resources->stateTargets[stateWrite]->getLayerGroup(0));
|
||||
|
||||
@@ -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<const RenderRasterArraySource&>(renderSource);
|
||||
|
||||
@@ -89,6 +89,17 @@ bool RasterArrayTimelineRuntime::moveToLocked(const size_t nextT0) {
|
||||
RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() {
|
||||
std::lock_guard<std::mutex> 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<double>(1.0 / static_cast<uint8_t>(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<uint8_t>(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<std::mutex> lock(mutex);
|
||||
targetFps_ = target;
|
||||
lastCadenceTick = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
RasterArrayTargetFps RasterArrayTimelineRuntime::targetFps() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return targetFps_;
|
||||
}
|
||||
|
||||
bool RasterArrayTimelineRuntime::cadenceDue(const std::chrono::steady_clock::time_point now) const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (targetFps_ == RasterArrayTargetFps::Auto || lastCadenceTick.time_since_epoch().count() == 0) return true;
|
||||
const auto interval = std::chrono::duration<double>(1.0 / static_cast<uint8_t>(targetFps_));
|
||||
return now - lastCadenceTick >= interval;
|
||||
}
|
||||
|
||||
void RasterArrayTimelineRuntime::markCadenceTick(const std::chrono::steady_clock::time_point now) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
lastCadenceTick = now;
|
||||
}
|
||||
|
||||
bool RasterArrayTimelineRuntime::play() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (state == RasterArrayPlaybackState::Waiting) {
|
||||
@@ -509,6 +544,7 @@ void RasterArraySource::tryInitializeTimeline() {
|
||||
return;
|
||||
}
|
||||
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
|
||||
timeline->setTargetFps(targetFps);
|
||||
auto updated = makeMutable<Impl>(impl());
|
||||
updated->timeline = timeline;
|
||||
baseImpl = std::move(updated);
|
||||
@@ -537,6 +573,7 @@ void RasterArraySource::tryInitializeTimeline() {
|
||||
}
|
||||
try {
|
||||
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
|
||||
timeline->setTargetFps(targetFps);
|
||||
auto updated = makeMutable<Impl>(impl());
|
||||
updated->timeline = timeline;
|
||||
baseImpl = std::move(updated);
|
||||
@@ -754,6 +791,7 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField(const std::s
|
||||
entries.push_back({std::make_shared<const particle::ParticleFieldSnapshot>(initialT0), initialT0.validTime});
|
||||
entries.push_back({std::make_shared<const particle::ParticleFieldSnapshot>(initialT1), initialT1.validTime});
|
||||
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
|
||||
timeline->setTargetFps(targetFps);
|
||||
auto updated = makeMutable<Impl>(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<uint8_t>(targetFps);
|
||||
}
|
||||
|
||||
bool RasterArraySource::supportsLayerType(const LayerTypeInfo* info) const {
|
||||
if (!info) return false;
|
||||
const std::string type{info->type};
|
||||
|
||||
@@ -613,6 +613,30 @@ TEST(Source, RasterArrayTimelineKeepsCoreOwnedWindowAndPlaybackControls) {
|
||||
EXPECT_EQ(timeline.frame().state, style::RasterArrayPlaybackState::Ended);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline) {
|
||||
std::vector<style::RasterArrayTimelineRuntime::Entry> entries;
|
||||
for (uint64_t index = 0; index < 3; ++index) {
|
||||
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user