#include #include #include #include #include #include #include #include #include #include namespace mbgl::style { namespace { // Snapshot revision is process-wide rather than source-instance local: a // Style recreation can replace an east source with a north source under the // same source ID, and that must always invalidate the prior GPU field. std::atomic_uint64_t nextRasterArrayFieldGeneration{0}; } // namespace RasterArrayTimelineRuntime::RasterArrayTimelineRuntime(std::vector entries_) : entries(std::move(entries_)) { if (entries.size() < 2) { throw std::invalid_argument("particle timeline requires at least two snapshots"); } for (size_t index = 1; index < entries.size(); ++index) { if (entries[index - 1].timestamp >= entries[index].timestamp) { throw std::invalid_argument("particle timeline timestamps must be strictly increasing"); } } if (entries.size() > cacheCapacity_) { throw std::invalid_argument("particle timeline exceeds bounded validation cache"); } } RasterArrayTimelineFrame RasterArrayTimelineRuntime::frameLocked() const { return {entries[t0].field, entries[t0 + 1].field, mix, t0, t0 + 1, windowRevision, state}; } RasterArrayTimelineFrame RasterArrayTimelineRuntime::frame() const { std::lock_guard lock(mutex); return frameLocked(); } float RasterArrayTimelineRuntime::playbackSpeed() const { std::lock_guard lock(mutex); return speed; } bool RasterArrayTimelineRuntime::moveToLocked(const size_t nextT0) { if (nextT0 + 1 >= entries.size()) return false; t0 = nextT0; mix = 0.0f; ++windowRevision; return true; } RasterArrayTimelineFrame RasterArrayTimelineRuntime::tick() { std::lock_guard lock(mutex); const auto now = std::chrono::steady_clock::now(); if (lastTick == std::chrono::steady_clock::time_point{}) { lastTick = now; return frameLocked(); } const auto elapsed = std::chrono::duration(now - lastTick).count(); lastTick = now; if (state != RasterArrayPlaybackState::Playing) return frameLocked(); const auto duration = static_cast(entries[t0 + 1].timestamp - entries[t0].timestamp); mix += elapsed * speed / duration; while (mix >= 1.0f) { if (gap || invalid) { mix = 1.0f; stateBeforeWait = state; state = RasterArrayPlaybackState::Waiting; Log::Warning(Event::Render, std::string("ParticleTimeline: event=TIMELINE_REJECTED reason=") + (invalid ? "INVALID_NEXT" : "GAP_HOLD") + " t1=" + entries[t0 + 1].field->fixtureID); return frameLocked(); } if (!moveToLocked(t0 + 1)) { mix = 1.0f; state = RasterArrayPlaybackState::Ended; Log::Info(Event::Render, "ParticleTimeline: event=END t1=" + entries[t0 + 1].field->fixtureID); return frameLocked(); } Log::Info(Event::Render, "ParticleTimeline: event=WINDOW_BOUNDARY oldT1=" + entries[t0].field->fixtureID + " newT1=" + entries[t0 + 1].field->fixtureID + " result=APPLIED reuse=OLD_T1_TO_NEW_T0"); Log::Info(Event::Render, "ParticleTimeline: event=TIMELINE_CACHE_HIT index=" + std::to_string(t0 + 1) + " cacheSize=" + std::to_string(entries.size()) + " capacity=" + std::to_string(cacheCapacity_)); } return frameLocked(); } bool RasterArrayTimelineRuntime::play() { std::lock_guard lock(mutex); if (state == RasterArrayPlaybackState::Waiting) { Log::Warning(Event::Render, "ParticleTimeline: event=PLAYBACK_REJECTED reason=WAITING"); return false; } if (state == RasterArrayPlaybackState::Ended) { t0 = 0; mix = 0.0f; ++windowRevision; Log::Info(Event::Render, "ParticleTimeline: event=PLAYBACK_PLAY reason=RESTART_FROM_END"); } state = RasterArrayPlaybackState::Playing; lastTick = std::chrono::steady_clock::now(); Log::Info(Event::Render, "ParticleTimeline: event=PLAYBACK_PLAY speed=" + std::to_string(speed) + " cursor=" + std::to_string(t0)); return true; } bool RasterArrayTimelineRuntime::pause() { std::lock_guard lock(mutex); if (state == RasterArrayPlaybackState::Waiting || state == RasterArrayPlaybackState::Ended) { Log::Warning(Event::Render, "ParticleTimeline: event=PLAYBACK_REJECTED reason=ENDED_OR_WAITING"); return false; } state = RasterArrayPlaybackState::Paused; Log::Info(Event::Render, "ParticleTimeline: event=PLAYBACK_PAUSE cursor=" + std::to_string(t0)); return true; } bool RasterArrayTimelineRuntime::setSpeed(const float value) { if (value != 0.5f && value != 1.0f && value != 2.0f) return false; std::lock_guard lock(mutex); const auto oldSpeed = speed; speed = value; Log::Info(Event::Render, "ParticleTimeline: event=PLAYBACK_SPEED oldSpeed=" + std::to_string(oldSpeed) + " newSpeed=" + std::to_string(speed) + " cursor=" + std::to_string(t0)); return true; } bool RasterArrayTimelineRuntime::seek(const size_t index) { std::lock_guard lock(mutex); if (index >= entries.size()) return false; const auto fromState = state; const auto fromCursor = t0; if (index + 1 == entries.size()) { t0 = index - 1; mix = 1.0f; state = RasterArrayPlaybackState::Ended; } else { moveToLocked(index); state = fromState == RasterArrayPlaybackState::Playing ? RasterArrayPlaybackState::Playing : RasterArrayPlaybackState::Paused; } Log::Info(Event::Render, "ParticleTimeline: event=PLAYBACK_SEEK fromState=" + std::to_string(static_cast(fromState)) + " toState=" + std::to_string(static_cast(state)) + " fromCursor=" + std::to_string(fromCursor) + " toCursor=" + std::to_string(t0) + " targetIndex=" + std::to_string(index) + " t0Index=" + std::to_string(t0) + " t1Index=" + std::to_string(t0 + 1) + " mix=" + std::to_string(mix)); return true; } bool RasterArrayTimelineRuntime::step(const int direction) { std::lock_guard lock(mutex); const auto target = direction < 0 ? (t0 == 0 ? 0 : t0 - 1) : std::min(t0 + 1, entries.size() - 2); moveToLocked(target); state = RasterArrayPlaybackState::Paused; Log::Info(Event::Render, "ParticleTimeline: event=STEP direction=" + std::to_string(direction)); return true; } bool RasterArrayTimelineRuntime::injectGap() { std::lock_guard lock(mutex); gap = true; invalid = false; Log::Info(Event::Render, "ParticleTimeline: event=GAP_INJECTED"); return true; } bool RasterArrayTimelineRuntime::injectInvalid() { std::lock_guard lock(mutex); invalid = true; gap = false; Log::Info(Event::Render, "ParticleTimeline: event=INVALID_INJECTED"); return true; } bool RasterArrayTimelineRuntime::recover() { std::lock_guard lock(mutex); gap = invalid = false; if (state == RasterArrayPlaybackState::Waiting) state = stateBeforeWait; Log::Info(Event::Render, "ParticleTimeline: event=RECOVERED state=" + std::to_string(static_cast(state))); return true; } RasterArraySource::RasterArraySource(std::string id, std::string fixtureID, std::string metadataURL, std::string fieldURL, std::string nextFixtureID, std::string nextMetadataURL, std::string nextFieldURL, std::vector runtimeFields) : Source(makeMutable(std::move(id), std::move(fixtureID), std::move(metadataURL), std::move(fieldURL), std::move(nextFixtureID), std::move(nextMetadataURL), std::move(nextFieldURL), std::move(runtimeFields))) {} RasterArraySource::~RasterArraySource() { weakFactory.invalidateWeakPtrs(); } const RasterArraySource::Impl& RasterArraySource::impl() const { return static_cast(*baseImpl); } void RasterArraySource::loadDescription(FileSource& fileSource) { if (metadataRequest || fieldRequest) return; metadataRequest = fileSource.request( Resource::source(impl().metadataURL), [this, self = makeWeakPtr()](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle metadata request failed"))); return; } metadata = *response.data; tryLoad(); } }); fieldRequest = fileSource.request( Resource::source(impl().fieldURL), [this, self = makeWeakPtr()](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle field request failed"))); return; } fieldBytes = std::vector(response.data->begin(), response.data->end()); tryLoad(); } }); if (!impl().nextFixtureID.empty()) { if (impl().nextMetadataURL.empty() || impl().nextFieldURL.empty()) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle next fixture requires metadata and field URLs"))); return; } nextMetadataRequest = fileSource.request( Resource::source(impl().nextMetadataURL), [this, self = makeWeakPtr()](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle next metadata request failed"))); return; } nextMetadata = *response.data; tryLoad(); } }); nextFieldRequest = fileSource.request( Resource::source(impl().nextFieldURL), [this, self = makeWeakPtr()](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle next field request failed"))); return; } nextFieldBytes = std::vector(response.data->begin(), response.data->end()); tryLoad(); } }); } runtimeFields.reserve(impl().runtimeFields.size()); for (const auto& descriptor : impl().runtimeFields) { RuntimeFieldCandidate runtimeCandidate; runtimeCandidate.descriptor = descriptor; runtimeFields.push_back(std::move(runtimeCandidate)); const auto index = runtimeFields.size() - 1; auto& candidate = runtimeFields.back(); candidate.metadataRequest = fileSource.request( Resource::source(candidate.descriptor.metadataURL), [this, self = makeWeakPtr(), index](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle runtime metadata request failed"))); return; } runtimeFields[index].metadata = *response.data; tryLoadRuntimeField(index); } }); candidate.fieldRequest = fileSource.request( Resource::source(candidate.descriptor.fieldURL), [this, self = makeWeakPtr(), index](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { observer->onSourceError( *this, std::make_exception_ptr(std::runtime_error("particle runtime field request failed"))); return; } runtimeFields[index].fieldBytes = std::vector(response.data->begin(), response.data->end()); tryLoadRuntimeField(index); } }); } } void RasterArraySource::tryLoad() { if (!metadata || !fieldBytes || (!impl().nextFixtureID.empty() && (!nextMetadata || !nextFieldBytes))) return; try { const auto fieldGeneration = ++nextRasterArrayFieldGeneration; auto field = particle::OfflineFixtureParticleAdapter::load( impl().fixtureID, *metadata, *fieldBytes, fieldGeneration); if (impl().nextFixtureID.empty()) { baseImpl = makeMutable(impl(), std::move(field)); } else { auto nextField = particle::OfflineFixtureParticleAdapter::load( impl().nextFixtureID, *nextMetadata, *nextFieldBytes, ++nextRasterArrayFieldGeneration); if (!field.isTimeInterpolationCompatibleWith(nextField)) { throw std::invalid_argument("particle time-window fixtures are incompatible"); } baseImpl = makeMutable(impl(), std::move(field), std::move(nextField)); } loaded = true; const auto& loadedField = *impl().field; Log::Info(Event::Render, "ParticleField: source loaded fixtureId=" + impl().fixtureID + " metadata=" + impl().metadataURL + " field=" + impl().fieldURL + " generation=" + std::to_string(loadedField.generation) + " dataVersion=" + std::to_string(loadedField.dataVersion)); if (impl().nextField) { Log::Info(Event::Render, "ParticleField: source loaded next fixtureId=" + impl().nextField->fixtureID + " generation=" + std::to_string(impl().nextField->generation) + " dataVersion=" + std::to_string(impl().nextField->dataVersion)); } observer->onSourceLoaded(*this); observer->onSourceChanged(*this); tryInitializeTimeline(); } catch (const std::exception& error) { observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what()))); } } void RasterArraySource::tryLoadRuntimeField(const size_t index) { auto& candidate = runtimeFields.at(index); if (candidate.snapshot || !candidate.metadata || !candidate.fieldBytes) return; try { candidate.snapshot = particle::OfflineFixtureParticleAdapter::load( candidate.descriptor.fixtureID, *candidate.metadata, *candidate.fieldBytes, 0); Log::Info(Event::Render, "ParticleField: runtime candidate ready fixtureId=" + candidate.snapshot->fixtureID + " dataVersion=" + std::to_string(candidate.snapshot->dataVersion)); tryInitializeTimeline(); } catch (const std::exception& error) { observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what()))); } } void RasterArraySource::tryInitializeTimeline() { if (timeline || !loaded || !impl().field || !impl().nextField || runtimeFields.empty()) return; std::vector entries; entries.reserve(runtimeFields.size() + 2); entries.push_back({std::make_shared(*impl().field), 0}); entries.push_back({std::make_shared(*impl().nextField), 40000}); for (size_t index = 0; index < runtimeFields.size(); ++index) { if (!runtimeFields[index].snapshot) return; auto field = *runtimeFields[index].snapshot; field.generation = ++nextRasterArrayFieldGeneration; entries.push_back({std::make_shared(std::move(field)), 80000 + static_cast(index) * 40000}); } try { timeline = std::make_shared(std::move(entries)); auto updated = makeMutable(impl()); updated->timeline = timeline; baseImpl = std::move(updated); Log::Info(Event::Render, "ParticleTimeline: event=READY snapshots=" + std::to_string(runtimeFields.size() + 2) + " cacheCapacity=" + std::to_string(timeline->cacheCapacity())); for (size_t index = 0; index < runtimeFields.size() + 2; ++index) { Log::Info(Event::Render, "ParticleTimeline: event=TIMELINE_PRELOAD timelineId=" + getID() + " index=" + std::to_string(index) + " cacheSize=" + std::to_string(runtimeFields.size() + 2) + " capacity=" + std::to_string(timeline->cacheCapacity())); } observer->onSourceChanged(*this); } catch (const std::exception& error) { observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what()))); } } RasterArrayRuntimeFieldUpdateResult RasterArraySource::rejectRuntimeFieldUpdate( const RasterArrayRuntimeFieldUpdateResult result, const std::string& reason) const { Log::Warning(Event::Render, "ParticleField: event=FIELD_WINDOW_UPDATE_REJECTED reason=" + reason + " sourceId=" + getID()); return result; } RasterArrayRuntimeFieldUpdateResult RasterArraySource::updateRuntimeFieldWindow(particle::ParticleFieldSnapshot newT1) { if (!loaded || !impl().field || !impl().nextField) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedNotLoaded, "SOURCE_NOT_LOADED"); } if (!newT1.isValid() || newT1.generation == 0) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedInvalidSnapshot, "INVALID_SNAPSHOT"); } if (!impl().nextField->isTimeInterpolationCompatibleWith(newT1)) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedIncompatibleSnapshot, "INCOMPATIBLE_SNAPSHOT"); } if (particle::sameFieldGpuContent(*impl().nextField, newT1)) { Log::Info(Event::Render, "ParticleField: event=FIELD_WINDOW_UPDATE_APPLIED result=NOOP sourceId=" + getID() + " T0=" + impl().field->fixtureID + " T1=" + impl().nextField->fixtureID); return RasterArrayRuntimeFieldUpdateResult::Noop; } if (newT1.generation <= impl().nextField->generation) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedNonMonotonicGeneration, "NON_MONOTONIC_GENERATION"); } const auto oldT0 = impl().field->fixtureID; const auto oldT1 = impl().nextField->fixtureID; Log::Info(Event::Render, "ParticleField: event=FIELD_WINDOW_UPDATE_REQUEST sourceId=" + getID() + " sequence=" + std::to_string(runtimeUpdateSequence + 1) + " oldT0=" + oldT0 + " oldT1=" + oldT1 + " newT1=" + newT1.fixtureID + " generation=" + std::to_string(newT1.generation)); auto newT0 = *impl().nextField; baseImpl = makeMutable(impl(), std::move(newT0), std::move(newT1)); ++runtimeUpdateSequence; observer->onSourceChanged(*this); Log::Info(Event::Render, "ParticleField: event=FIELD_WINDOW_UPDATE_APPLIED result=APPLIED sourceId=" + getID() + " sequence=" + std::to_string(runtimeUpdateSequence) + " newT0=" + impl().field->fixtureID + " newT1=" + impl().nextField->fixtureID + " generation=" + std::to_string(impl().nextField->generation)); return RasterArrayRuntimeFieldUpdateResult::Applied; } RasterArrayRuntimeFieldUpdateResult RasterArraySource::advanceRuntimeFieldWindow() { if (timeline) { return timeline->step(1) ? RasterArrayRuntimeFieldUpdateResult::Applied : RasterArrayRuntimeFieldUpdateResult::RejectedNoCandidate; } if (nextRuntimeField >= runtimeFields.size()) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedNoCandidate, "NO_PRELOADED_FIELD"); } auto& candidate = runtimeFields[nextRuntimeField]; if (!candidate.snapshot) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedCandidateNotReady, "PRELOADED_FIELD_NOT_READY"); } // A preloaded candidate is immutable content, not an active runtime // revision. Allocate its revision when it enters the authoritative window // so concurrent asset completion order cannot make it stale. auto newT1 = *candidate.snapshot; newT1.generation = ++nextRasterArrayFieldGeneration; const auto result = updateRuntimeFieldWindow(std::move(newT1)); if (result == RasterArrayRuntimeFieldUpdateResult::Applied || result == RasterArrayRuntimeFieldUpdateResult::Noop) { ++nextRuntimeField; } return result; } RasterArrayRuntimeFieldUpdateResult RasterArraySource::repeatRuntimeFieldWindow() { if (timeline) { Log::Info(Event::Render, "ParticleField: event=FIELD_WINDOW_UPDATE_APPLIED result=NOOP sourceId=" + getID()); return RasterArrayRuntimeFieldUpdateResult::Noop; } if (!loaded || !impl().nextField) { return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedNotLoaded, "SOURCE_NOT_LOADED"); } return updateRuntimeFieldWindow(*impl().nextField); } bool RasterArraySource::controlRuntimeTimeline(const int command, const float value) { if (!timeline) { Log::Warning(Event::Render, "ParticleTimeline: event=REJECTED reason=TIMELINE_NOT_READY sourceId=" + getID()); return false; } switch (command) { case 0: return timeline->play(); case 1: return timeline->pause(); case 2: return timeline->setSpeed(value); case 3: return timeline->step(-1); case 4: return timeline->step(1); case 5: return timeline->seek(static_cast(value)); case 6: return timeline->injectGap(); case 7: return timeline->injectInvalid(); case 8: return timeline->recover(); default: return false; } } bool RasterArraySource::supportsLayerType(const LayerTypeInfo* info) const { return info && std::string(info->type) == "raster-particle"; } Mutable RasterArraySource::createMutable() const noexcept { return staticMutableCast(makeMutable(impl())); } } // namespace mbgl::style