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 (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 {};
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 {}; if (!p0 || !p1) return {};
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane)); auto t0 = std::make_shared<ParticleFieldSnapshot>(
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane)); baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1)) auto t1 = std::make_shared<ParticleFieldSnapshot>(
return {}; baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane));
return MaterializedColorProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState}; 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

@@ -46,13 +46,13 @@ float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSourc
float result = 1.0f; float result = 1.0f;
for (size_t index = 0; index < field.valid.size(); ++index) for (size_t index = 0; index < field.valid.size(); ++index)
if (field.valid[index]) { if (field.valid[index]) {
if (valueSource == style::FieldValueSource::Scalar) { if (valueSource == style::FieldValueSource::Scalar) {
result = std::max(result, std::abs(field.scalar[index])); result = std::max(result, std::abs(field.scalar[index]));
} else { } else {
result = std::max(result, std::abs(field.vector[index * 2])); result = std::max(result, std::abs(field.vector[index * 2]));
result = std::max(result, std::abs(field.vector[index * 2 + 1])); result = std::max(result, std::abs(field.vector[index * 2 + 1]));
}
} }
}
return result; return result;
} }
@@ -62,17 +62,17 @@ std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot&
auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height}); auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
for (uint32_t y = 0; y < field.height; ++y) for (uint32_t y = 0; y < field.height; ++y)
for (uint32_t x = 0; x < field.width; ++x) { for (uint32_t x = 0; x < field.width; ++x) {
const auto cell = static_cast<size_t>(y) * field.width + x; const auto cell = static_cast<size_t>(y) * field.width + x;
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels; const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell]
: field.vector[cell * 2]; : field.vector[cell * 2];
const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1]; const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1];
result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) *
255.0f); 255.0f);
result->data[pixel + 2] = 0; result->data[pixel + 2] = 0;
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0; result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
} }
return result; return result;
} }
@@ -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);
@@ -154,8 +155,8 @@ bool uploadField(gfx::Context& context,
if (!outputTexture) return false; if (!outputTexture) return false;
outputTexture->setImage(outputImage); outputTexture->setImage(outputImage);
outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear, outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear,
.wrapU = gfx::TextureWrapType::Clamp, .wrapU = gfx::TextureWrapType::Clamp,
.wrapV = gfx::TextureWrapType::Clamp}); .wrapV = gfx::TextureWrapType::Clamp});
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload); particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload);
return true; return true;
} }
@@ -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

@@ -39,21 +39,21 @@ RasterArrayEncodedFieldSubmitResult mapEncodedFieldStoreResult(const particle::F
switch (result) { switch (result) {
case particle::FieldSeriesSubmitResult::Inserted: case particle::FieldSeriesSubmitResult::Inserted:
return RasterArrayEncodedFieldSubmitResult::AcceptedInserted; return RasterArrayEncodedFieldSubmitResult::AcceptedInserted;
case particle::FieldSeriesSubmitResult::ReplacedRevision: case particle::FieldSeriesSubmitResult::ReplacedRevision:
return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision; return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision;
case particle::FieldSeriesSubmitResult::Duplicate: case particle::FieldSeriesSubmitResult::Duplicate:
return RasterArrayEncodedFieldSubmitResult::Duplicate; return RasterArrayEncodedFieldSubmitResult::Duplicate;
case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey: case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey:
return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey; return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey;
case particle::FieldSeriesSubmitResult::RejectedIdentityConflict: case particle::FieldSeriesSubmitResult::RejectedIdentityConflict:
return RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict; return RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict;
case particle::FieldSeriesSubmitResult::RejectedStaleRevision: case particle::FieldSeriesSubmitResult::RejectedStaleRevision:
return RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision; return RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision;
case particle::FieldSeriesSubmitResult::RejectedIncompatibleFrame: case particle::FieldSeriesSubmitResult::RejectedIncompatibleFrame:
case particle::FieldSeriesSubmitResult::RejectedInvalidFrame: case particle::FieldSeriesSubmitResult::RejectedInvalidFrame:
return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible; return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible;
case particle::FieldSeriesSubmitResult::RejectedCapacity: case particle::FieldSeriesSubmitResult::RejectedCapacity:
return RasterArrayEncodedFieldSubmitResult::RejectedCapacity; return RasterArrayEncodedFieldSubmitResult::RejectedCapacity;
} }
return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible; return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible;
} }
@@ -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
@@ -207,7 +207,7 @@ RasterArrayCadenceAdvance RasterArrayTimelineRuntime::advanceIfDue(const std::ch
lastCadenceLog = now; lastCadenceLog = now;
Log::Info(Event::Render, Log::Info(Event::Render,
"WeatherCadence: targetFps=" + std::to_string(static_cast<uint8_t>(targetFps_)) + "WeatherCadence: targetFps=" + std::to_string(static_cast<uint8_t>(targetFps_)) +
" weatherTicks=" + std::to_string(cadenceTickCount)); " weatherTicks=" + std::to_string(cadenceTickCount));
} }
if (lastTick == std::chrono::steady_clock::time_point{}) { if (lastTick == std::chrono::steady_clock::time_point{}) {
lastTick = now; lastTick = now;
@@ -341,8 +341,8 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) {
const auto previousState = state; const auto previousState = state;
const auto upper = std::upper_bound( const auto upper = std::upper_bound(
entries.begin(), entries.end(), timestamp, [](uint64_t value, const Entry& entry) { 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.begin()) return false;
if (upper == entries.end()) { if (upper == entries.end()) {
t0 = entries.size() - 2; t0 = entries.size() - 2;
@@ -353,7 +353,7 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) {
const auto duration = static_cast<float>(entries[t0 + 1].timestamp - entries[t0].timestamp); const auto duration = static_cast<float>(entries[t0 + 1].timestamp - entries[t0].timestamp);
mix = duration > 0.0f ? static_cast<float>(timestamp - entries[t0].timestamp) / duration : 0.0f; mix = duration > 0.0f ? static_cast<float>(timestamp - entries[t0].timestamp) / duration : 0.0f;
state = previousState == RasterArrayPlaybackState::Playing ? RasterArrayPlaybackState::Playing state = previousState == RasterArrayPlaybackState::Playing ? RasterArrayPlaybackState::Playing
: RasterArrayPlaybackState::Paused; : RasterArrayPlaybackState::Paused;
} }
if (t0 != previousT0) ++windowRevision; if (t0 != previousT0) ++windowRevision;
lastTick = std::chrono::steady_clock::now(); 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. // 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)
@@ -499,16 +498,19 @@ void RasterArraySource::refreshV4RendererState() {
if (colorBinding) { if (colorBinding) {
const auto resolved = resolveColorProjection(); const auto resolved = resolveColorProjection();
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()
: std::nullopt) ? particle::materializeParticleProjection(*resolved.projection)
: 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;
@@ -691,7 +693,7 @@ void RasterArraySource::tryLoad() {
" dataVersion=" + std::to_string(loadedField.dataVersion)); " dataVersion=" + std::to_string(loadedField.dataVersion));
if (impl().nextField) { if (impl().nextField) {
Log::Info(Event::Render, 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) + " generation=" + std::to_string(impl().nextField->generation) +
" dataVersion=" + std::to_string(impl().nextField->dataVersion)); " 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.snapshot = particle::OfflineFixtureParticleAdapter::load(candidate.descriptor.fixtureID,
*candidate.metadata, *candidate.metadata,
*candidate.fieldBytes, *candidate.fieldBytes,
++nextRasterArrayFieldGeneration); ++nextRasterArrayFieldGeneration);
Log::Info(Event::Render, Log::Info(Event::Render,
"ParticleField: runtime candidate ready fieldId=" + candidate.snapshot->fieldID + "ParticleField: runtime candidate ready fieldId=" + candidate.snapshot->fieldID +
" dataVersion=" + std::to_string(candidate.snapshot->dataVersion)); " dataVersion=" + std::to_string(candidate.snapshot->dataVersion));
@@ -725,8 +727,8 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) {
"ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() +
" candidateIndex=" + std::to_string(index) + " fieldId=" + candidate.descriptor.fixtureID + " candidateIndex=" + std::to_string(index) + " fieldId=" + candidate.descriptor.fixtureID +
" generation=" + std::to_string(candidate.snapshot ? candidate.snapshot->generation : 0) + " generation=" + std::to_string(candidate.snapshot ? candidate.snapshot->generation : 0) +
" validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) + " validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) +
" error=" + error.what()); " error=" + error.what());
observer->onSourceError(*this, std::make_exception_ptr(std::runtime_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); candidate.snapshot = std::move(*result.snapshot);
Log::Info(Event::Render, Log::Info(Event::Render,
"ParticleField: event=PRODUCTION_FEED_LOAD fieldId=" + candidate.snapshot->fieldID + "ParticleField: event=PRODUCTION_FEED_LOAD fieldId=" + candidate.snapshot->fieldID +
" generation=" + std::to_string(candidate.snapshot->generation) + " generation=" + std::to_string(candidate.snapshot->generation) +
" validTime=" + std::to_string(candidate.snapshot->validTime) + " validTime=" + std::to_string(candidate.snapshot->validTime) +
" payloadBytes=" + std::to_string(candidate.fieldBytes->size())); " payloadBytes=" + std::to_string(candidate.fieldBytes->size()));
tryInitializeTimeline(); tryInitializeTimeline();
} }
@@ -796,7 +798,7 @@ void RasterArraySource::tryInitializeTimeline() {
auto field = *runtimeFields[index].snapshot; auto field = *runtimeFields[index].snapshot;
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(std::move(field)), entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(std::move(field)),
runtimeFields[index].descriptor.timestamp ? runtimeFields[index].descriptor.timestamp runtimeFields[index].descriptor.timestamp ? runtimeFields[index].descriptor.timestamp
: 80000 + static_cast<uint64_t>(index) * 40000}); : 80000 + static_cast<uint64_t>(index) * 40000});
} }
try { try {
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries)); timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
@@ -1112,7 +1114,7 @@ bool RasterArraySource::seekRuntimeTimelineToTimestamp(const uint64_t timestamp)
if (!accepted) { if (!accepted) {
Log::Warning(Event::Render, Log::Warning(Event::Render,
"WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=OUT_OF_RANGE sourceId=" + getID() + "WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=OUT_OF_RANGE sourceId=" + getID() +
" timestamp=" + std::to_string(timestamp)); " timestamp=" + std::to_string(timestamp));
} }
return accepted; return accepted;
} }

View File

@@ -124,8 +124,8 @@ particle::ResolvedFieldProfileContract windContract() {
result.profileVersion = 1; result.profileVersion = 1;
result.requiredPlanes = { result.requiredPlanes = {
{1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true}, {1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true},
{2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true}, {2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true},
{3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}}; {3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}};
return result; return result;
} }
particle::ResolvedFieldProfileContract waveContract() { particle::ResolvedFieldProfileContract waveContract() {
@@ -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;