Files
MapLibreParticleLab/test/source/raster_array_v4_host_api.test.cpp
OpenAI Codex 0759a06104
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
test: close STEP-076 V4 host API audit
2026-09-13 15:02:17 +08:00

260 lines
15 KiB
C++

#include <mbgl/particle/ingress/qbin_v2_decoder.hpp>
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/style/sources/raster_array_source_impl.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstring>
#include <vector>
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<uint8_t>& bytes, size_t offset, uint16_t value) {
bytes[offset] = uint8_t(value); bytes[offset + 1] = uint8_t(value >> 8);
}
void u32(std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t>& bytes, size_t offset, int64_t value) { u64(bytes, offset, static_cast<uint64_t>(value)); }
void f32(std::vector<uint8_t>& bytes, size_t offset, float value) {
uint32_t bits; std::memcpy(&bits, &value, sizeof(bits)); u32(bytes, offset, bits);
}
void f64(std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t> bytes;
};
std::vector<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
std::vector<Plane> 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<uint8_t> 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<int64_t>(validTime / 1000)); i64(bytes, 32, static_cast<int64_t>(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);
}
void expectSameParticleProjection(const particle::ParticleProjection& expected,
const particle::ParticleProjection& actual) {
EXPECT_EQ(actual.contentIdentity, expected.contentIdentity);
EXPECT_EQ(actual.contentIdentity.t0Coordinate, expected.contentIdentity.t0Coordinate);
EXPECT_EQ(actual.contentIdentity.t1Coordinate, expected.contentIdentity.t1Coordinate);
EXPECT_EQ(actual.t0FirstPlaneStorage, expected.t0FirstPlaneStorage);
EXPECT_EQ(actual.t1FirstPlaneStorage, expected.t1FirstPlaneStorage);
EXPECT_EQ(actual.t0SecondPlaneStorage, expected.t0SecondPlaneStorage);
EXPECT_EQ(actual.t1SecondPlaneStorage, expected.t1SecondPlaneStorage);
EXPECT_EQ(actual.t0ValidityStorage, expected.t0ValidityStorage);
EXPECT_EQ(actual.t1ValidityStorage, expected.t1ValidityStorage);
}
} // 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);
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));
const auto baseline = source.resolveParticleProjection();
ASSERT_TRUE(baseline.accepted());
const auto expectNoPartialMutation = [&] {
const auto resolved = source.resolveParticleProjection();
ASSERT_TRUE(resolved.accepted());
expectSameParticleProjection(*baseline.projection, *resolved.projection);
};
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first),
style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision);
expectNoPartialMutation();
auto corrupt = first; corrupt[0] = 'X';
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), windContract(), corrupt),
style::RasterArrayEncodedFieldSubmitResult::RejectedDecode);
expectNoPartialMutation();
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), waveContract(), first),
style::RasterArrayEncodedFieldSubmitResult::RejectedContract);
expectNoPartialMutation();
EXPECT_EQ(source.submitEncodedField("", metadata("wind", 3), windContract(), first),
style::RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey);
expectNoPartialMutation();
}
TEST(Source, RasterArrayV4HostApiRejectsKeepPublicProjectionStable) {
style::RasterArraySource source{"v4"};
const auto t0 = qbin(Base);
const auto t1 = qbin(Base + Hour);
submit(source, "wind", "wind", Base, 2);
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));
const auto baseline = source.resolveParticleProjection();
ASSERT_TRUE(baseline.accepted());
const auto expectNoPartialMutation = [&] {
const auto resolved = source.resolveParticleProjection();
ASSERT_TRUE(resolved.accepted());
expectSameParticleProjection(*baseline.projection, *resolved.projection);
};
auto corrupt = t1;
corrupt[0] = 'X';
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 4), windContract(), corrupt),
style::RasterArrayEncodedFieldSubmitResult::RejectedDecode);
expectNoPartialMutation();
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 4), waveContract(), t1),
style::RasterArrayEncodedFieldSubmitResult::RejectedContract);
expectNoPartialMutation();
EXPECT_EQ(source.submitEncodedField("", metadata("wind", 4), windContract(), t1),
style::RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey);
expectNoPartialMutation();
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), t0),
style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision);
expectNoPartialMutation();
}
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"};
const auto v3T0 = particle::OfflineFixtureParticleAdapter::load("uniform-east", 71);
auto v3T1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 72);
v3T1.validTime = v3T0.validTime + Hour;
ASSERT_EQ(source.submitInitialRuntimeFieldWindow(v3T0, v3T1), style::RasterArrayRuntimeFieldUpdateResult::Applied);
ASSERT_TRUE(source.impl().field);
ASSERT_TRUE(source.impl().nextField);
const auto v3FieldIdentity = source.impl().field->fieldID;
const auto v3FieldGeneration = source.impl().field->generation;
const auto v3FieldValidTime = source.impl().field->validTime;
const auto v3NextIdentity = source.impl().nextField->fieldID;
const auto v3NextGeneration = source.impl().nextField->generation;
const auto v3NextValidTime = source.impl().nextField->validTime;
const auto expectV3Unchanged = [&] {
ASSERT_TRUE(source.impl().field);
ASSERT_TRUE(source.impl().nextField);
EXPECT_EQ(source.impl().field->fieldID, v3FieldIdentity);
EXPECT_EQ(source.impl().field->generation, v3FieldGeneration);
EXPECT_EQ(source.impl().field->validTime, v3FieldValidTime);
EXPECT_EQ(source.impl().nextField->fieldID, v3NextIdentity);
EXPECT_EQ(source.impl().nextField->generation, v3NextGeneration);
EXPECT_EQ(source.impl().nextField->validTime, v3NextValidTime);
};
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);
expectV3Unchanged();
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);
expectV3Unchanged();
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);
EXPECT_TRUE(source.resolveColorProjection().accepted());
EXPECT_TRUE(source.resolveParticleProjection().accepted());
expectV3Unchanged();
}