fix: preserve canonical color semantics after QBIN decode

This commit is contained in:
tei
2026-09-21 15:52:22 +09:00
parent a4cb727b16
commit 3f7ec99f85
6 changed files with 177 additions and 68 deletions

View File

@@ -5,16 +5,23 @@
#include <optional> #include <optional>
#include <memory> #include <memory>
#include <variant>
namespace mbgl::particle { namespace mbgl::particle {
// Renderer-neutral canonical snapshots. QBIN stays confined to ingress and // Renderer-neutral canonical snapshots. QBIN stays confined to ingress and
// field Core; renderers receive only the existing canonical field shape. // field Core; renderers receive only the existing canonical field shape.
enum class MaterializedColorValueSource : uint8_t {
Scalar,
VectorMagnitude,
};
struct MaterializedColorProjection { struct MaterializedColorProjection {
std::shared_ptr<const ParticleFieldSnapshot> t0; std::shared_ptr<const ParticleFieldSnapshot> t0;
std::shared_ptr<const ParticleFieldSnapshot> t1; std::shared_ptr<const ParticleFieldSnapshot> t1;
ColorProjectionContentIdentity contentIdentity; ColorProjectionContentIdentity contentIdentity;
FieldSampleState sampleState; FieldSampleState sampleState;
MaterializedColorValueSource valueSource = MaterializedColorValueSource::Scalar;
}; };
struct MaterializedParticleProjection { struct MaterializedParticleProjection {

View File

@@ -138,23 +138,76 @@ bool vectorSnapshot(const DecodedFieldFrame& frame, const ParticleBinding& bindi
std::optional<MaterializedColorProjection> materializeColorProjection(const ColorProjection& projection) { std::optional<MaterializedColorProjection> materializeColorProjection(const ColorProjection& projection) {
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; 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 (!p0 || !p1) return {}; if (binding.expectedValueClass == FieldValueClass::VelocityComponent) {
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane)); const auto findComponent = [&](const DecodedFieldFrame& frame, const FieldComponent component) {
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane)); const DecodedFieldPlane* result = nullptr;
if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1)) 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 {};
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) { std::optional<MaterializedParticleProjection> materializeParticleProjection(const ParticleProjection& projection) {
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {};
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane)); auto t0 = std::make_shared<ParticleFieldSnapshot>(
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane)); 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) || if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, *t0) ||
!vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, *t1)) !vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, *t1))
return {}; 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 } // namespace mbgl::particle

View File

@@ -100,7 +100,8 @@ struct Resources {
class Tweaker final : public gfx::DrawableTweaker { class Tweaker final : public gfx::DrawableTweaker {
public: public:
Tweaker(std::shared_ptr<Resources> resources_, int16_t wrap_) 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 init(gfx::Drawable&) override {}
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture); 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; if (!parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) return;
const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source); const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source);
sourceUsesV4Projection = false; sourceUsesV4Projection = false;
sourceProjectionUsesVectorMagnitude = false;
sourceProjectionMix = 0.0f; sourceProjectionMix = 0.0f;
if (source.impl().v4RendererState.color) { if (source.impl().v4RendererState.color) {
const auto& projection = *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; sourceProjectionMix = projection.sampleState.mix;
sourceTimeline.reset(); sourceTimeline.reset();
sourceUsesV4Projection = true; sourceUsesV4Projection = true;
sourceProjectionUsesVectorMagnitude = projection.valueSource ==
particle::MaterializedColorValueSource::VectorMagnitude;
return; return;
} }
sourceTimeline = source.impl().timeline; sourceTimeline = source.impl().timeline;
@@ -242,10 +246,11 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders,
resources.reset(); resources.reset();
return; return;
} }
// V4 ColorBinding has already selected and materialized its scalar plane; // Encoded ingress is decoder-only. After materialization, WeatherColor
// it must not be reinterpreted through the legacy style value source. // consumes the same canonical scalar/vector semantics as the legacy path.
const auto valueSource = sourceUsesV4Projection const auto valueSource = sourceUsesV4Projection
? style::FieldValueSource::Scalar ? (sourceProjectionUsesVectorMagnitude ? style::FieldValueSource::VectorMagnitude
: style::FieldValueSource::Scalar)
: static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource; : static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) { if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables(); if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();

View File

@@ -34,6 +34,7 @@ private:
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline; std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
float sourceProjectionMix = 0.0f; float sourceProjectionMix = 0.0f;
bool sourceUsesV4Projection = false; bool sourceUsesV4Projection = false;
bool sourceProjectionUsesVectorMagnitude = false;
}; };
} // namespace mbgl } // namespace mbgl

View File

@@ -66,8 +66,8 @@ template <class Projection>
bool sameProjection(const std::shared_ptr<const Projection>& a, const std::shared_ptr<const Projection>& b) { 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; if (static_cast<bool>(a) != static_cast<bool>(b)) return false;
return !a || (a->contentIdentity == b->contentIdentity && a->sampleState.mix == b->sampleState.mix && return !a || (a->contentIdentity == b->contentIdentity && a->sampleState.mix == b->sampleState.mix &&
a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && a->t0 && b->t0 &&
a->t0 && b->t0 && a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1)); a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1));
} }
} // namespace } // namespace
@@ -428,8 +428,7 @@ RasterArrayEncodedFieldSubmitResult RasterArraySource::submitEncodedField(
// checks metadata, but validate it here to make the Host ordering explicit. // checks metadata, but validate it here to make the Host ordering explicit.
if (!validEncodedFieldKey(fieldKey)) return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey; if (!validEncodedFieldKey(fieldKey)) return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey;
if (!validRuntimeMetadata(runtimeMetadata)) return RasterArrayEncodedFieldSubmitResult::RejectedDecode; if (!validRuntimeMetadata(runtimeMetadata)) return RasterArrayEncodedFieldSubmitResult::RejectedDecode;
auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate( auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate(runtimeMetadata, &contract, rawQbinBytes);
runtimeMetadata, &contract, rawQbinBytes);
if (ingress.code == particle::ingress::EncodedFieldIngressCode::DecodeRejected) if (ingress.code == particle::ingress::EncodedFieldIngressCode::DecodeRejected)
return RasterArrayEncodedFieldSubmitResult::RejectedDecode; return RasterArrayEncodedFieldSubmitResult::RejectedDecode;
if (ingress.code != particle::ingress::EncodedFieldIngressCode::Accepted) if (ingress.code != particle::ingress::EncodedFieldIngressCode::Accepted)
@@ -501,14 +500,17 @@ void RasterArraySource::refreshV4RendererState() {
if (const auto materialized = resolved.accepted() ? particle::materializeColorProjection(*resolved.projection) if (const auto materialized = resolved.accepted() ? particle::materializeColorProjection(*resolved.projection)
: std::nullopt) : std::nullopt)
next.color = std::make_shared<const particle::MaterializedColorProjection>(std::move(*materialized)); 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) { if (particleBinding) {
const auto resolved = resolveParticleProjection(); 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) : std::nullopt)
next.particle = std::make_shared<const particle::MaterializedParticleProjection>(std::move(*materialized)); 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; const auto& current = impl().v4RendererState;
if (sameProjection(current.color, next.color) && sameProjection(current.particle, next.particle)) return; if (sameProjection(current.color, next.color) && sameProjection(current.particle, next.particle)) return;

View File

@@ -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->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(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.t0->scalar, (std::vector<float>{1, 2, 3, 4}));
EXPECT_EQ(color.valueSource, particle::MaterializedColorValueSource::Scalar);
const auto particleIdentity = particles.contentIdentity; const auto particleIdentity = particles.contentIdentity;
const auto retainedParticleT0 = particles.t0; const auto retainedParticleT0 = particles.t0;
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 4)); 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.particle);
EXPECT_FALSE(source.impl().v4RendererState.color); 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) { TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStableRevision) {
const auto t0 = materializerFrame("scalar", const auto t0 = materializerFrame("scalar",
Base, Base,
@@ -457,8 +498,8 @@ TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStab
particle::ColorBinding binding{ particle::ColorBinding binding{
"scalar", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}; "scalar", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77};
particle::ColorProjection projection; particle::ColorProjection projection;
projection.contentIdentity = projection.contentIdentity = {
{binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"}; binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"};
projection.sampleState = {Base + Hour / 2, 0.5f}; projection.sampleState = {Base + Hour / 2, 0.5f};
projection.t0Frame = t0; projection.t0Frame = t0;
projection.t1Frame = t1; projection.t1Frame = t1;