feat: add runtime particle field updates
This commit is contained in:
@@ -1,14 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
|
||||
#include <mbgl/style/source.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace mbgl {
|
||||
class AsyncRequest;
|
||||
|
||||
namespace style {
|
||||
|
||||
// A Source-owned, preloaded validation candidate. Production runtimes can use
|
||||
// updateRuntimeFieldWindow() directly with an already decoded snapshot instead.
|
||||
struct RasterArrayRuntimeFieldDescriptor {
|
||||
std::string fixtureID;
|
||||
std::string metadataURL;
|
||||
std::string fieldURL;
|
||||
};
|
||||
|
||||
enum class RasterArrayRuntimeFieldUpdateResult : uint8_t {
|
||||
Applied,
|
||||
Noop,
|
||||
RejectedNotLoaded,
|
||||
RejectedInvalidSnapshot,
|
||||
RejectedIncompatibleSnapshot,
|
||||
RejectedNonMonotonicGeneration,
|
||||
RejectedCandidateNotReady,
|
||||
RejectedNoCandidate,
|
||||
};
|
||||
|
||||
class RasterArraySource final : public Source {
|
||||
public:
|
||||
RasterArraySource(std::string id,
|
||||
@@ -17,12 +38,20 @@ public:
|
||||
std::string fieldURL,
|
||||
std::string nextFixtureID = {},
|
||||
std::string nextMetadataURL = {},
|
||||
std::string nextFieldURL = {});
|
||||
std::string nextFieldURL = {},
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields = {});
|
||||
~RasterArraySource() override;
|
||||
|
||||
class Impl;
|
||||
const Impl& impl() const;
|
||||
void loadDescription(FileSource&) override;
|
||||
// Advances the authoritative Source time window from T0/T1 to T1/newT1.
|
||||
// The caller transfers an immutable, already decoded Field snapshot; the
|
||||
// renderer observes the resulting Source change and owns GPU reuse/upload.
|
||||
RasterArrayRuntimeFieldUpdateResult updateRuntimeFieldWindow(particle::ParticleFieldSnapshot newT1);
|
||||
// Validation convenience: advance using the next Source-owned preloaded
|
||||
// candidate. Android supplies no T0/T1 state and no GPU resource here.
|
||||
RasterArrayRuntimeFieldUpdateResult advanceRuntimeFieldWindow();
|
||||
bool supportsLayerType(const LayerTypeInfo*) const override;
|
||||
mapbox::base::WeakPtr<Source> makeWeakPtr() override { return weakFactory.makeWeakPtr(); }
|
||||
|
||||
@@ -30,7 +59,19 @@ protected:
|
||||
Mutable<Source::Impl> createMutable() const noexcept override;
|
||||
|
||||
private:
|
||||
struct RuntimeFieldCandidate {
|
||||
RasterArrayRuntimeFieldDescriptor descriptor;
|
||||
std::unique_ptr<AsyncRequest> metadataRequest;
|
||||
std::unique_ptr<AsyncRequest> fieldRequest;
|
||||
std::optional<std::string> metadata;
|
||||
std::optional<std::vector<char>> fieldBytes;
|
||||
std::optional<particle::ParticleFieldSnapshot> snapshot;
|
||||
};
|
||||
|
||||
void tryLoad();
|
||||
void tryLoadRuntimeField(size_t);
|
||||
RasterArrayRuntimeFieldUpdateResult rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult,
|
||||
const std::string& reason) const;
|
||||
|
||||
std::unique_ptr<AsyncRequest> metadataRequest;
|
||||
std::unique_ptr<AsyncRequest> fieldRequest;
|
||||
@@ -40,6 +81,9 @@ private:
|
||||
std::optional<std::vector<char>> fieldBytes;
|
||||
std::optional<std::string> nextMetadata;
|
||||
std::optional<std::vector<char>> nextFieldBytes;
|
||||
std::vector<RuntimeFieldCandidate> runtimeFields;
|
||||
size_t nextRuntimeField = 0;
|
||||
uint64_t runtimeUpdateSequence = 0;
|
||||
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <mbgl/util/projection.hpp>
|
||||
#include <mbgl/style/style.hpp>
|
||||
#include <mbgl/style/layers/raster_particle_layer.hpp>
|
||||
#include <mbgl/style/sources/raster_array_source.hpp>
|
||||
#include <mbgl/style/image.hpp>
|
||||
#include <mbgl/style/filter.hpp>
|
||||
#include <mbgl/renderer/query.hpp>
|
||||
@@ -1145,6 +1146,24 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
|
||||
return true;
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::advanceRasterArraySourceFieldWindow(JNIEnv& env, const jni::String& sourceId) {
|
||||
const auto id = jni::Make<std::string>(env, sourceId);
|
||||
auto* source = map->getStyle().getSource(id);
|
||||
auto* rasterArraySource = dynamic_cast<mbgl::style::RasterArraySource*>(source);
|
||||
if (!rasterArraySource) {
|
||||
mbgl::Log::Error(mbgl::Event::JNI,
|
||||
"ParticleField: event=FIELD_WINDOW_UPDATE_REJECTED reason=SOURCE_NOT_FOUND sourceId=" + id);
|
||||
return false;
|
||||
}
|
||||
const auto result = rasterArraySource->advanceRuntimeFieldWindow();
|
||||
const bool success = result == mbgl::style::RasterArrayRuntimeFieldUpdateResult::Applied ||
|
||||
result == mbgl::style::RasterArrayRuntimeFieldUpdateResult::Noop;
|
||||
mbgl::Log::Info(mbgl::Event::JNI,
|
||||
"ParticleField: event=FIELD_WINDOW_UPDATE_BRIDGE sourceId=" + id +
|
||||
" result=" + (success ? "SUCCESS" : "REJECTED"));
|
||||
return success;
|
||||
}
|
||||
|
||||
void NativeMapView::addLayer(JNIEnv& env, jlong nativeLayerPtr, const jni::String& before) {
|
||||
assert(nativeLayerPtr != 0);
|
||||
|
||||
@@ -1529,6 +1548,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
|
||||
METHOD(&NativeMapView::getLayers, "nativeGetLayers"),
|
||||
METHOD(&NativeMapView::getLayer, "nativeGetLayer"),
|
||||
METHOD(&NativeMapView::setStyleLayerFloatProperty, "nativeSetStyleLayerFloatProperty"),
|
||||
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),
|
||||
METHOD(&NativeMapView::addLayer, "nativeAddLayer"),
|
||||
METHOD(&NativeMapView::addLayerAbove, "nativeAddLayerAbove"),
|
||||
METHOD(&NativeMapView::addLayerAt, "nativeAddLayerAt"),
|
||||
|
||||
@@ -282,6 +282,7 @@ public:
|
||||
|
||||
jni::Local<jni::Object<Layer>> getLayer(JNIEnv&, const jni::String&);
|
||||
jni::jboolean setStyleLayerFloatProperty(JNIEnv&, const jni::String&, const jni::String&, jni::jfloat);
|
||||
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
||||
|
||||
void addLayer(JNIEnv&, jlong, const jni::String&);
|
||||
|
||||
|
||||
@@ -291,6 +291,11 @@ public final class MapLibreMap {
|
||||
return nativeMapView.setStyleLayerFloatProperty(layerId, propertyName, value);
|
||||
}
|
||||
|
||||
/** Advances a Source-owned raster-array runtime Field window without reloading the Style. */
|
||||
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
|
||||
return nativeMapView.advanceRasterArraySourceFieldWindow(sourceId);
|
||||
}
|
||||
|
||||
// Style
|
||||
|
||||
/**
|
||||
|
||||
@@ -141,6 +141,8 @@ interface NativeMap {
|
||||
|
||||
boolean setStyleLayerFloatProperty(String layerId, String propertyName, float value);
|
||||
|
||||
boolean advanceRasterArraySourceFieldWindow(String sourceId);
|
||||
|
||||
boolean removeLayer(@NonNull String layerId);
|
||||
|
||||
boolean removeLayer(@NonNull Layer layer);
|
||||
|
||||
@@ -956,6 +956,12 @@ final class NativeMapView implements NativeMap {
|
||||
&& nativeSetStyleLayerFloatProperty(layerId, propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
|
||||
return !checkState("advanceRasterArraySourceFieldWindow")
|
||||
&& nativeAdvanceRasterArraySourceFieldWindow(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLayer(@NonNull Layer layer) {
|
||||
if (checkState("addLayer")) {
|
||||
@@ -1690,6 +1696,9 @@ final class NativeMapView implements NativeMap {
|
||||
@Keep
|
||||
private native boolean nativeSetStyleLayerFloatProperty(String layerId, String propertyName, float value);
|
||||
|
||||
@Keep
|
||||
private native boolean nativeAdvanceRasterArraySourceFieldWindow(String sourceId);
|
||||
|
||||
@Keep
|
||||
private native void nativeAddLayer(long layerPtr, String before) throws CannotAddLayerException;
|
||||
|
||||
|
||||
@@ -105,10 +105,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationParticle65536).setOnClickListener { setParticleCount(PARTICLE_COUNT_65536) }
|
||||
findViewById<Button>(R.id.validationParticle1048576).setOnClickListener { setParticleCount(PARTICLE_COUNT_1048576) }
|
||||
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_NORTH
|
||||
timeMix = 0.0f
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "ADVANCE")
|
||||
if (this::maplibreMap.isInitialized) advanceRuntimeFieldWindow()
|
||||
}
|
||||
findViewById<Button>(R.id.validationDateline).setOnClickListener {
|
||||
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
|
||||
@@ -196,6 +193,26 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_APPLIED requestedParticleCount=$requestedParticleCount effectiveParticleCount=$particleCount styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun advanceRuntimeFieldWindow() {
|
||||
val rejectReason = when {
|
||||
!styleReady -> "STYLE_LOADING"
|
||||
!enabled -> "DISABLED"
|
||||
!timeWindowMode -> "TIME_WINDOW_OFF"
|
||||
else -> null
|
||||
}
|
||||
if (rejectReason != null) {
|
||||
Log.i(TAG, "ParticleField: event=FIELD_WINDOW_UPDATE_REJECTED reason=$rejectReason styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "ParticleField: event=FIELD_WINDOW_UPDATE_REQUEST sourceId=$PARTICLE_SOURCE_ID styleEpoch=$styleLoadEpoch")
|
||||
if (!maplibreMap.advanceRasterArraySourceFieldWindow(PARTICLE_SOURCE_ID)) {
|
||||
Log.i(TAG, "ParticleField: event=FIELD_WINDOW_UPDATE_REJECTED reason=CORE_SOURCE_REJECTED styleEpoch=$styleLoadEpoch")
|
||||
return
|
||||
}
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleField: event=FIELD_WINDOW_UPDATE_APPLIED styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun statusText(): String {
|
||||
val mask = when (maskMode) {
|
||||
MASK_OCEAN_A -> "OCEAN A"
|
||||
@@ -204,7 +221,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
val runtime = if (enabled) if (active) "ACTIVE" else "STATIC" else "DISABLED"
|
||||
val fields = if (timeWindowMode) {
|
||||
"T0 EAST · T1 NORTH"
|
||||
"RUNTIME WINDOW"
|
||||
} else if (fixtureMode == FIXTURE_UNIFORM_NORTH) {
|
||||
"NORTH"
|
||||
} else {
|
||||
@@ -239,7 +256,13 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
"field": "asset://integration/particle-fixtures/$fixtureId/field.f32",
|
||||
"next-fixture": "$nextFixtureId",
|
||||
"next-metadata": "asset://integration/particle-fixtures/$nextFixtureId/metadata.json",
|
||||
"next-field": "asset://integration/particle-fixtures/$nextFixtureId/field.f32"
|
||||
"next-field": "asset://integration/particle-fixtures/$nextFixtureId/field.f32",
|
||||
"runtime-fields": [
|
||||
{"fixture":"vortex","metadata":"asset://integration/particle-fixtures/vortex/metadata.json",
|
||||
"field":"asset://integration/particle-fixtures/vortex/field.f32"},
|
||||
{"fixture":"nodata","metadata":"asset://integration/particle-fixtures/nodata/metadata.json",
|
||||
"field":"asset://integration/particle-fixtures/nodata/field.f32"}
|
||||
]
|
||||
},
|
||||
"validation-point": {
|
||||
"type": "geojson",
|
||||
@@ -292,6 +315,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val STATE_PARTICLE_COUNT = "particle_validation.particle_count"
|
||||
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
|
||||
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
||||
private const val PARTICLE_SOURCE_ID = "particle-fixture"
|
||||
private const val MASK_GLOBAL = 0
|
||||
private const val MASK_OCEAN_A = 1
|
||||
private const val MASK_OCEAN_B = 2
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<string name="particle_validation_uniform_east">UNIFORM EAST</string>
|
||||
<string name="particle_validation_uniform_north">UNIFORM NORTH</string>
|
||||
<string name="particle_validation_time_window">T0 EAST / T1 NORTH</string>
|
||||
<string name="particle_validation_advance_window">ADVANCE B→C</string>
|
||||
<string name="particle_validation_advance_window">ADVANCE WINDOW</string>
|
||||
<string name="particle_validation_mix_0">MIX 0%</string>
|
||||
<string name="particle_validation_mix_50">MIX 50%</string>
|
||||
<string name="particle_validation_mix_100">MIX 100%</string>
|
||||
|
||||
@@ -229,7 +229,35 @@ std::optional<std::unique_ptr<Source>> convertRasterArraySource(const std::strin
|
||||
error.message = "raster-array next fixture requires next-fixture, next-metadata, and next-field";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!nextFixture) return {std::make_unique<RasterArraySource>(id, *fixtureID, *metadataURL, *fieldURL)};
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields;
|
||||
if (const auto runtimeFieldsValue = objectMember(value, "runtime-fields")) {
|
||||
if (!isArray(*runtimeFieldsValue)) {
|
||||
error.message = "raster-array runtime-fields must be an array";
|
||||
return std::nullopt;
|
||||
}
|
||||
for (size_t index = 0; index < arrayLength(*runtimeFieldsValue); ++index) {
|
||||
const auto runtimeField = arrayMember(*runtimeFieldsValue, index);
|
||||
const auto runtimeFixture = objectMember(runtimeField, "fixture");
|
||||
const auto runtimeMetadata = objectMember(runtimeField, "metadata");
|
||||
const auto runtimeData = objectMember(runtimeField, "field");
|
||||
if (!runtimeFixture || !runtimeMetadata || !runtimeData) {
|
||||
error.message = "raster-array runtime field requires fixture, metadata, and field";
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto runtimeFixtureID = toString(*runtimeFixture);
|
||||
const auto runtimeMetadataURL = toString(*runtimeMetadata);
|
||||
const auto runtimeFieldURL = toString(*runtimeData);
|
||||
if (!runtimeFixtureID || !runtimeMetadataURL || !runtimeFieldURL) {
|
||||
error.message = "raster-array runtime field inputs must be strings";
|
||||
return std::nullopt;
|
||||
}
|
||||
runtimeFields.push_back({*runtimeFixtureID, *runtimeMetadataURL, *runtimeFieldURL});
|
||||
}
|
||||
}
|
||||
if (!nextFixture) {
|
||||
return {std::make_unique<RasterArraySource>(
|
||||
id, *fixtureID, *metadataURL, *fieldURL, "", "", "", std::move(runtimeFields))};
|
||||
}
|
||||
const auto nextFixtureID = toString(*nextFixture);
|
||||
const auto nextMetadataURL = toString(*nextMetadata);
|
||||
const auto nextFieldURL = toString(*nextField);
|
||||
@@ -237,8 +265,14 @@ std::optional<std::unique_ptr<Source>> convertRasterArraySource(const std::strin
|
||||
error.message = "raster-array next fixture inputs must be strings";
|
||||
return std::nullopt;
|
||||
}
|
||||
return {std::make_unique<RasterArraySource>(
|
||||
id, *fixtureID, *metadataURL, *fieldURL, *nextFixtureID, *nextMetadataURL, *nextFieldURL)};
|
||||
return {std::make_unique<RasterArraySource>(id,
|
||||
*fixtureID,
|
||||
*metadataURL,
|
||||
*fieldURL,
|
||||
*nextFixtureID,
|
||||
*nextMetadataURL,
|
||||
*nextFieldURL,
|
||||
std::move(runtimeFields))};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -27,14 +27,16 @@ RasterArraySource::RasterArraySource(std::string id,
|
||||
std::string fieldURL,
|
||||
std::string nextFixtureID,
|
||||
std::string nextMetadataURL,
|
||||
std::string nextFieldURL)
|
||||
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(nextFieldURL),
|
||||
std::move(runtimeFields))) {}
|
||||
|
||||
RasterArraySource::~RasterArraySource() {
|
||||
weakFactory.invalidateWeakPtrs();
|
||||
@@ -103,6 +105,42 @@ void RasterArraySource::loadDescription(FileSource& fileSource) {
|
||||
}
|
||||
});
|
||||
}
|
||||
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() {
|
||||
@@ -140,6 +178,89 @@ void RasterArraySource::tryLoad() {
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
} 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 (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;
|
||||
}
|
||||
|
||||
bool RasterArraySource::supportsLayerType(const LayerTypeInfo* info) const {
|
||||
return info && std::string(info->type) == "raster-particle";
|
||||
}
|
||||
|
||||
@@ -14,14 +14,16 @@ public:
|
||||
std::string fieldURL_,
|
||||
std::string nextFixtureID_ = {},
|
||||
std::string nextMetadataURL_ = {},
|
||||
std::string nextFieldURL_ = {})
|
||||
std::string nextFieldURL_ = {},
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields_ = {})
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)),
|
||||
fixtureID(std::move(fixtureID_)),
|
||||
metadataURL(std::move(metadataURL_)),
|
||||
fieldURL(std::move(fieldURL_)),
|
||||
nextFixtureID(std::move(nextFixtureID_)),
|
||||
nextMetadataURL(std::move(nextMetadataURL_)),
|
||||
nextFieldURL(std::move(nextFieldURL_)) {}
|
||||
nextFieldURL(std::move(nextFieldURL_)),
|
||||
runtimeFields(std::move(runtimeFields_)) {}
|
||||
Impl(const Impl&) = default;
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_)
|
||||
: Source::Impl(other),
|
||||
@@ -31,6 +33,7 @@ public:
|
||||
nextFixtureID(other.nextFixtureID),
|
||||
nextMetadataURL(other.nextMetadataURL),
|
||||
nextFieldURL(other.nextFieldURL),
|
||||
runtimeFields(other.runtimeFields),
|
||||
field(std::move(field_)) {}
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_, particle::ParticleFieldSnapshot nextField_)
|
||||
: Impl(other, std::move(field_)) {
|
||||
@@ -44,6 +47,7 @@ public:
|
||||
std::string nextFixtureID;
|
||||
std::string nextMetadataURL;
|
||||
std::string nextFieldURL;
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields;
|
||||
std::optional<particle::ParticleFieldSnapshot> field;
|
||||
std::optional<particle::ParticleFieldSnapshot> nextField;
|
||||
};
|
||||
|
||||
@@ -241,6 +241,73 @@ TEST(Source, RasterArrayGpuReuseConsumesNewRevision) {
|
||||
EXPECT_FALSE(particle::fieldIdentityChanged(assignedAfterReuse, wantedNewT0));
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayRuntimeFieldWindowAdvancesWithoutReplacingTheSource) {
|
||||
SourceTest test;
|
||||
const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json");
|
||||
const auto eastField = fixtureBytes("uniform-east", "field.f32");
|
||||
const auto northMetadata = fixtureBytes("uniform-north", "metadata.json");
|
||||
const auto northField = fixtureBytes("uniform-north", "field.f32");
|
||||
|
||||
test.fileSource->sourceResponse = [&](const Resource& resource) -> Response {
|
||||
Response response;
|
||||
if (resource.url == "east-metadata") {
|
||||
response.data = std::make_unique<std::string>(eastMetadata);
|
||||
} else if (resource.url == "east-field") {
|
||||
response.data = std::make_unique<std::string>(eastField);
|
||||
} else if (resource.url == "north-metadata") {
|
||||
response.data = std::make_unique<std::string>(northMetadata);
|
||||
} else if (resource.url == "north-field") {
|
||||
response.data = std::make_unique<std::string>(northField);
|
||||
} else {
|
||||
ADD_FAILURE() << "Unexpected RasterArray resource: " << resource.url;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
style::RasterArraySource source{
|
||||
"source", "uniform-east", "east-metadata", "east-field", "uniform-north", "north-metadata", "north-field"};
|
||||
source.setObserver(&test.styleObserver);
|
||||
bool initialLoad = true;
|
||||
uint32_t sourceChangedCount = 0;
|
||||
test.styleObserver.sourceChanged = [&](Source&) {
|
||||
++sourceChangedCount;
|
||||
if (initialLoad) {
|
||||
initialLoad = false;
|
||||
test.end();
|
||||
}
|
||||
};
|
||||
source.loadDescription(*test.fileSource);
|
||||
test.run();
|
||||
|
||||
ASSERT_TRUE(source.impl().field);
|
||||
ASSERT_TRUE(source.impl().nextField);
|
||||
const auto initialT0Generation = source.impl().field->generation;
|
||||
const auto initialT1Generation = source.impl().nextField->generation;
|
||||
const auto vortex = particle::OfflineFixtureParticleAdapter::load("vortex", initialT1Generation + 1);
|
||||
const auto nodata = particle::OfflineFixtureParticleAdapter::load("nodata", vortex.generation + 1);
|
||||
|
||||
EXPECT_EQ(source.updateRuntimeFieldWindow(vortex), style::RasterArrayRuntimeFieldUpdateResult::Applied);
|
||||
EXPECT_EQ(source.impl().field->fixtureID, "uniform-north");
|
||||
EXPECT_EQ(source.impl().field->generation, initialT1Generation);
|
||||
EXPECT_EQ(source.impl().nextField->fixtureID, "vortex");
|
||||
EXPECT_EQ(source.impl().nextField->generation, vortex.generation);
|
||||
EXPECT_EQ(sourceChangedCount, 2u);
|
||||
|
||||
EXPECT_EQ(source.updateRuntimeFieldWindow(vortex), style::RasterArrayRuntimeFieldUpdateResult::Noop);
|
||||
auto invalid = nodata;
|
||||
invalid.velocity.clear();
|
||||
EXPECT_EQ(source.updateRuntimeFieldWindow(invalid),
|
||||
style::RasterArrayRuntimeFieldUpdateResult::RejectedInvalidSnapshot);
|
||||
EXPECT_EQ(source.impl().field->fixtureID, "uniform-north");
|
||||
EXPECT_EQ(source.impl().nextField->fixtureID, "vortex");
|
||||
|
||||
EXPECT_EQ(source.updateRuntimeFieldWindow(nodata), style::RasterArrayRuntimeFieldUpdateResult::Applied);
|
||||
EXPECT_EQ(source.impl().field->fixtureID, "vortex");
|
||||
EXPECT_EQ(source.impl().nextField->fixtureID, "nodata");
|
||||
EXPECT_EQ(sourceChangedCount, 3u);
|
||||
EXPECT_NE(source.impl().field->generation, initialT0Generation);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayTimeWindowKeepsIndependentCompatibleSnapshots) {
|
||||
const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 11);
|
||||
const auto north = particle::OfflineFixtureParticleAdapter::load("uniform-north", 12);
|
||||
|
||||
Reference in New Issue
Block a user