feat: add runtime particle timeline playback
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
#include <mbgl/util/logging.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
|
||||
namespace mbgl::style {
|
||||
|
||||
@@ -21,6 +22,138 @@ 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,
|
||||
@@ -173,6 +306,7 @@ void RasterArraySource::tryLoad() {
|
||||
}
|
||||
observer->onSourceLoaded(*this);
|
||||
observer->onSourceChanged(*this);
|
||||
tryInitializeTimeline();
|
||||
} catch (const std::exception& error) {
|
||||
observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what())));
|
||||
}
|
||||
@@ -187,6 +321,34 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) {
|
||||
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())));
|
||||
}
|
||||
@@ -241,6 +403,10 @@ RasterArrayRuntimeFieldUpdateResult RasterArraySource::updateRuntimeFieldWindow(
|
||||
}
|
||||
|
||||
RasterArrayRuntimeFieldUpdateResult RasterArraySource::advanceRuntimeFieldWindow() {
|
||||
if (timeline) {
|
||||
return timeline->step(1) ? RasterArrayRuntimeFieldUpdateResult::Applied
|
||||
: RasterArrayRuntimeFieldUpdateResult::RejectedNoCandidate;
|
||||
}
|
||||
if (nextRuntimeField >= runtimeFields.size()) {
|
||||
return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedNoCandidate, "NO_PRELOADED_FIELD");
|
||||
}
|
||||
@@ -262,12 +428,45 @@ RasterArrayRuntimeFieldUpdateResult RasterArraySource::advanceRuntimeFieldWindow
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user