feat: add GPU particle time interpolation
This commit is contained in:
@@ -42,6 +42,7 @@ void main() {
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_mask_texture;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
uniform sampler2D u_state_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
@@ -57,9 +58,12 @@ void main() {
|
||||
// u_mask.w is the explicit State lifecycle flag: new RenderTargets are
|
||||
// cleared opaque by MapLibre, so sampled alpha cannot identify a seed.
|
||||
vec2 position = u_mask.w > 0.5 ? v_uv : previous.rg;
|
||||
vec4 field = texture(u_field_texture, position);
|
||||
vec2 velocity = field.rg * 2.0 - 1.0;
|
||||
if (field.a < 0.5) {
|
||||
// 40-02: decode each independent Field texture, then interpolate the
|
||||
// velocity on GPU. u_marker.x is the clamped Layer timeMix property.
|
||||
vec4 field0 = texture(u_field_texture, position);
|
||||
vec4 field1 = texture(u_next_field_texture, position);
|
||||
vec2 velocity = mix(field0.rg * 2.0 - 1.0, field1.rg * 2.0 - 1.0, u_marker.x);
|
||||
if (field0.a < 0.5 || field1.a < 0.5) {
|
||||
position = v_uv;
|
||||
velocity = vec2(0.0);
|
||||
}
|
||||
|
||||
@@ -342,6 +342,7 @@ enum {
|
||||
idParticleGpuValidationPreviousTexture,
|
||||
idParticleGpuValidationMaskTexture,
|
||||
idParticleGpuValidationFieldTexture,
|
||||
idParticleGpuValidationNextFieldTexture,
|
||||
idParticleGpuValidationStateTexture,
|
||||
particleGpuValidationTextureCount
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ public:
|
||||
void setActive(bool);
|
||||
uint8_t getMaskMode() const;
|
||||
void setMaskMode(uint8_t);
|
||||
float getTimeMix() const;
|
||||
void setTimeMix(float);
|
||||
class Impl;
|
||||
const Impl& impl() const;
|
||||
Mutable<Impl> mutableImpl() const;
|
||||
|
||||
@@ -11,7 +11,13 @@ namespace style {
|
||||
|
||||
class RasterArraySource final : public Source {
|
||||
public:
|
||||
RasterArraySource(std::string id, std::string fixtureID, std::string metadataURL, std::string fieldURL);
|
||||
RasterArraySource(std::string id,
|
||||
std::string fixtureID,
|
||||
std::string metadataURL,
|
||||
std::string fieldURL,
|
||||
std::string nextFixtureID = {},
|
||||
std::string nextMetadataURL = {},
|
||||
std::string nextFieldURL = {});
|
||||
~RasterArraySource() override;
|
||||
|
||||
class Impl;
|
||||
@@ -28,8 +34,12 @@ private:
|
||||
|
||||
std::unique_ptr<AsyncRequest> metadataRequest;
|
||||
std::unique_ptr<AsyncRequest> fieldRequest;
|
||||
std::unique_ptr<AsyncRequest> nextMetadataRequest;
|
||||
std::unique_ptr<AsyncRequest> nextFieldRequest;
|
||||
std::optional<std::string> metadata;
|
||||
std::optional<std::vector<char>> fieldBytes;
|
||||
std::optional<std::string> nextMetadata;
|
||||
std::optional<std::vector<char>> nextFieldBytes;
|
||||
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,12 +22,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private var enabled = true
|
||||
private var maskMode = MASK_GLOBAL
|
||||
private var fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
private var timeMix = 0.0f
|
||||
private var timeWindowMode = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
active = savedInstanceState?.getBoolean(STATE_ACTIVE, true) ?: true
|
||||
enabled = savedInstanceState?.getBoolean(STATE_ENABLED, true) ?: true
|
||||
maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL
|
||||
fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST
|
||||
timeMix = savedInstanceState?.getFloat(STATE_TIME_MIX, 0.0f) ?: 0.0f
|
||||
timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.i(
|
||||
TAG,
|
||||
@@ -72,13 +76,30 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
if (this::maplibreMap.isInitialized) setValidationProperties(active = active, maskMode = MASK_OCEAN_B)
|
||||
}
|
||||
findViewById<Button>(R.id.validationUniformEast).setOnClickListener {
|
||||
timeWindowMode = false
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
|
||||
}
|
||||
findViewById<Button>(R.id.validationUniformNorth).setOnClickListener {
|
||||
timeWindowMode = false
|
||||
fixtureMode = FIXTURE_UNIFORM_NORTH
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
|
||||
}
|
||||
findViewById<Button>(R.id.validationTimeWindow).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
timeMix = 0.0f
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
|
||||
}
|
||||
findViewById<Button>(R.id.validationMix0).setOnClickListener { setTimeMix(0.0f) }
|
||||
findViewById<Button>(R.id.validationMix50).setOnClickListener { setTimeMix(0.5f) }
|
||||
findViewById<Button>(R.id.validationMix100).setOnClickListener { setTimeMix(1.0f) }
|
||||
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_NORTH
|
||||
timeMix = 0.0f
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled)
|
||||
}
|
||||
findViewById<Button>(R.id.validationDateline).setOnClickListener {
|
||||
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
|
||||
Log.i(TAG, "ParticleValidation30_03: DATELINE preset center=0,179 zoom=2")
|
||||
@@ -116,6 +137,20 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleRender: property update active=$active maskMode=$maskMode fixtureMode=$fixtureMode")
|
||||
}
|
||||
|
||||
private fun setTimeMix(value: Float) {
|
||||
timeMix = value.coerceIn(0.0f, 1.0f)
|
||||
val layer = maplibreMap.style?.getLayer(PARTICLE_LAYER_ID)
|
||||
if (layer == null || !enabled || !timeWindowMode) {
|
||||
timeWindowMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
loadValidationStyle(active, enabled)
|
||||
return
|
||||
}
|
||||
layer.setProperties(PaintPropertyValue("raster-particle-time-mix", timeMix))
|
||||
status.text = "40-02 T0 east / T1 north timeMix=$timeMix"
|
||||
Log.i(TAG, "ParticleGPU: test timeMix=$timeMix; expect no Field upload or simulation rebuild")
|
||||
}
|
||||
|
||||
private fun statusText(): String = when {
|
||||
!enabled -> getString(R.string.particle_validation_disabled_status)
|
||||
maskMode == MASK_OCEAN_A -> getString(R.string.particle_validation_ocean_a_status)
|
||||
@@ -126,9 +161,15 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
|
||||
private fun validationStyle(active: Boolean, enabled: Boolean): String {
|
||||
val fixtureId = fixtureId()
|
||||
val nextFixtureId = if (timeWindowMode) {
|
||||
if (fixtureMode == FIXTURE_UNIFORM_NORTH) "vortex" else "uniform-north"
|
||||
} else {
|
||||
fixtureId
|
||||
}
|
||||
val particleLayer = if (enabled) {
|
||||
"""{"id":"particle-raster-core","type":"raster-particle","source":"particle-fixture",
|
||||
"paint":{"raster-particle-active":$active,"raster-particle-mask-mode":$maskMode}},"""
|
||||
"paint":{"raster-particle-active":$active,"raster-particle-mask-mode":$maskMode,
|
||||
"raster-particle-time-mix":$timeMix}},"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -141,7 +182,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
"type": "raster-array",
|
||||
"fixture": "$fixtureId",
|
||||
"metadata": "asset://integration/particle-fixtures/$fixtureId/metadata.json",
|
||||
"field": "asset://integration/particle-fixtures/$fixtureId/field.f32"
|
||||
"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"
|
||||
},
|
||||
"validation-point": {
|
||||
"type": "geojson",
|
||||
@@ -172,6 +216,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
outState.putBoolean(STATE_ENABLED, enabled)
|
||||
outState.putInt(STATE_MASK_MODE, maskMode)
|
||||
outState.putInt(STATE_FIXTURE_MODE, fixtureMode)
|
||||
outState.putFloat(STATE_TIME_MIX, timeMix)
|
||||
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
mapView.onSaveInstanceState(outState)
|
||||
}
|
||||
@@ -187,6 +233,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private const val STATE_ENABLED = "particle_validation.enabled"
|
||||
private const val STATE_MASK_MODE = "particle_validation.mask_mode"
|
||||
private const val STATE_FIXTURE_MODE = "particle_validation.fixture_mode"
|
||||
private const val STATE_TIME_MIX = "particle_validation.time_mix"
|
||||
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
|
||||
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
||||
private const val MASK_GLOBAL = 0
|
||||
private const val MASK_OCEAN_A = 1
|
||||
|
||||
@@ -44,6 +44,53 @@
|
||||
android:text="@string/particle_validation_uniform_north" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationTimeWindow"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_time_window" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationAdvanceWindow"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_advance_window" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationMix0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_mix_0" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationMix50"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_mix_50" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationMix100"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/particle_validation_mix_100" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
<string name="particle_validation_world_wrap">WORLD WRAP</string>
|
||||
<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_mix_0">MIX 0%</string>
|
||||
<string name="particle_validation_mix_50">MIX 50%</string>
|
||||
<string name="particle_validation_mix_100">MIX 100%</string>
|
||||
<string name="particle_validation_global_active_status">40-01 GLOBAL ACTIVE: offline field simulation and current-frame composite.</string>
|
||||
<string name="particle_validation_global_static_status">40-01 GLOBAL STATIC: zero-step bootstrap and frozen composite.</string>
|
||||
<string name="particle_validation_ocean_a_status">40-01 OCEAN_ONLY_A: shared coastal-mask consumer.</string>
|
||||
|
||||
@@ -8,6 +8,7 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_mask_texture;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
uniform sampler2D u_state_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
@@ -23,9 +24,12 @@ void main() {
|
||||
// u_mask.w is the explicit State lifecycle flag: new RenderTargets are
|
||||
// cleared opaque by MapLibre, so sampled alpha cannot identify a seed.
|
||||
vec2 position = u_mask.w > 0.5 ? v_uv : previous.rg;
|
||||
vec4 field = texture(u_field_texture, position);
|
||||
vec2 velocity = field.rg * 2.0 - 1.0;
|
||||
if (field.a < 0.5) {
|
||||
// 40-02: decode each independent Field texture, then interpolate the
|
||||
// velocity on GPU. u_marker.x is the clamped Layer timeMix property.
|
||||
vec4 field0 = texture(u_field_texture, position);
|
||||
vec4 field1 = texture(u_next_field_texture, position);
|
||||
vec2 velocity = mix(field0.rg * 2.0 - 1.0, field1.rg * 2.0 - 1.0, u_marker.x);
|
||||
if (field0.a < 0.5 || field1.a < 0.5) {
|
||||
position = v_uv;
|
||||
velocity = vec2(0.0);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,12 @@ bool ParticleFieldSnapshot::isValid() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParticleFieldSnapshot::isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot& other) const {
|
||||
return width == other.width && height == other.height && west == other.west && south == other.south &&
|
||||
east == other.east && north == other.north && noData == other.noData && fieldType == other.fieldType &&
|
||||
displayDomain == other.displayDomain;
|
||||
}
|
||||
|
||||
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID,
|
||||
const std::string& metadataText,
|
||||
const std::vector<char>& bytes,
|
||||
|
||||
@@ -27,6 +27,10 @@ struct ParticleFieldSnapshot {
|
||||
std::vector<uint8_t> valid;
|
||||
|
||||
bool isValid() const;
|
||||
// Two snapshots can be interpolated only when their geographic sampling
|
||||
// contract is identical. Fixture identity/version intentionally do not
|
||||
// participate: they identify different values in the same grid.
|
||||
bool isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot&) const;
|
||||
};
|
||||
|
||||
// A snapshot revision identifies a runtime load, while fixtureID and
|
||||
@@ -37,6 +41,15 @@ inline bool fieldIdentityChanged(const ParticleFieldSnapshot& current, const Par
|
||||
current.dataVersion != next.dataVersion;
|
||||
}
|
||||
|
||||
// A formal source time window. T0/T1 remain independent immutable snapshots;
|
||||
// the renderer uploads and samples them independently on the GPU.
|
||||
struct ParticleFieldTimeWindow {
|
||||
ParticleFieldSnapshot t0;
|
||||
ParticleFieldSnapshot t1;
|
||||
|
||||
bool isValid() const { return t0.isValid() && t1.isValid() && t0.isTimeInterpolationCompatibleWith(t1); }
|
||||
};
|
||||
|
||||
// Phase-one OfflineFixtureParticleAdapter. Its public contract deliberately
|
||||
// contains no Android/UI/GPU objects, allowing the same decoded snapshot to be
|
||||
// used by future Android Vulkan and iOS renderers.
|
||||
|
||||
@@ -54,16 +54,17 @@ struct ParticleSettings {
|
||||
uint8_t maskMode;
|
||||
uint8_t fixtureMode;
|
||||
bool usesFormalSource;
|
||||
float timeMix;
|
||||
};
|
||||
|
||||
ParticleSettings particleSettings(const Immutable<style::Layer::Impl>& value) {
|
||||
if (value->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo()) {
|
||||
const auto& layer = static_cast<const style::RasterParticleLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, 0, true};
|
||||
return {layer.active, layer.maskMode, 0, true, layer.timeMix};
|
||||
}
|
||||
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
|
||||
const auto& layer = static_cast<const style::ParticleValidationLayer::Impl&>(*value);
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false};
|
||||
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f};
|
||||
}
|
||||
|
||||
enum class ValidationPass : uint8_t {
|
||||
@@ -164,6 +165,10 @@ public:
|
||||
uint64_t maskGeneration = 0;
|
||||
uint64_t fieldGeneration = 0;
|
||||
uint64_t gpuFieldGeneration = 0;
|
||||
uint64_t nextFieldGeneration = 0;
|
||||
uint64_t gpuNextFieldGeneration = 0;
|
||||
uint64_t fieldUploadCount = 0;
|
||||
float timeMix = 0.0f;
|
||||
uint8_t fixtureMode = 255;
|
||||
uint8_t loggedMaskMode = 255;
|
||||
Size trailSize{};
|
||||
@@ -174,9 +179,12 @@ public:
|
||||
gfx::ShaderProgramBasePtr shader;
|
||||
gfx::Texture2DPtr maskTexture;
|
||||
gfx::Texture2DPtr fieldTexture;
|
||||
gfx::Texture2DPtr nextFieldTexture;
|
||||
gfx::Texture2DPtr stateTexture;
|
||||
particle::ParticleFieldSnapshot field;
|
||||
particle::ParticleFieldSnapshot nextField;
|
||||
std::shared_ptr<PremultipliedImage> fieldImage;
|
||||
std::shared_ptr<PremultipliedImage> nextFieldImage;
|
||||
std::shared_ptr<PremultipliedImage> maskFixture;
|
||||
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
|
||||
SegmentVector segments;
|
||||
@@ -263,12 +271,15 @@ public:
|
||||
if (resources->fieldTexture) {
|
||||
drawable.setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture);
|
||||
}
|
||||
if (resources->nextFieldTexture) {
|
||||
drawable.setTexture(resources->nextFieldTexture, shaders::idParticleGpuValidationNextFieldTexture);
|
||||
}
|
||||
if (stateTexture) {
|
||||
drawable.setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
|
||||
}
|
||||
|
||||
const float frame = simulationStep;
|
||||
const float markerX = 0.5f + 0.32f * std::sin(frame * 0.071f);
|
||||
const float markerX = resources->timeMix;
|
||||
const float markerY = 0.5f + 0.32f * std::cos(frame * 0.053f);
|
||||
// RenderTarget clear is opaque black. State initialization must therefore
|
||||
// be an explicit lifecycle signal, never inferred from sampled alpha.
|
||||
@@ -472,6 +483,9 @@ bool buildQuad(gfx::Context& context,
|
||||
if (resources->fieldTexture) {
|
||||
builder->setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture);
|
||||
}
|
||||
if (resources->nextFieldTexture) {
|
||||
builder->setTexture(resources->nextFieldTexture, shaders::idParticleGpuValidationNextFieldTexture);
|
||||
}
|
||||
if (stateTexture) {
|
||||
builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
|
||||
}
|
||||
@@ -495,7 +509,7 @@ RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable<style::Layer::Imp
|
||||
: RenderLayer([&] {
|
||||
const auto settings = particleSettings(impl_);
|
||||
return makeMutable<style::ParticleRenderLayerProperties>(
|
||||
std::move(impl_), settings.active, settings.maskMode, settings.fixtureMode);
|
||||
std::move(impl_), settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix);
|
||||
}()) {
|
||||
formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo();
|
||||
Log::Info(Event::Render,
|
||||
@@ -515,7 +529,7 @@ RenderRasterParticleLayer::~RenderRasterParticleLayer() {
|
||||
void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
const auto settings = particleSettings(baseImpl);
|
||||
auto properties = makeMutable<style::ParticleRenderLayerProperties>(
|
||||
baseImpl, settings.active, settings.maskMode, settings.fixtureMode);
|
||||
baseImpl, settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix);
|
||||
passes = RenderPass::Translucent;
|
||||
properties->renderPasses = mbgl::underlying_type(passes);
|
||||
evaluatedProperties = std::move(properties);
|
||||
@@ -524,7 +538,8 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
|
||||
"ParticleRender: state=" + std::string(settings.active ? "ACTIVE" : "STATIC") +
|
||||
" maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + " fixture=" +
|
||||
(formalSource && sourceField ? sourceField->fixtureID : fixtureID(settings.fixtureMode)) +
|
||||
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=40-01");
|
||||
" layerIndex=" + std::to_string(getLayerIndex()) + " timeMix=" + std::to_string(settings.timeMix) +
|
||||
" stage=40-02");
|
||||
}
|
||||
|
||||
bool RenderRasterParticleLayer::needsRepaint() const {
|
||||
@@ -565,6 +580,7 @@ void RenderRasterParticleLayer::prepare(const LayerPrepareParameters& parameters
|
||||
const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source);
|
||||
if (source.impl().field) {
|
||||
sourceField = *source.impl().field;
|
||||
sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,20 +639,26 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
owned->maskGeneration = ++nextMaskGeneration;
|
||||
try {
|
||||
if (formalSource) {
|
||||
if (!sourceField) {
|
||||
Log::Error(Event::Render, "ParticleField: raster-array source has no loaded field");
|
||||
if (!sourceField || !sourceNextField) {
|
||||
Log::Error(Event::Render, "ParticleField: raster-array source has no loaded time window");
|
||||
return;
|
||||
}
|
||||
owned->field = *sourceField;
|
||||
owned->nextField = *sourceNextField;
|
||||
owned->fieldGeneration = owned->field.generation;
|
||||
owned->nextFieldGeneration = owned->nextField.generation;
|
||||
} else {
|
||||
owned->fieldGeneration = ++nextFieldGeneration;
|
||||
owned->field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode),
|
||||
owned->fieldGeneration);
|
||||
owned->nextField = owned->field;
|
||||
owned->nextFieldGeneration = owned->fieldGeneration;
|
||||
}
|
||||
owned->fieldImage = makeVelocityFieldImage(owned->field);
|
||||
owned->fieldTexture = context.createTexture2D();
|
||||
if (!owned->fieldImage || !owned->fieldTexture) {
|
||||
owned->nextFieldImage = makeVelocityFieldImage(owned->nextField);
|
||||
owned->nextFieldTexture = context.createTexture2D();
|
||||
if (!owned->fieldImage || !owned->fieldTexture || !owned->nextFieldImage || !owned->nextFieldTexture) {
|
||||
Log::Error(Event::Render, "ParticleGPU: field resource creation failed");
|
||||
return;
|
||||
}
|
||||
@@ -644,7 +666,13 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
owned->nextFieldTexture->setImage(owned->nextFieldImage);
|
||||
owned->nextFieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
owned->gpuFieldGeneration = owned->fieldGeneration;
|
||||
owned->gpuNextFieldGeneration = owned->nextFieldGeneration;
|
||||
owned->fieldUploadCount = 2;
|
||||
owned->fixtureMode = fixtureMode;
|
||||
} catch (const std::exception& error) {
|
||||
Log::Error(Event::Render, "ParticleField: fixture load failed: " + std::string(error.what()));
|
||||
@@ -675,6 +703,11 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
" fieldGeneration=" + std::to_string(resources->fieldGeneration) +
|
||||
" gpuFieldGeneration=" + std::to_string(resources->gpuFieldGeneration) +
|
||||
" resourceGeneration=" + std::to_string(resources->generation));
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: time window T0=" + resources->field.fixtureID + "@" +
|
||||
std::to_string(resources->field.dataVersion) + " T1=" + resources->nextField.fixtureID + "@" +
|
||||
std::to_string(resources->nextField.dataVersion) +
|
||||
" uploads=" + std::to_string(resources->fieldUploadCount));
|
||||
Log::Info(
|
||||
Event::Render,
|
||||
"ParticleValidation30_03: mask fixture ready id=coastal-mask-validation-v1 bounds=[-180,180]x[-85,85] "
|
||||
@@ -684,48 +717,100 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
"ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0");
|
||||
}
|
||||
|
||||
const bool sourceChanged = formalSource && sourceField &&
|
||||
particle::fieldIdentityChanged(resources->field, *sourceField);
|
||||
const bool sourceChanged = formalSource && sourceField && sourceNextField &&
|
||||
(particle::fieldIdentityChanged(resources->field, *sourceField) ||
|
||||
particle::fieldIdentityChanged(resources->nextField, *sourceNextField));
|
||||
if ((!formalSource && resources->fixtureMode != fixtureMode) || sourceChanged) {
|
||||
try {
|
||||
const auto oldFixtureID = resources->field.fixtureID;
|
||||
const auto oldFieldGeneration = resources->fieldGeneration;
|
||||
const auto fieldGeneration = formalSource ? sourceField->generation : ++nextFieldGeneration;
|
||||
auto field = formalSource
|
||||
? *sourceField
|
||||
: particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), fieldGeneration);
|
||||
auto image = makeVelocityFieldImage(field);
|
||||
auto texture = context.createTexture2D();
|
||||
if (!image || !texture) {
|
||||
const auto oldT0 = resources->field;
|
||||
const auto oldT1 = resources->nextField;
|
||||
auto nextT0 = formalSource ? *sourceField
|
||||
: particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode),
|
||||
++nextFieldGeneration);
|
||||
auto nextT1 = formalSource ? *sourceNextField : nextT0;
|
||||
if (!nextT0.isTimeInterpolationCompatibleWith(nextT1)) {
|
||||
Log::Error(Event::Render, "ParticleGPU: rejected incompatible time-window fields");
|
||||
return;
|
||||
}
|
||||
const bool reuseOldT0 = !particle::fieldIdentityChanged(oldT0, nextT0);
|
||||
const bool reuseOldT1 = !particle::fieldIdentityChanged(oldT1, nextT0);
|
||||
const bool reuseNewT0 = !particle::fieldIdentityChanged(oldT0, nextT1);
|
||||
const bool reuseNewT1 = !particle::fieldIdentityChanged(oldT1, nextT1);
|
||||
const bool windowOverlap = reuseOldT0 || reuseOldT1 || reuseNewT0 || reuseNewT1;
|
||||
const auto upload = [&](const particle::ParticleFieldSnapshot& field,
|
||||
std::shared_ptr<PremultipliedImage>& image,
|
||||
gfx::Texture2DPtr& texture) {
|
||||
image = makeVelocityFieldImage(field);
|
||||
texture = context.createTexture2D();
|
||||
if (!image || !texture) return false;
|
||||
texture->setImage(image);
|
||||
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
++resources->fieldUploadCount;
|
||||
return true;
|
||||
};
|
||||
auto takeSlot = [&](const particle::ParticleFieldSnapshot& wanted,
|
||||
particle::ParticleFieldSnapshot& assigned,
|
||||
std::shared_ptr<PremultipliedImage>& image,
|
||||
gfx::Texture2DPtr& texture) {
|
||||
if (!particle::fieldIdentityChanged(oldT0, wanted)) {
|
||||
assigned = oldT0;
|
||||
image = resources->fieldImage;
|
||||
texture = resources->fieldTexture;
|
||||
return true;
|
||||
}
|
||||
if (!particle::fieldIdentityChanged(oldT1, wanted)) {
|
||||
assigned = oldT1;
|
||||
image = resources->nextFieldImage;
|
||||
texture = resources->nextFieldTexture;
|
||||
return true;
|
||||
}
|
||||
assigned = wanted;
|
||||
return upload(assigned, image, texture);
|
||||
};
|
||||
particle::ParticleFieldSnapshot assignedT0;
|
||||
particle::ParticleFieldSnapshot assignedT1;
|
||||
std::shared_ptr<PremultipliedImage> imageT0;
|
||||
std::shared_ptr<PremultipliedImage> imageT1;
|
||||
gfx::Texture2DPtr textureT0;
|
||||
gfx::Texture2DPtr textureT1;
|
||||
if (!takeSlot(nextT0, assignedT0, imageT0, textureT0) ||
|
||||
!takeSlot(nextT1, assignedT1, imageT1, textureT1)) {
|
||||
Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed");
|
||||
return;
|
||||
}
|
||||
texture->setImage(image);
|
||||
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
||||
.wrapU = gfx::TextureWrapType::Repeat,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
resources->field = std::move(field);
|
||||
resources->fieldImage = std::move(image);
|
||||
resources->fieldTexture = std::move(texture);
|
||||
resources->fieldGeneration = fieldGeneration;
|
||||
resources->gpuFieldGeneration = fieldGeneration;
|
||||
resources->field = std::move(assignedT0);
|
||||
resources->nextField = std::move(assignedT1);
|
||||
resources->fieldImage = std::move(imageT0);
|
||||
resources->nextFieldImage = std::move(imageT1);
|
||||
resources->fieldTexture = std::move(textureT0);
|
||||
resources->nextFieldTexture = std::move(textureT1);
|
||||
resources->fieldGeneration = resources->field.generation;
|
||||
resources->nextFieldGeneration = resources->nextField.generation;
|
||||
resources->gpuFieldGeneration = resources->fieldGeneration;
|
||||
resources->gpuNextFieldGeneration = resources->nextFieldGeneration;
|
||||
resources->fixtureMode = fixtureMode;
|
||||
resources->resetSequence();
|
||||
if (!windowOverlap) resources->resetSequence();
|
||||
Log::Info(Event::Render,
|
||||
"ParticleRender: fixture switched from=" + oldFixtureID + " to=" + resources->field.fixtureID +
|
||||
" oldGeneration=" + std::to_string(oldFieldGeneration) +
|
||||
" newGeneration=" + std::to_string(resources->fieldGeneration) +
|
||||
" gpuFieldGeneration=" + std::to_string(resources->gpuFieldGeneration) +
|
||||
" trail=cleared stateSeed=INITIALIZE upload reason=FIELD_CHANGED");
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: fixture validated id=" + resources->field.fixtureID +
|
||||
" noData=" + std::to_string(resources->field.noData) +
|
||||
" velocityCount=" + std::to_string(resources->field.velocity.size()));
|
||||
"ParticleGPU: time window advance T0=" + oldT0.fixtureID + " T1=" + oldT1.fixtureID +
|
||||
" -> T0=" + resources->field.fixtureID + " T1=" + resources->nextField.fixtureID +
|
||||
" reuse=" + (windowOverlap ? "true" : "false") +
|
||||
" uploads=" + std::to_string(resources->fieldUploadCount) + " simulationRebuild=" +
|
||||
(windowOverlap ? "false" : "true") + " trailCleared=" + (windowOverlap ? "false" : "true"));
|
||||
} catch (const std::exception& error) {
|
||||
Log::Error(Event::Render, "ParticleField: fixture switch failed: " + std::string(error.what()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto requestedTimeMix = std::clamp(properties.timeMix, 0.0f, 1.0f);
|
||||
if (resources->timeMix != requestedTimeMix) {
|
||||
resources->timeMix = requestedTimeMix;
|
||||
Log::Info(Event::Render,
|
||||
"ParticleGPU: timeMix=" + std::to_string(resources->timeMix) + " T0=" + resources->field.fixtureID +
|
||||
" T1=" + resources->nextField.fixtureID + " uploads=" +
|
||||
std::to_string(resources->fieldUploadCount) + " simulationRebuild=0 trailRebuild=0");
|
||||
}
|
||||
|
||||
if (!resources->lifecycleModeKnown || resources->lifecycleActive != active) {
|
||||
Log::Info(Event::Render,
|
||||
|
||||
@@ -32,6 +32,7 @@ private:
|
||||
|
||||
std::shared_ptr<ParticleGpuResources> gpuResources;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceField;
|
||||
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
|
||||
bool formalSource = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -300,6 +300,7 @@ const std::vector<TextureInfo> ParticleGpuValidationShaderInfo::textures = {
|
||||
TextureInfo{"u_previous", idParticleGpuValidationPreviousTexture},
|
||||
TextureInfo{"u_mask_texture", idParticleGpuValidationMaskTexture},
|
||||
TextureInfo{"u_field_texture", idParticleGpuValidationFieldTexture},
|
||||
TextureInfo{"u_next_field_texture", idParticleGpuValidationNextFieldTexture},
|
||||
TextureInfo{"u_state_texture", idParticleGpuValidationStateTexture},
|
||||
};
|
||||
|
||||
|
||||
@@ -221,7 +221,24 @@ std::optional<std::unique_ptr<Source>> convertRasterArraySource(const std::strin
|
||||
error.message = "raster-array fixture inputs must be strings";
|
||||
return std::nullopt;
|
||||
}
|
||||
return {std::make_unique<RasterArraySource>(id, *fixtureID, *metadataURL, *fieldURL)};
|
||||
const auto nextFixture = objectMember(value, "next-fixture");
|
||||
const auto nextMetadata = objectMember(value, "next-metadata");
|
||||
const auto nextField = objectMember(value, "next-field");
|
||||
if (static_cast<bool>(nextFixture) != static_cast<bool>(nextMetadata) ||
|
||||
static_cast<bool>(nextFixture) != static_cast<bool>(nextField)) {
|
||||
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)};
|
||||
const auto nextFixtureID = toString(*nextFixture);
|
||||
const auto nextMetadataURL = toString(*nextMetadata);
|
||||
const auto nextFieldURL = toString(*nextField);
|
||||
if (!nextFixtureID || !nextMetadataURL || !nextFieldURL) {
|
||||
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)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -6,15 +6,18 @@ namespace mbgl::style {
|
||||
|
||||
class ParticleRenderLayerProperties final : public LayerProperties {
|
||||
public:
|
||||
ParticleRenderLayerProperties(Immutable<Layer::Impl> impl, bool active_, uint8_t maskMode_, uint8_t fixtureMode_)
|
||||
ParticleRenderLayerProperties(
|
||||
Immutable<Layer::Impl> impl, bool active_, uint8_t maskMode_, uint8_t fixtureMode_, float timeMix_)
|
||||
: LayerProperties(std::move(impl)),
|
||||
active(active_),
|
||||
maskMode(maskMode_),
|
||||
fixtureMode(fixtureMode_) {}
|
||||
fixtureMode(fixtureMode_),
|
||||
timeMix(timeMix_) {}
|
||||
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
|
||||
bool active;
|
||||
uint8_t maskMode;
|
||||
uint8_t fixtureMode;
|
||||
float timeMix;
|
||||
};
|
||||
|
||||
} // namespace mbgl::style
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace mbgl::style {
|
||||
namespace {
|
||||
constexpr const char* ActiveProperty = "raster-particle-active";
|
||||
constexpr const char* MaskModeProperty = "raster-particle-mask-mode";
|
||||
constexpr const char* TimeMixProperty = "raster-particle-time-mix";
|
||||
const LayerTypeInfo typeInfo{.type = "raster-particle",
|
||||
.source = LayerTypeInfo::Source::Required,
|
||||
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
|
||||
@@ -58,6 +59,18 @@ void RasterParticleLayer::setMaskMode(uint8_t mode) {
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
float RasterParticleLayer::getTimeMix() const {
|
||||
return impl().timeMix;
|
||||
}
|
||||
void RasterParticleLayer::setTimeMix(float timeMix) {
|
||||
const auto clamped = std::clamp(timeMix, 0.0f, 1.0f);
|
||||
if (clamped != getTimeMix()) {
|
||||
auto next = mutableImpl();
|
||||
next->timeMix = clamped;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
}
|
||||
std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const std::string& name,
|
||||
const conversion::Convertible& value) {
|
||||
conversion::Error error;
|
||||
@@ -74,18 +87,27 @@ std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const
|
||||
setMaskMode(static_cast<uint8_t>(*converted));
|
||||
return std::nullopt;
|
||||
}
|
||||
if (name == TimeMixProperty) {
|
||||
auto converted = conversion::convert<float>(value, error);
|
||||
if (!converted || !std::isfinite(*converted))
|
||||
return conversion::Error{"raster-particle-time-mix must be finite"};
|
||||
setTimeMix(*converted);
|
||||
return std::nullopt;
|
||||
}
|
||||
return conversion::Error{"layer doesn't support this property"};
|
||||
}
|
||||
StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
|
||||
if (name == ActiveProperty) return StyleProperty(Value{isActive()}, StyleProperty::Kind::Constant);
|
||||
if (name == MaskModeProperty)
|
||||
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
|
||||
if (name == TimeMixProperty) return StyleProperty(Value{getTimeMix()}, StyleProperty::Kind::Constant);
|
||||
return {};
|
||||
}
|
||||
Value RasterParticleLayer::serialize() const {
|
||||
auto result = Layer::serialize();
|
||||
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
|
||||
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
|
||||
serializeProperty(result, getProperty(TimeMixProperty), TimeMixProperty, true);
|
||||
return result;
|
||||
}
|
||||
std::unique_ptr<Layer> RasterParticleLayer::cloneRef(const std::string& id) const {
|
||||
|
||||
@@ -14,6 +14,7 @@ public:
|
||||
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override {}
|
||||
bool active = true;
|
||||
uint8_t maskMode = 0;
|
||||
float timeMix = 0.0f;
|
||||
DECLARE_LAYER_TYPE_INFO;
|
||||
};
|
||||
|
||||
|
||||
@@ -24,8 +24,17 @@ std::atomic_uint64_t nextRasterArrayFieldGeneration{0};
|
||||
RasterArraySource::RasterArraySource(std::string id,
|
||||
std::string fixtureID,
|
||||
std::string metadataURL,
|
||||
std::string fieldURL)
|
||||
: Source(makeMutable<Impl>(std::move(id), std::move(fixtureID), std::move(metadataURL), std::move(fieldURL))) {}
|
||||
std::string fieldURL,
|
||||
std::string nextFixtureID,
|
||||
std::string nextMetadataURL,
|
||||
std::string nextFieldURL)
|
||||
: Source(makeMutable<Impl>(std::move(id),
|
||||
std::move(fixtureID),
|
||||
std::move(metadataURL),
|
||||
std::move(fieldURL),
|
||||
std::move(nextFixtureID),
|
||||
std::move(nextMetadataURL),
|
||||
std::move(nextFieldURL))) {}
|
||||
|
||||
RasterArraySource::~RasterArraySource() {
|
||||
weakFactory.invalidateWeakPtrs();
|
||||
@@ -61,21 +70,69 @@ void RasterArraySource::loadDescription(FileSource& fileSource) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void RasterArraySource::tryLoad() {
|
||||
if (!metadata || !fieldBytes) return;
|
||||
if (!metadata || !fieldBytes || (!impl().nextFixtureID.empty() && (!nextMetadata || !nextFieldBytes))) return;
|
||||
try {
|
||||
const auto fieldGeneration = ++nextRasterArrayFieldGeneration;
|
||||
auto field = particle::OfflineFixtureParticleAdapter::load(
|
||||
impl().fixtureID, *metadata, *fieldBytes, fieldGeneration);
|
||||
baseImpl = makeMutable<Impl>(impl(), std::move(field));
|
||||
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);
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -8,24 +8,44 @@ namespace mbgl::style {
|
||||
|
||||
class RasterArraySource::Impl final : public Source::Impl {
|
||||
public:
|
||||
Impl(std::string id, std::string fixtureID_, std::string metadataURL_, std::string fieldURL_)
|
||||
Impl(std::string id,
|
||||
std::string fixtureID_,
|
||||
std::string metadataURL_,
|
||||
std::string fieldURL_,
|
||||
std::string nextFixtureID_ = {},
|
||||
std::string nextMetadataURL_ = {},
|
||||
std::string nextFieldURL_ = {})
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)),
|
||||
fixtureID(std::move(fixtureID_)),
|
||||
metadataURL(std::move(metadataURL_)),
|
||||
fieldURL(std::move(fieldURL_)) {}
|
||||
fieldURL(std::move(fieldURL_)),
|
||||
nextFixtureID(std::move(nextFixtureID_)),
|
||||
nextMetadataURL(std::move(nextMetadataURL_)),
|
||||
nextFieldURL(std::move(nextFieldURL_)) {}
|
||||
Impl(const Impl&) = default;
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_)
|
||||
: Source::Impl(other),
|
||||
fixtureID(other.fixtureID),
|
||||
metadataURL(other.metadataURL),
|
||||
fieldURL(other.fieldURL),
|
||||
nextFixtureID(other.nextFixtureID),
|
||||
nextMetadataURL(other.nextMetadataURL),
|
||||
nextFieldURL(other.nextFieldURL),
|
||||
field(std::move(field_)) {}
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_, particle::ParticleFieldSnapshot nextField_)
|
||||
: Impl(other, std::move(field_)) {
|
||||
nextField = std::move(nextField_);
|
||||
}
|
||||
std::optional<std::string> getAttribution() const override { return {}; }
|
||||
|
||||
std::string fixtureID;
|
||||
std::string metadataURL;
|
||||
std::string fieldURL;
|
||||
std::string nextFixtureID;
|
||||
std::string nextMetadataURL;
|
||||
std::string nextFieldURL;
|
||||
std::optional<particle::ParticleFieldSnapshot> field;
|
||||
std::optional<particle::ParticleFieldSnapshot> nextField;
|
||||
};
|
||||
|
||||
} // namespace mbgl::style
|
||||
|
||||
@@ -219,6 +219,27 @@ TEST(Source, RasterArrayFieldIdentityIncludesFixtureAndDataVersion) {
|
||||
EXPECT_FALSE(particle::fieldIdentityChanged(east, east));
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayTimeWindowKeepsIndependentCompatibleSnapshots) {
|
||||
const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 11);
|
||||
const auto north = particle::OfflineFixtureParticleAdapter::load("uniform-north", 12);
|
||||
const particle::ParticleFieldTimeWindow window{east, north};
|
||||
|
||||
EXPECT_TRUE(window.isValid());
|
||||
EXPECT_NE(window.t0.generation, window.t1.generation);
|
||||
EXPECT_TRUE(window.t0.isTimeInterpolationCompatibleWith(window.t1));
|
||||
EXPECT_TRUE(particle::fieldIdentityChanged(window.t0, window.t1));
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayTimeWindowRejectsIncompatibleGrid) {
|
||||
const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 13);
|
||||
auto incompatible = east;
|
||||
++incompatible.width;
|
||||
const particle::ParticleFieldTimeWindow window{east, incompatible};
|
||||
|
||||
EXPECT_FALSE(east.isTimeInterpolationCompatibleWith(incompatible));
|
||||
EXPECT_FALSE(window.isValid());
|
||||
}
|
||||
|
||||
TEST(Source, LoadingCorrupt) {
|
||||
SourceTest test;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user