feat: add runtime particle timeline playback
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-03 21:23:29 +08:00
parent 73bf01ebc0
commit 49b2e8aebd
17 changed files with 491 additions and 140 deletions

View File

@@ -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";
}