fix: preserve canonical color semantics after QBIN decode
This commit is contained in:
@@ -5,16 +5,23 @@
|
||||
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
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<const ParticleFieldSnapshot> t0;
|
||||
std::shared_ptr<const ParticleFieldSnapshot> t1;
|
||||
ColorProjectionContentIdentity contentIdentity;
|
||||
FieldSampleState sampleState;
|
||||
MaterializedColorValueSource valueSource = MaterializedColorValueSource::Scalar;
|
||||
};
|
||||
|
||||
struct MaterializedParticleProjection {
|
||||
|
||||
@@ -138,23 +138,76 @@ bool vectorSnapshot(const DecodedFieldFrame& frame, const ParticleBinding& bindi
|
||||
|
||||
std::optional<MaterializedColorProjection> 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);
|
||||
if (!p0 || !p1) return {};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane));
|
||||
if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1))
|
||||
|
||||
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<const DecodedFieldPlane*>(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 {};
|
||||
return MaterializedColorProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState};
|
||||
|
||||
ParticleBinding vectorBinding{
|
||||
ParticleBindingMode::VectorComponents,
|
||||
binding.fieldKey,
|
||||
u0->key,
|
||||
v0->key,
|
||||
{},
|
||||
binding.expectedUnitID,
|
||||
};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t0Frame, u0->contentIdentity + "|" + v0->contentIdentity));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(
|
||||
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<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(
|
||||
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<MaterializedParticleProjection> materializeParticleProjection(const ParticleProjection& projection) {
|
||||
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane));
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(
|
||||
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
|
||||
|
||||
@@ -100,7 +100,8 @@ struct Resources {
|
||||
class Tweaker final : public gfx::DrawableTweaker {
|
||||
public:
|
||||
Tweaker(std::shared_ptr<Resources> 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);
|
||||
@@ -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<const RenderRasterArraySource&>(*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<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
|
||||
if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
|
||||
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
||||
|
||||
@@ -34,6 +34,7 @@ private:
|
||||
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
|
||||
float sourceProjectionMix = 0.0f;
|
||||
bool sourceUsesV4Projection = false;
|
||||
bool sourceProjectionUsesVectorMagnitude = false;
|
||||
};
|
||||
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -66,8 +66,8 @@ template <class Projection>
|
||||
bool sameProjection(const std::shared_ptr<const Projection>& a, const std::shared_ptr<const Projection>& b) {
|
||||
if (static_cast<bool>(a) != static_cast<bool>(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
|
||||
@@ -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)
|
||||
@@ -501,14 +500,17 @@ void RasterArraySource::refreshV4RendererState() {
|
||||
if (const auto materialized = resolved.accepted() ? particle::materializeColorProjection(*resolved.projection)
|
||||
: std::nullopt)
|
||||
next.color = std::make_shared<const particle::MaterializedColorProjection>(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)
|
||||
if (const auto materialized = resolved.accepted()
|
||||
? particle::materializeParticleProjection(*resolved.projection)
|
||||
: std::nullopt)
|
||||
next.particle = std::make_shared<const particle::MaterializedParticleProjection>(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;
|
||||
|
||||
@@ -414,6 +414,7 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching
|
||||
EXPECT_EQ(particles.t0->vector, (std::vector<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
||||
EXPECT_EQ(particles.t0->valid, (std::vector<uint8_t>{1, 1, 1, 1}));
|
||||
EXPECT_EQ(color.t0->scalar, (std::vector<float>{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<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
||||
EXPECT_EQ(colorWithoutParticles.t0->valid, (std::vector<uint8_t>{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;
|
||||
|
||||
Reference in New Issue
Block a user