fix: share weather cadence across renderers
This commit is contained in:
@@ -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<Entry>);
|
||||
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{};
|
||||
|
||||
@@ -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<ColorStop> 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"); }
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<int>(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&>(*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.
|
||||
|
||||
@@ -37,6 +37,7 @@ private:
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
|
||||
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
|
||||
uint64_t sourceTimelineWindowRevision = 0;
|
||||
uint64_t sourceTimelineCadenceRevision = 0;
|
||||
bool formalSource = false;
|
||||
uint64_t renderLayerInstanceId = 0;
|
||||
};
|
||||
|
||||
@@ -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>(resources);
|
||||
if (!state) {
|
||||
state = std::make_shared<Resources>(); 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<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity;
|
||||
auto& group = static_cast<LayerGroup&>(*layerGroup); group.clearDrawables();
|
||||
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
||||
|
||||
@@ -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<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();
|
||||
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<float, std::milli>(now - lastTick).count();
|
||||
lastTick = now;
|
||||
if (state != RasterArrayPlaybackState::Playing) return frameLocked();
|
||||
if (state != RasterArrayPlaybackState::Playing) return {frameLocked(), true};
|
||||
const auto duration = static_cast<float>(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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user