From ab86eb8f6780f2a77c3772e695650f46fc0b6608 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 7 Sep 2026 15:16:11 +0800 Subject: [PATCH] fix: share weather cadence across renderers --- .../style/sources/raster_array_source.hpp | 10 ++++++ .../maplibre/android/maps/WeatherRuntime.java | 35 ++++++++++++------- .../android/maps/WeatherRuntimeStateTest.java | 26 ++++++++++++++ .../ParticleCoreLayerValidationActivity.kt | 6 +++- .../render_particle_validation_layer.cpp | 31 +++++----------- .../layers/render_raster_particle_layer.hpp | 1 + .../layers/render_weather_color_layer.cpp | 18 +++++++--- .../style/sources/raster_array_source.cpp | 22 +++++++----- test/style/source.test.cpp | 32 ++++++++++++----- 9 files changed, 124 insertions(+), 57 deletions(-) diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index 665fe52df..ba21c6d6e 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -50,9 +50,15 @@ struct RasterArrayTimelineFrame { size_t t0Index = 0; size_t t1Index = 0; uint64_t windowRevision = 0; + uint64_t cadenceRevision = 0; RasterArrayPlaybackState state = RasterArrayPlaybackState::Ready; }; +struct RasterArrayCadenceAdvance { + RasterArrayTimelineFrame frame; + bool advanced = false; +}; + // Source-owned playback state. Android only sends low-frequency commands; // render frames obtain an immutable T0/T1 pair and scalar mix from Core. class RasterArrayTimelineRuntime { @@ -64,6 +70,9 @@ public: explicit RasterArrayTimelineRuntime(std::vector); bool append(Entry); + // Atomically claims one shared cadence deadline and advances wall-time + // playback at most once. Either weather renderer may consume it. + RasterArrayCadenceAdvance advanceIfDue(std::chrono::steady_clock::time_point); RasterArrayTimelineFrame tick(); bool play(); bool pause(); @@ -97,6 +106,7 @@ private: bool invalid = false; RasterArrayPlaybackState stateBeforeWait = RasterArrayPlaybackState::Paused; uint64_t windowRevision = 1; + uint64_t cadenceRevision = 0; std::chrono::steady_clock::time_point lastTick{}; std::chrono::steady_clock::time_point lastCadenceTick{}; std::chrono::steady_clock::time_point lastCadenceLog{}; 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 f489712e6..d89e0c581 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 @@ -50,13 +50,10 @@ public final class WeatherRuntime { private boolean particleVisible = true; private PlaybackState playbackState = PlaybackState.PAUSE; private TargetFps targetFps = TargetFps.AUTO; + private boolean disposed; private Handler cadenceHandler; private final Runnable cadenceWake = new Runnable() { - @Override public void run() { - if (!shouldWakeWeather()) return; - map.triggerRepaint(); - scheduleCadenceWake(); - } + @Override public void run() { runCadenceWake(); } }; public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) { @@ -64,10 +61,14 @@ public final class WeatherRuntime { } public State getState() { return state; } public Config getConfig() { return config; } + public boolean isDisposed() { return disposed; } + /** Ends this map/style-scoped facade. It is safe to call repeatedly. */ + public void dispose() { if (disposed) return; disposed = true; cancelCadenceWake(); } /** 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) { + requireAttached(); requireEnabled(); if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER; FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes)); @@ -75,8 +76,9 @@ public final class WeatherRuntime { 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); if (ok) cancelCadenceWake(); return result(ok); } + public CommandResult clearWindFields() { if (disposed || 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 (disposed) return CommandResult.REJECTED; if (!enabled) { if (state == State.DISABLED) return CommandResult.ACCEPTED; map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f); @@ -94,12 +96,12 @@ public final class WeatherRuntime { state = stateAfterEnable(state, color && particle); return result(color && particle); } - 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 setWeatherColorVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; } + public CommandResult setParticleVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; particleVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); 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(); CommandResult result = result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); if (result == CommandResult.ACCEPTED) { playbackState = playback; scheduleCadenceWake(); } return result; } + 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; cancelCadenceWake(); 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 setWeatherColorOpacity(float opacity) { return disposed ? CommandResult.REJECTED : result(Float.isFinite(opacity) && map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-opacity", opacity)); } 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); } @@ -109,17 +111,21 @@ public final class WeatherRuntime { public CommandResult setParticleSpeedColorEnabled(boolean enabled) { return setParticleFloat("raster-particle-speed-color-enabled", enabled ? 1f : 0f); } /** Sets only the particle RGBA; trail color and WeatherColor ramp are unchanged. */ public CommandResult setParticleColor(float red, float green, float blue, float alpha) { + if (disposed) return CommandResult.REJECTED; return result(map.setRasterParticleColor(config.particleLayerId, new float[]{red, green, blue, alpha})); } /** Sets only the trail RGBA; particle color and WeatherColor ramp are unchanged. */ public CommandResult setTrailColor(float red, float green, float blue, float alpha) { + if (disposed) return CommandResult.REJECTED; return result(map.setRasterParticleTrailColor(config.particleLayerId, new float[]{red, green, blue, alpha})); } /** Compatibility helper for existing callers; new Host code should use the independent setters. */ public CommandResult setParticleColors(float red, float green, float blue, float alpha, float trailRed, float trailGreen, float trailBlue, float trailAlpha) { + if (disposed) return CommandResult.REJECTED; 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 (disposed) return CommandResult.REJECTED; if (!map.setRasterArraySourceTargetFps(config.sourceId, target.value)) return CommandResult.REJECTED; targetFps = target; cancelCadenceWake(); @@ -128,6 +134,7 @@ public final class WeatherRuntime { } public TargetFps getTargetFps() { return targetFps; } public CommandResult setWeatherColorStops(@NonNull List stops) { + if (disposed) 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]; 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; } @@ -135,7 +142,7 @@ public final class WeatherRuntime { } 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)); + return !disposed && targetFps != TargetFps.AUTO && state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY)); } private void scheduleCadenceWake() { if (!shouldWakeWeather()) return; @@ -148,7 +155,8 @@ public final class WeatherRuntime { } } 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)); } + void runCadenceWake() { if (!shouldWakeWeather()) return; map.triggerRepaint(); scheduleCadenceWake(); } + private CommandResult setParticleFloat(String property, float value) { return disposed ? CommandResult.REJECTED : 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; @@ -162,5 +170,6 @@ public final class WeatherRuntime { if (current == State.DISABLED) return State.WAITING_FOR_FIELDS; return current; } - private void requireEnabled() { if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); } + private void requireAttached() { if (disposed) throw new IllegalStateException("WeatherRuntime is disposed"); } + private void requireEnabled() { requireAttached(); if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); } } 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 422147112..9655c521e 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 @@ -102,4 +102,30 @@ public class WeatherRuntimeStateTest { assertEquals(WeatherRuntime.TargetFps.FPS_30, runtime.getTargetFps()); Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class)); } + + @Test public void disposeIsIdempotentAndRejectsFurtherControls() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + runtime.dispose(); + runtime.dispose(); + assertEquals(true, runtime.isDisposed()); + Mockito.clearInvocations(map); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTargetFps(WeatherRuntime.TargetFps.FPS_30)); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherColorVisible(false)); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleVisible(false)); + assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherEnabled(false)); + Mockito.verifyNoInteractions(map); + } + + @Test public void disposedWakeCannotRepaintOrRescheduleOldMap() { + MapLibreMap map = Mockito.mock(MapLibreMap.class); + Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8); + WeatherRuntime oldRuntime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + oldRuntime.dispose(); + oldRuntime.runCadenceWake(); + Mockito.verify(map, Mockito.never()).triggerRepaint(); + WeatherRuntime replacement = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle")); + assertEquals(false, replacement.isDisposed()); + } } 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 4e2fb9f27..6fd797c98 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 @@ -256,6 +256,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private fun leaveRealWeatherMode() { dismissRealWeatherTuning() + weatherRuntime?.dispose() + weatherRuntime = null if (realWeatherMode) { preRealWeatherVisuals?.let { visuals -> particleCount = visuals.count; lineWidth = visuals.width; particleLength = visuals.length @@ -438,6 +440,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") { + weatherRuntime?.dispose() + weatherRuntime = null this.active = active this.enabled = enabled val state = statusText() @@ -806,7 +810,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { override fun onResume() { super.onResume(); mapView.onResume() } override fun onPause() { mapView.onPause(); super.onPause() } override fun onStop() { mapView.onStop(); super.onStop() } - override fun onDestroy() { mapView.onDestroy(); super.onDestroy() } + override fun onDestroy() { weatherRuntime?.dispose(); weatherRuntime = null; mapView.onDestroy(); super.onDestroy() } override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index cb7f0b61b..d44737350 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -495,26 +495,6 @@ public: return; } - // update() is not called for every drawable frame. State executes once - // per particle simulation frame, making it the appropriate existing - // Core render lifecycle for the Source-owned monotonic playback clock. - if (pass == ValidationPass::State && resources->timeline) { - const auto frame = resources->timeline->tick(); - resources->timeMix = frame.mix; - const auto now = std::chrono::steady_clock::now(); - if (now - resources->lastPlaybackHeartbeat >= std::chrono::seconds(1)) { - resources->lastPlaybackHeartbeat = now; - Log::Info( - Event::Render, - "ParticleTimeline: event=PLAYBACK_TICK state=" + std::to_string(static_cast(frame.state)) + - " speed=" + std::to_string(resources->timeline->playbackSpeed()) + - " cursor=" + std::to_string(frame.t0Index) + " mix=" + std::to_string(frame.mix) + - " window=" + std::to_string(frame.t0Index) + "/" + std::to_string(frame.t1Index) + - " fieldUploadCount=" + std::to_string(resources->fieldUploadCount) + - " simulationResetCount=" + std::to_string(resources->resetCount)); - } - } - if (pass == ValidationPass::State) { const auto now = std::chrono::steady_clock::now(); if (now - resources->lastMotionLog >= std::chrono::seconds(1)) { @@ -1105,9 +1085,14 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const uint8_t fixtureMode = properties.fixtureMode; const uint32_t particleCount = properties.particleCount; float timelineMix = properties.timeMix; + bool cadenceAdvanced = true; + uint64_t cadenceRevision = sourceTimelineCadenceRevision; if (formalSource && sourceTimeline) { - const auto frame = sourceTimeline->tick(); + const auto advance = sourceTimeline->advanceIfDue(std::chrono::steady_clock::now()); + const auto& frame = advance.frame; timelineMix = frame.mix; + cadenceRevision = frame.cadenceRevision; + cadenceAdvanced = frame.cadenceRevision != sourceTimelineCadenceRevision; if (frame.windowRevision != sourceTimelineWindowRevision) { sourceField = *frame.t0; sourceNextField = *frame.t1; @@ -1741,7 +1726,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, // 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())) { + if (formalSource && sourceTimeline && !cadenceAdvanced) { auto& compositeGroup = static_cast(*layerGroup); compositeGroup.clearDrawables(); const auto center = transformState.getLatLng(LatLng::Unwrapped); @@ -1848,6 +1833,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, return; } + if (formalSource && sourceTimeline) sourceTimelineCadenceRevision = cadenceRevision; + // Read targets are registered only for their one-time clear. Normal frames register // only the opposite write targets, in dependency order, so a sampled texture can // never be attached to the active render pass. diff --git a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp index 969c077ec..39c520ffa 100644 --- a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp +++ b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp @@ -37,6 +37,7 @@ private: std::optional sourceNextField; std::shared_ptr sourceTimeline; uint64_t sourceTimelineWindowRevision = 0; + uint64_t sourceTimelineCadenceRevision = 0; bool formalSource = false; uint64_t renderLayerInstanceId = 0; }; diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index 2f0648174..26a7d2a11 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -138,8 +138,10 @@ void RenderWeatherColorLayer::evaluate(const PropertyEvaluationParameters&) { evaluatedProperties = std::move(properties); } bool RenderWeatherColorLayer::needsRepaint() const { - return isLayerRenderable() && sourceField && sourceNextField && - (!sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now())); + if (!isLayerRenderable() || !sourceField || !sourceNextField) return false; + if (!sourceTimeline) return true; + return sourceTimeline->frame().state == style::RasterArrayPlaybackState::Playing && + sourceTimeline->cadenceDue(std::chrono::steady_clock::now()); } bool RenderWeatherColorLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const { if (!resources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false; @@ -172,7 +174,15 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& return; } if (!layerGroup || !isLayerRenderable()) return; - if (sourceTimeline) { const auto frame = sourceTimeline->frame(); sourceField = *frame.t0; sourceNextField = *frame.t1; } + style::RasterArrayTimelineFrame timelineFrame; + if (sourceTimeline) { + timelineFrame = sourceTimeline->frame(); + if (timelineFrame.state == style::RasterArrayPlaybackState::Playing) { + timelineFrame = sourceTimeline->advanceIfDue(std::chrono::steady_clock::now()).frame; + } + sourceField = *timelineFrame.t0; + sourceNextField = *timelineFrame.t1; + } auto state = std::static_pointer_cast(resources); if (!state) { state = std::make_shared(); state->shader = context.getGenericShader(shaders, ShaderName); @@ -197,7 +207,7 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& state->texture0 = std::move(x0); state->texture1 = std::move(x1); state->scale0 = s0; state->scale1 = s1; Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID); } - const float mix = sourceTimeline ? sourceTimeline->frame().mix : 0.0f; + const float mix = sourceTimeline ? timelineFrame.mix : 0.0f; const float opacity = static_cast(*baseImpl).opacity; auto& group = static_cast(*layerGroup); group.clearDrawables(); const auto center = transformState.getLatLng(LatLng::Unwrapped); diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index c83201295..8b6812141 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -55,7 +55,7 @@ bool RasterArrayTimelineRuntime::append(Entry entry) { } RasterArrayTimelineFrame RasterArrayTimelineRuntime::frameLocked() const { - return {entries[t0].field, entries[t0 + 1].field, mix, t0, t0 + 1, windowRevision, state}; + return {entries[t0].field, entries[t0 + 1].field, mix, t0, t0 + 1, windowRevision, cadenceRevision, state}; } RasterArrayTimelineFrame RasterArrayTimelineRuntime::frame() const { @@ -86,14 +86,14 @@ bool RasterArrayTimelineRuntime::moveToLocked(const size_t nextT0) { return true; } -RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { +RasterArrayCadenceAdvance RasterArrayTimelineRuntime::advanceIfDue(const std::chrono::steady_clock::time_point now) { 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(); + if (now - lastCadenceTick < interval) return {frameLocked(), false}; } lastCadenceTick = now; + ++cadenceRevision; ++cadenceTickCount; if (lastCadenceLog.time_since_epoch().count() == 0 || now - lastCadenceLog >= std::chrono::seconds(1)) { lastCadenceLog = now; @@ -102,11 +102,11 @@ RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { } if (lastTick == std::chrono::steady_clock::time_point{}) { lastTick = now; - return frameLocked(); + return {frameLocked(), true}; } const auto elapsed = std::chrono::duration(now - lastTick).count(); lastTick = now; - if (state != RasterArrayPlaybackState::Playing) return frameLocked(); + if (state != RasterArrayPlaybackState::Playing) return {frameLocked(), true}; const auto duration = static_cast(entries[t0 + 1].timestamp - entries[t0].timestamp); mix += elapsed * speed / duration; while (mix >= 1.0f) { @@ -117,13 +117,13 @@ RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { Log::Warning(Event::Render, std::string("ParticleTimeline: event=TIMELINE_REJECTED reason=") + (invalid ? "INVALID_NEXT" : "GAP_HOLD") + " t1=" + entries[t0 + 1].field->fieldID); - return frameLocked(); + return {frameLocked(), true}; } if (!moveToLocked(t0 + 1)) { mix = 1.0f; state = RasterArrayPlaybackState::Ended; Log::Info(Event::Render, "ParticleTimeline: event=END t1=" + entries[t0 + 1].field->fieldID); - return frameLocked(); + return {frameLocked(), true}; } Log::Info(Event::Render, "ParticleTimeline: event=WINDOW_BOUNDARY oldT1=" + entries[t0].field->fieldID + @@ -132,7 +132,11 @@ RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { "ParticleTimeline: event=TIMELINE_CACHE_HIT index=" + std::to_string(t0 + 1) + " cacheSize=" + std::to_string(entries.size()) + " capacity=" + std::to_string(cacheCapacity_)); } - return frameLocked(); + return {frameLocked(), true}; +} + +RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { + return advanceIfDue(std::chrono::steady_clock::now()).frame; } bool RasterArrayTimelineRuntime::setTargetFps(const RasterArrayTargetFps target) { diff --git a/test/style/source.test.cpp b/test/style/source.test.cpp index 42cf81284..0290ef311 100644 --- a/test/style/source.test.cpp +++ b/test/style/source.test.cpp @@ -621,18 +621,34 @@ TEST(Source, RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline) { } 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); + const auto weatherColorAdvance = timeline.advanceIfDue(start); + EXPECT_TRUE(weatherColorAdvance.advanced); + const auto particleSameDeadline = timeline.advanceIfDue(start); + EXPECT_FALSE(particleSameDeadline.advanced); + EXPECT_EQ(particleSameDeadline.frame.cadenceRevision, weatherColorAdvance.frame.cadenceRevision); EXPECT_FALSE(timeline.cadenceDue(start + std::chrono::milliseconds(33))); EXPECT_TRUE(timeline.cadenceDue(start + std::chrono::milliseconds(34))); + const auto particleNextDeadline = timeline.advanceIfDue(start + std::chrono::milliseconds(34)); + EXPECT_TRUE(particleNextDeadline.advanced); + const auto weatherColorLateSameDeadline = timeline.advanceIfDue(start + std::chrono::milliseconds(34)); + EXPECT_FALSE(weatherColorLateSameDeadline.advanced); + EXPECT_TRUE(timeline.pause()); + const auto pausedColorFrame = timeline.frame(); + EXPECT_EQ(pausedColorFrame.state, style::RasterArrayPlaybackState::Paused); + // A paused WeatherColor-only layer asks no continuous repaint; if another + // map frame arrives, one shared advance is harmless and does not move MIX. + const auto pausedAdvance = timeline.advanceIfDue(start + std::chrono::milliseconds(68)); + EXPECT_TRUE(pausedAdvance.advanced); + EXPECT_FLOAT_EQ(pausedAdvance.frame.mix, pausedColorFrame.mix); 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))); + const auto afterSwitch = timeline.advanceIfDue(start + std::chrono::milliseconds(68)); + EXPECT_TRUE(afterSwitch.advanced); + EXPECT_FALSE(timeline.advanceIfDue(start + std::chrono::milliseconds(84)).advanced); + EXPECT_TRUE(timeline.advanceIfDue(start + std::chrono::milliseconds(85)).advanced); + const auto missedDeadline = timeline.advanceIfDue(start + std::chrono::seconds(5)); + EXPECT_TRUE(missedDeadline.advanced); + EXPECT_FALSE(timeline.advanceIfDue(start + std::chrono::seconds(5)).advanced); EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Auto)); EXPECT_TRUE(timeline.cadenceDue(start)); }