feat: add runtime particle field updates

This commit is contained in:
OpenAI Codex
2026-09-03 18:26:13 +08:00
parent 7af12e7d62
commit 45502a53f4
12 changed files with 346 additions and 15 deletions

View File

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

View File

@@ -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"),

View File

@@ -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&);

View File

@@ -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
/**

View File

@@ -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);

View File

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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

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

View File

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

View File

@@ -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);