#include #include #include #include #include #include using namespace mbgl; namespace { constexpr size_t Header = 112; constexpr size_t Descriptor = 32; constexpr uint64_t Base = 1'700'000'000'000ULL; constexpr uint64_t Hour = 3'600'000ULL; void u16(std::vector& bytes, size_t offset, uint16_t value) { bytes[offset] = uint8_t(value); bytes[offset + 1] = uint8_t(value >> 8); } void u32(std::vector& bytes, size_t offset, uint32_t value) { for (size_t i = 0; i < 4; ++i) bytes[offset + i] = uint8_t(value >> (8 * i)); } void u64(std::vector& bytes, size_t offset, uint64_t value) { for (size_t i = 0; i < 8; ++i) bytes[offset + i] = uint8_t(value >> (8 * i)); } void i64(std::vector& bytes, size_t offset, int64_t value) { u64(bytes, offset, static_cast(value)); } void f32(std::vector& bytes, size_t offset, float value) { uint32_t bits; std::memcpy(&bits, &value, sizeof(bits)); u32(bytes, offset, bits); } void f64(std::vector& bytes, size_t offset, double value) { uint64_t bits; std::memcpy(&bits, &value, sizeof(bits)); u64(bytes, offset, bits); } size_t aligned16(size_t value) { return (value + 15) & ~size_t(15); } uint32_t crc(const std::vector& bytes) { uint32_t result = ~0u; for (size_t i = 0; i < bytes.size(); ++i) { uint8_t value = i >= 76 && i < 80 ? 0 : bytes[i]; result ^= value; for (int bit = 0; bit < 8; ++bit) result = (result >> 1) ^ ((result & 1) ? 0xedb88320u : 0); } return ~result; } struct Plane { uint16_t semantic, unit, group; uint8_t type, valueClass, component; std::vector bytes; }; std::vector qbin(uint64_t validTime, bool scalar = false) { std::vector planes; if (scalar) planes.push_back({99, 77, 0, 3, 0, 0, {1, 2, 3, 4}}); else { planes.push_back({1, 1, 1, 1, 1, 1, {1, 0, 2, 0, 3, 0, 4, 0}}); planes.push_back({2, 1, 1, 1, 1, 2, {5, 0, 6, 0, 7, 0, 8, 0}}); } planes.push_back({3, 0, 0, 6, 3, 0, {15}}); std::sort(planes.begin(), planes.end(), [](const Plane& a, const Plane& b) { return a.semantic < b.semantic; }); size_t payloadLength = 0; for (const auto& plane : planes) payloadLength = aligned16(payloadLength + plane.bytes.size()); std::vector bytes(Header + planes.size() * Descriptor + payloadLength); std::memcpy(bytes.data(), "QBV2", 4); bytes[4] = 2; bytes[5] = 1; u16(bytes, 6, 999); bytes[8] = bytes[9] = bytes[10] = 1; u16(bytes, 12, planes.size()); u32(bytes, 16, 2); u32(bytes, 20, 2); i64(bytes, 24, static_cast(validTime / 1000)); i64(bytes, 32, static_cast(validTime / 1000)); f64(bytes, 40, -10); f64(bytes, 48, -10); f64(bytes, 56, 10); f64(bytes, 64, 10); u32(bytes, 72, payloadLength); u16(bytes, 80, 0xffff); size_t payloadOffset = 0; for (size_t i = 0; i < planes.size(); ++i) { const auto& plane = planes[i]; const size_t descriptor = Header + i * Descriptor; u16(bytes, descriptor, plane.semantic); bytes[descriptor + 2] = plane.type; bytes[descriptor + 3] = 1; f32(bytes, descriptor + 4, 1); f32(bytes, descriptor + 8, 0); u32(bytes, descriptor + 12, 4); u32(bytes, descriptor + 16, payloadOffset); u32(bytes, descriptor + 20, plane.bytes.size()); u16(bytes, descriptor + 24, plane.unit); u16(bytes, descriptor + 26, plane.group); bytes[descriptor + 28] = plane.valueClass; bytes[descriptor + 29] = plane.component; std::copy(plane.bytes.begin(), plane.bytes.end(), bytes.begin() + Header + planes.size() * Descriptor + payloadOffset); payloadOffset = aligned16(payloadOffset + plane.bytes.size()); } u32(bytes, 76, crc(bytes)); return bytes; } particle::ingress::EncodedFieldRuntimeMetadata metadata(const char* id, uint64_t generation, uint64_t version = 1) { return {id, generation, version}; } particle::ResolvedFieldProfileContract windContract() { particle::ResolvedFieldProfileContract result; result.profileID = "test.wind"; 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}}; return result; } particle::ResolvedFieldProfileContract waveContract() { particle::ResolvedFieldProfileContract result; result.profileID = "test.wave"; result.profileVersion = 1; result.requiredPlanes = {{3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}, {99, particle::FieldValueClass::Scalar, 77, 0, particle::FieldComponent::None, true}}; return result; } void submit(style::RasterArraySource& source, const char* key, const char* id, uint64_t time, uint64_t generation, bool scalar = false) { EXPECT_EQ(source.submitEncodedField(key, metadata(id, generation), scalar ? waveContract() : windContract(), qbin(time, scalar)), style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted); } } // namespace TEST(Source, RasterArrayV4HostApiRejectsWithoutPartialMutationAndMapsStoreResults) { style::RasterArraySource source{"v4"}; const auto first = qbin(Base); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::Duplicate); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 2, 2), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision); auto corrupt = first; corrupt[0] = 'X'; EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), windContract(), corrupt), style::RasterArrayEncodedFieldSubmitResult::RejectedDecode); EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), waveContract(), first), style::RasterArrayEncodedFieldSubmitResult::RejectedContract); EXPECT_EQ(source.submitEncodedField("", metadata("wind", 3), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey); submit(source, "wind", "wind", Base + Hour, 3); source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); EXPECT_TRUE(source.resolveParticleProjection().accepted()); } TEST(Source, RasterArrayV4HostApiResolvesIndependentBindingsFromOneForecastClock) { style::RasterArraySource source{"v4"}; submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); submit(source, "wind", "wind", Base + 2 * Hour, 3); submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); submit(source, "wave", "wave", Base + 6 * Hour, 3, true); source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}); source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour + Hour / 2)); const auto color = source.resolveColorProjection(); const auto particles = source.resolveParticleProjection(); ASSERT_TRUE(color.accepted()); ASSERT_TRUE(particles.accepted()); EXPECT_FLOAT_EQ(color.projection->sampleState.mix, 0.5f); EXPECT_FLOAT_EQ(particles.projection->sampleState.mix, 0.5f); EXPECT_NE(color.projection->contentIdentity.t1Coordinate.validTimeMs, particles.projection->contentIdentity.t1Coordinate.validTimeMs); const auto identity = particles.projection->contentIdentity; source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing); ASSERT_TRUE(source.advanceForecastClock(10)); const auto advanced = source.resolveParticleProjection(); ASSERT_TRUE(advanced.accepted()); EXPECT_NE(advanced.projection->sampleState.mix, particles.projection->sampleState.mix); EXPECT_EQ(advanced.projection->contentIdentity, identity); source.setForecastPlaybackState(particle::ForecastPlaybackState::Paused); EXPECT_FALSE(source.advanceForecastClock(10)); EXPECT_TRUE(source.setForecastPlaybackSpeed(0.5f)); EXPECT_TRUE(source.setForecastPlaybackSpeed(1.0f)); EXPECT_TRUE(source.setForecastPlaybackSpeed(2.0f)); } TEST(Source, RasterArrayV4HostApiClearRetainsBindingsAndNeverTouchesV3) { style::RasterArraySource source{"v4"}; submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}); source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour)); ASSERT_TRUE(source.resolveColorProjection().accepted()); EXPECT_TRUE(source.clearEncodedField("wave")); EXPECT_TRUE(source.resolveParticleProjection().accepted()); EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::FieldSeriesMissing); submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); EXPECT_TRUE(source.resolveColorProjection().accepted()); source.clearAllEncodedFields(); EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::FieldSeriesMissing); EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::FieldSeriesMissing); }