diff --git a/include/mbgl/particle/field/field_projection_materialization.hpp b/include/mbgl/particle/field/field_projection_materialization.hpp index 341098f29..00025bfb9 100644 --- a/include/mbgl/particle/field/field_projection_materialization.hpp +++ b/include/mbgl/particle/field/field_projection_materialization.hpp @@ -5,16 +5,23 @@ #include #include +#include namespace mbgl::particle { // Renderer-neutral canonical snapshots. QBIN stays confined to ingress and // field Core; renderers receive only the existing canonical field shape. +enum class MaterializedColorValueSource : uint8_t { + Scalar, + VectorMagnitude, +}; + struct MaterializedColorProjection { std::shared_ptr t0; std::shared_ptr t1; ColorProjectionContentIdentity contentIdentity; FieldSampleState sampleState; + MaterializedColorValueSource valueSource = MaterializedColorValueSource::Scalar; }; struct MaterializedParticleProjection { diff --git a/src/mbgl/particle/field/field_projection_materialization.cpp b/src/mbgl/particle/field/field_projection_materialization.cpp index f5aef55f3..2ef7f9785 100644 --- a/src/mbgl/particle/field/field_projection_materialization.cpp +++ b/src/mbgl/particle/field/field_projection_materialization.cpp @@ -138,23 +138,76 @@ bool vectorSnapshot(const DecodedFieldFrame& frame, const ParticleBinding& bindi std::optional materializeColorProjection(const ColorProjection& projection) { if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; - const auto* p0 = findPlane(*projection.t0Frame, projection.contentIdentity.binding.plane); - const auto* p1 = findPlane(*projection.t1Frame, projection.contentIdentity.binding.plane); + + const auto& binding = projection.contentIdentity.binding; + if (binding.expectedValueClass == FieldValueClass::VelocityComponent) { + const auto findComponent = [&](const DecodedFieldFrame& frame, const FieldComponent component) { + const DecodedFieldPlane* result = nullptr; + for (const auto& plane : frame.planes) { + if (plane.valueClass != FieldValueClass::VelocityComponent || plane.unitID != binding.expectedUnitID || + plane.key.groupID != binding.plane.groupID || plane.key.component != component) + continue; + if (result) return static_cast(nullptr); + result = &plane; + } + return result; + }; + + const auto* u0 = findComponent(*projection.t0Frame, FieldComponent::UEast); + const auto* v0 = findComponent(*projection.t0Frame, FieldComponent::VNorth); + const auto* u1 = findComponent(*projection.t1Frame, FieldComponent::UEast); + const auto* v1 = findComponent(*projection.t1Frame, FieldComponent::VNorth); + if (!u0 || !v0 || !u1 || !v1 || !u0->isInterpolationCompatibleWith(*u1) || + !v0->isInterpolationCompatibleWith(*v1)) + return {}; + + ParticleBinding vectorBinding{ + ParticleBindingMode::VectorComponents, + binding.fieldKey, + u0->key, + v0->key, + {}, + binding.expectedUnitID, + }; + auto t0 = std::make_shared( + baseSnapshot(*projection.t0Frame, u0->contentIdentity + "|" + v0->contentIdentity)); + auto t1 = std::make_shared( + baseSnapshot(*projection.t1Frame, u1->contentIdentity + "|" + v1->contentIdentity)); + if (!vectorSnapshot(*projection.t0Frame, vectorBinding, *t0) || + !vectorSnapshot(*projection.t1Frame, vectorBinding, *t1)) + return {}; + return MaterializedColorProjection{std::move(t0), + std::move(t1), + projection.contentIdentity, + projection.sampleState, + MaterializedColorValueSource::VectorMagnitude}; + } + + const auto* p0 = findPlane(*projection.t0Frame, binding.plane); + const auto* p1 = findPlane(*projection.t1Frame, binding.plane); if (!p0 || !p1) return {}; - auto t0 = std::make_shared(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane)); - auto t1 = std::make_shared(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane)); - if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1)) - return {}; - return MaterializedColorProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState}; + auto t0 = std::make_shared( + baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane)); + auto t1 = std::make_shared( + baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane)); + if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1)) return {}; + return MaterializedColorProjection{std::move(t0), + std::move(t1), + projection.contentIdentity, + projection.sampleState, + MaterializedColorValueSource::Scalar}; } std::optional materializeParticleProjection(const ParticleProjection& projection) { if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; - auto t0 = std::make_shared(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane)); - auto t1 = std::make_shared(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane)); + auto t0 = std::make_shared( + baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane)); + auto t1 = std::make_shared( + baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane)); if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, *t0) || !vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, *t1)) return {}; - return MaterializedParticleProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState}; + return MaterializedParticleProjection{ + std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState}; } } // namespace mbgl::particle diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index 99e37df9f..04198ad7b 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -46,13 +46,13 @@ float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSourc float result = 1.0f; for (size_t index = 0; index < field.valid.size(); ++index) if (field.valid[index]) { - if (valueSource == style::FieldValueSource::Scalar) { - result = std::max(result, std::abs(field.scalar[index])); - } else { - result = std::max(result, std::abs(field.vector[index * 2])); - result = std::max(result, std::abs(field.vector[index * 2 + 1])); + if (valueSource == style::FieldValueSource::Scalar) { + result = std::max(result, std::abs(field.scalar[index])); + } else { + result = std::max(result, std::abs(field.vector[index * 2])); + result = std::max(result, std::abs(field.vector[index * 2 + 1])); + } } - } return result; } @@ -62,17 +62,17 @@ std::shared_ptr image(const particle::ParticleFieldSnapshot& auto result = std::make_shared(Size{field.width, field.height}); for (uint32_t y = 0; y < field.height; ++y) for (uint32_t x = 0; x < field.width; ++x) { - const auto cell = static_cast(y) * field.width + x; - const auto pixel = (static_cast(y) * field.width + x) * PremultipliedImage::channels; + const auto cell = static_cast(y) * field.width + x; + const auto pixel = (static_cast(y) * field.width + x) * PremultipliedImage::channels; const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] : field.vector[cell * 2]; - const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1]; - result->data[pixel] = static_cast(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); + const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1]; + result->data[pixel] = static_cast(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); result->data[pixel + 1] = static_cast(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); - result->data[pixel + 2] = 0; - result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0; - } + result->data[pixel + 2] = 0; + result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0; + } return result; } @@ -100,7 +100,8 @@ struct Resources { class Tweaker final : public gfx::DrawableTweaker { public: Tweaker(std::shared_ptr resources_, int16_t wrap_) - : resources(std::move(resources_)), wrap(wrap_) {} + : resources(std::move(resources_)), + wrap(wrap_) {} void init(gfx::Drawable&) override {} void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture); @@ -154,8 +155,8 @@ bool uploadField(gfx::Context& context, if (!outputTexture) return false; outputTexture->setImage(outputImage); outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear, - .wrapU = gfx::TextureWrapType::Clamp, - .wrapV = gfx::TextureWrapType::Clamp}); + .wrapU = gfx::TextureWrapType::Clamp, + .wrapV = gfx::TextureWrapType::Clamp}); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload); return true; } @@ -199,6 +200,7 @@ void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters) if (!parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) return; const auto& source = static_cast(*parameters.source); sourceUsesV4Projection = false; + sourceProjectionUsesVectorMagnitude = false; sourceProjectionMix = 0.0f; if (source.impl().v4RendererState.color) { const auto& projection = *source.impl().v4RendererState.color; @@ -207,6 +209,8 @@ void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters) sourceProjectionMix = projection.sampleState.mix; sourceTimeline.reset(); sourceUsesV4Projection = true; + sourceProjectionUsesVectorMagnitude = projection.valueSource == + particle::MaterializedColorValueSource::VectorMagnitude; return; } sourceTimeline = source.impl().timeline; @@ -242,10 +246,11 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, resources.reset(); return; } - // V4 ColorBinding has already selected and materialized its scalar plane; - // it must not be reinterpreted through the legacy style value source. + // Encoded ingress is decoder-only. After materialization, WeatherColor + // consumes the same canonical scalar/vector semantics as the legacy path. const auto valueSource = sourceUsesV4Projection - ? style::FieldValueSource::Scalar + ? (sourceProjectionUsesVectorMagnitude ? style::FieldValueSource::VectorMagnitude + : style::FieldValueSource::Scalar) : static_cast(*baseImpl).valueSource; if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.hpp b/src/mbgl/renderer/layers/render_weather_color_layer.hpp index 424030d8d..7b62c6528 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.hpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.hpp @@ -34,6 +34,7 @@ private: std::shared_ptr sourceTimeline; float sourceProjectionMix = 0.0f; bool sourceUsesV4Projection = false; + bool sourceProjectionUsesVectorMagnitude = false; }; } // namespace mbgl diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index 9aca4ebfa..5b4f49631 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -39,21 +39,21 @@ RasterArrayEncodedFieldSubmitResult mapEncodedFieldStoreResult(const particle::F switch (result) { case particle::FieldSeriesSubmitResult::Inserted: return RasterArrayEncodedFieldSubmitResult::AcceptedInserted; - case particle::FieldSeriesSubmitResult::ReplacedRevision: - return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision; + case particle::FieldSeriesSubmitResult::ReplacedRevision: + return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision; case particle::FieldSeriesSubmitResult::Duplicate: return RasterArrayEncodedFieldSubmitResult::Duplicate; - case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey: - return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey; - case particle::FieldSeriesSubmitResult::RejectedIdentityConflict: - return RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict; - case particle::FieldSeriesSubmitResult::RejectedStaleRevision: - return RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision; - case particle::FieldSeriesSubmitResult::RejectedIncompatibleFrame: - case particle::FieldSeriesSubmitResult::RejectedInvalidFrame: - return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible; - case particle::FieldSeriesSubmitResult::RejectedCapacity: - return RasterArrayEncodedFieldSubmitResult::RejectedCapacity; + case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey: + return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey; + case particle::FieldSeriesSubmitResult::RejectedIdentityConflict: + return RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict; + case particle::FieldSeriesSubmitResult::RejectedStaleRevision: + return RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision; + case particle::FieldSeriesSubmitResult::RejectedIncompatibleFrame: + case particle::FieldSeriesSubmitResult::RejectedInvalidFrame: + return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible; + case particle::FieldSeriesSubmitResult::RejectedCapacity: + return RasterArrayEncodedFieldSubmitResult::RejectedCapacity; } return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible; } @@ -66,8 +66,8 @@ template bool sameProjection(const std::shared_ptr& a, const std::shared_ptr& b) { if (static_cast(a) != static_cast(b)) return false; return !a || (a->contentIdentity == b->contentIdentity && a->sampleState.mix == b->sampleState.mix && - a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && - a->t0 && b->t0 && a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1)); + a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && a->t0 && b->t0 && + a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1)); } } // namespace @@ -207,7 +207,7 @@ RasterArrayCadenceAdvance RasterArrayTimelineRuntime::advanceIfDue(const std::ch lastCadenceLog = now; Log::Info(Event::Render, "WeatherCadence: targetFps=" + std::to_string(static_cast(targetFps_)) + - " weatherTicks=" + std::to_string(cadenceTickCount)); + " weatherTicks=" + std::to_string(cadenceTickCount)); } if (lastTick == std::chrono::steady_clock::time_point{}) { lastTick = now; @@ -341,8 +341,8 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) { const auto previousState = state; const auto upper = std::upper_bound( entries.begin(), entries.end(), timestamp, [](uint64_t value, const Entry& entry) { - return value < entry.timestamp; - }); + return value < entry.timestamp; + }); if (upper == entries.begin()) return false; if (upper == entries.end()) { t0 = entries.size() - 2; @@ -353,7 +353,7 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) { const auto duration = static_cast(entries[t0 + 1].timestamp - entries[t0].timestamp); mix = duration > 0.0f ? static_cast(timestamp - entries[t0].timestamp) / duration : 0.0f; state = previousState == RasterArrayPlaybackState::Playing ? RasterArrayPlaybackState::Playing - : RasterArrayPlaybackState::Paused; + : RasterArrayPlaybackState::Paused; } if (t0 != previousT0) ++windowRevision; lastTick = std::chrono::steady_clock::now(); @@ -428,8 +428,7 @@ RasterArrayEncodedFieldSubmitResult RasterArraySource::submitEncodedField( // checks metadata, but validate it here to make the Host ordering explicit. if (!validEncodedFieldKey(fieldKey)) return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey; if (!validRuntimeMetadata(runtimeMetadata)) return RasterArrayEncodedFieldSubmitResult::RejectedDecode; - auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate( - runtimeMetadata, &contract, rawQbinBytes); + auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate(runtimeMetadata, &contract, rawQbinBytes); if (ingress.code == particle::ingress::EncodedFieldIngressCode::DecodeRejected) return RasterArrayEncodedFieldSubmitResult::RejectedDecode; if (ingress.code != particle::ingress::EncodedFieldIngressCode::Accepted) @@ -499,16 +498,19 @@ void RasterArraySource::refreshV4RendererState() { if (colorBinding) { const auto resolved = resolveColorProjection(); if (const auto materialized = resolved.accepted() ? particle::materializeColorProjection(*resolved.projection) - : std::nullopt) + : std::nullopt) next.color = std::make_shared(std::move(*materialized)); - if (next.color) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize); + if (next.color) + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize); } if (particleBinding) { const auto resolved = resolveParticleProjection(); - if (const auto materialized = resolved.accepted() ? particle::materializeParticleProjection(*resolved.projection) - : std::nullopt) + if (const auto materialized = resolved.accepted() + ? particle::materializeParticleProjection(*resolved.projection) + : std::nullopt) next.particle = std::make_shared(std::move(*materialized)); - if (next.particle) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize); + if (next.particle) + particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize); } const auto& current = impl().v4RendererState; if (sameProjection(current.color, next.color) && sameProjection(current.particle, next.particle)) return; @@ -691,7 +693,7 @@ void RasterArraySource::tryLoad() { " dataVersion=" + std::to_string(loadedField.dataVersion)); if (impl().nextField) { Log::Info(Event::Render, - "ParticleField: source loaded next fieldId=" + impl().nextField->fieldID + + "ParticleField: source loaded next fieldId=" + impl().nextField->fieldID + " generation=" + std::to_string(impl().nextField->generation) + " dataVersion=" + std::to_string(impl().nextField->dataVersion)); } @@ -715,7 +717,7 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) { candidate.snapshot = particle::OfflineFixtureParticleAdapter::load(candidate.descriptor.fixtureID, *candidate.metadata, *candidate.fieldBytes, - ++nextRasterArrayFieldGeneration); + ++nextRasterArrayFieldGeneration); Log::Info(Event::Render, "ParticleField: runtime candidate ready fieldId=" + candidate.snapshot->fieldID + " dataVersion=" + std::to_string(candidate.snapshot->dataVersion)); @@ -725,8 +727,8 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) { "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + " candidateIndex=" + std::to_string(index) + " fieldId=" + candidate.descriptor.fixtureID + " generation=" + std::to_string(candidate.snapshot ? candidate.snapshot->generation : 0) + - " validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) + - " error=" + error.what()); + " validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) + + " error=" + error.what()); observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what()))); } } @@ -747,9 +749,9 @@ void RasterArraySource::tryLoadLocalField(const size_t index) { candidate.snapshot = std::move(*result.snapshot); Log::Info(Event::Render, "ParticleField: event=PRODUCTION_FEED_LOAD fieldId=" + candidate.snapshot->fieldID + - " generation=" + std::to_string(candidate.snapshot->generation) + - " validTime=" + std::to_string(candidate.snapshot->validTime) + - " payloadBytes=" + std::to_string(candidate.fieldBytes->size())); + " generation=" + std::to_string(candidate.snapshot->generation) + + " validTime=" + std::to_string(candidate.snapshot->validTime) + + " payloadBytes=" + std::to_string(candidate.fieldBytes->size())); tryInitializeTimeline(); } @@ -796,7 +798,7 @@ void RasterArraySource::tryInitializeTimeline() { auto field = *runtimeFields[index].snapshot; entries.push_back({std::make_shared(std::move(field)), runtimeFields[index].descriptor.timestamp ? runtimeFields[index].descriptor.timestamp - : 80000 + static_cast(index) * 40000}); + : 80000 + static_cast(index) * 40000}); } try { timeline = std::make_shared(std::move(entries)); @@ -1112,7 +1114,7 @@ bool RasterArraySource::seekRuntimeTimelineToTimestamp(const uint64_t timestamp) if (!accepted) { Log::Warning(Event::Render, "WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=OUT_OF_RANGE sourceId=" + getID() + - " timestamp=" + std::to_string(timestamp)); + " timestamp=" + std::to_string(timestamp)); } return accepted; } diff --git a/test/source/raster_array_v4_host_api.test.cpp b/test/source/raster_array_v4_host_api.test.cpp index 0f0838321..5a51d91f9 100644 --- a/test/source/raster_array_v4_host_api.test.cpp +++ b/test/source/raster_array_v4_host_api.test.cpp @@ -124,8 +124,8 @@ particle::ResolvedFieldProfileContract windContract() { result.profileVersion = 1; result.requiredPlanes = { {1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true}, - {2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true}, - {3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}}; + {2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true}, + {3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}}; return result; } particle::ResolvedFieldProfileContract waveContract() { @@ -414,6 +414,7 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching EXPECT_EQ(particles.t0->vector, (std::vector{1, 5, 2, 6, 3, 7, 4, 8})); EXPECT_EQ(particles.t0->valid, (std::vector{1, 1, 1, 1})); EXPECT_EQ(color.t0->scalar, (std::vector{1, 2, 3, 4})); + EXPECT_EQ(color.valueSource, particle::MaterializedColorValueSource::Scalar); const auto particleIdentity = particles.contentIdentity; const auto retainedParticleT0 = particles.t0; ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 4)); @@ -429,6 +430,46 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching EXPECT_FALSE(source.impl().v4RendererState.particle); EXPECT_FALSE(source.impl().v4RendererState.color); } +TEST(Source, RasterArrayV4VelocityColorReconstructsDecodedUVIndependentlyOfParticles) { + style::RasterArraySource source{"v4-vector-color"}; + submit(source, "wind", "wind", Base, 1); + submit(source, "wind", "wind", Base + Hour, 2); + + source.setColorBinding( + {"wind", {1, 1, particle::FieldComponent::UEast}, particle::FieldValueClass::VelocityComponent, 1}); + ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); + + ASSERT_TRUE(source.impl().v4RendererState.color); + EXPECT_FALSE(source.impl().v4RendererState.particle); + const auto& colorWithoutParticles = *source.impl().v4RendererState.color; + EXPECT_EQ(colorWithoutParticles.valueSource, particle::MaterializedColorValueSource::VectorMagnitude); + ASSERT_TRUE(colorWithoutParticles.t0); + ASSERT_TRUE(colorWithoutParticles.t1); + EXPECT_TRUE(colorWithoutParticles.t0->hasVector()); + EXPECT_FALSE(colorWithoutParticles.t0->hasScalar()); + EXPECT_EQ(colorWithoutParticles.t0->vector, (std::vector{1, 5, 2, 6, 3, 7, 4, 8})); + EXPECT_EQ(colorWithoutParticles.t0->valid, (std::vector{1, 1, 1, 1})); + EXPECT_FLOAT_EQ(colorWithoutParticles.sampleState.mix, 0.5f); + + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, + "wind", + {1, 1, particle::FieldComponent::UEast}, + {2, 1, particle::FieldComponent::VNorth}, + {}, + 1}); + ASSERT_TRUE(source.impl().v4RendererState.color); + ASSERT_TRUE(source.impl().v4RendererState.particle); + const auto& color = *source.impl().v4RendererState.color; + const auto& particles = *source.impl().v4RendererState.particle; + EXPECT_EQ(color.valueSource, particle::MaterializedColorValueSource::VectorMagnitude); + EXPECT_EQ(color.t0->vector, particles.t0->vector); + EXPECT_EQ(color.t1->vector, particles.t1->vector); + EXPECT_EQ(color.t0->valid, particles.t0->valid); + EXPECT_FALSE(source.impl().field); + EXPECT_FALSE(source.impl().nextField); + EXPECT_FALSE(source.impl().timeline); +} + TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStableRevision) { const auto t0 = materializerFrame("scalar", Base, @@ -457,8 +498,8 @@ TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStab particle::ColorBinding binding{ "scalar", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}; particle::ColorProjection projection; - projection.contentIdentity = - {binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"}; + projection.contentIdentity = { + binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"}; projection.sampleState = {Base + Hour / 2, 0.5f}; projection.t0Frame = t0; projection.t1Frame = t1;