Files
MapLibreParticleLab/src/mbgl/style/sources/raster_array_source.cpp
OpenAI Codex 49b2e8aebd
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
feat: add runtime particle timeline playback
2026-09-03 21:23:29 +08:00

479 lines
22 KiB
C++

#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/storage/file_source.hpp>
#include <mbgl/storage/resource.hpp>
#include <mbgl/style/source_observer.hpp>
#include <mbgl/style/sources/raster_array_source_impl.hpp>
#include <mbgl/util/async_request.hpp>
#include <mbgl/util/logging.hpp>
#include <atomic>
#include <cmath>
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<Entry> 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<std::mutex> lock(mutex);
return frameLocked();
}
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<std::mutex> 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<float, std::milli>(now - lastTick).count();
lastTick = now;
if (state != RasterArrayPlaybackState::Playing) return frameLocked();
const auto duration = static_cast<float>(entries[t0 + 1].timestamp - entries[t0].timestamp);
mix += elapsed * speed / duration;
while (mix >= 1.0f) {
if (gap || invalid) {
mix = 1.0f;
state = RasterArrayPlaybackState::Waiting;
Log::Warning(Event::Render,
std::string("ParticleTimeline: event=") + (invalid ? "INVALID_WAITING" : "GAP_WAITING") +
" 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");
}
return frameLocked();
}
bool RasterArrayTimelineRuntime::play() {
std::lock_guard<std::mutex> lock(mutex);
if (state == RasterArrayPlaybackState::Waiting || state == RasterArrayPlaybackState::Ended) return false;
state = RasterArrayPlaybackState::Playing;
lastTick = std::chrono::steady_clock::now();
Log::Info(Event::Render, "ParticleTimeline: event=PLAY speed=" + std::to_string(speed));
return true;
}
bool RasterArrayTimelineRuntime::pause() {
std::lock_guard<std::mutex> lock(mutex);
if (state == RasterArrayPlaybackState::Waiting || state == RasterArrayPlaybackState::Ended) return false;
state = RasterArrayPlaybackState::Paused;
Log::Info(Event::Render, "ParticleTimeline: event=PAUSE");
return true;
}
bool RasterArrayTimelineRuntime::setSpeed(const float value) {
if (value != 0.5f && value != 1.0f && value != 2.0f) return false;
std::lock_guard<std::mutex> lock(mutex);
speed = value;
Log::Info(Event::Render, "ParticleTimeline: event=SPEED value=" + std::to_string(speed));
return true;
}
bool RasterArrayTimelineRuntime::seek(const size_t index) {
std::lock_guard<std::mutex> lock(mutex);
if (index >= entries.size()) return false;
if (index + 1 == entries.size()) {
t0 = index - 1;
mix = 1.0f;
state = RasterArrayPlaybackState::Ended;
} else {
moveToLocked(index);
state = RasterArrayPlaybackState::Paused;
}
Log::Info(Event::Render, "ParticleTimeline: event=SEEK index=" + std::to_string(index));
return true;
}
bool RasterArrayTimelineRuntime::step(const int direction) {
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex);
gap = true;
invalid = false;
Log::Info(Event::Render, "ParticleTimeline: event=GAP_INJECTED");
return true;
}
bool RasterArrayTimelineRuntime::injectInvalid() {
std::lock_guard<std::mutex> lock(mutex);
invalid = true;
gap = false;
Log::Info(Event::Render, "ParticleTimeline: event=INVALID_INJECTED");
return true;
}
bool RasterArrayTimelineRuntime::recover() {
std::lock_guard<std::mutex> lock(mutex);
gap = invalid = false;
if (state == RasterArrayPlaybackState::Waiting) state = RasterArrayPlaybackState::Paused;
Log::Info(Event::Render, "ParticleTimeline: event=RECOVERED");
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<RasterArrayRuntimeFieldDescriptor> runtimeFields)
: Source(makeMutable<Impl>(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<const Impl&>(*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<char>(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<char>(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<char>(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>(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>(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<RasterArrayTimelineRuntime::Entry> entries;
entries.reserve(runtimeFields.size() + 2);
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(*impl().field), 0});
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(*impl().nextField), 4000});
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<particle::ParticleFieldSnapshot>(std::move(field)),
8000 + static_cast<uint64_t>(index) * 4000});
}
try {
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
auto updated = makeMutable<Impl>(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()));
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>(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<size_t>(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<Source::Impl> RasterArraySource::createMutable() const noexcept {
return staticMutableCast<Source::Impl>(makeMutable<Impl>(impl()));
}
} // namespace mbgl::style