feat: add v2 multi-plane field ingress

This commit is contained in:
OpenAI Codex
2026-09-11 09:00:29 +08:00
parent 9e9ea07bc7
commit 148bf80fe4
17 changed files with 368 additions and 20 deletions

View File

@@ -94,6 +94,23 @@ particle::ParticleFieldPayloadBundle v2Payload(std::optional<std::vector<char>>
return {.scalar = std::move(scalar), .vector = std::move(vector), .validity = std::move(validity)};
}
std::string v2HostMetadata(const int index, const bool scalar, const bool vector, const char* semantics = "VELOCITY") {
std::string result = "{\"schema\":\"maplibre-particle-field/v2\",\"fieldId\":\"v2-host-" +
std::to_string(index) + "\",\"generation\":" + std::to_string(index + 1) +
",\"validTime\":" + std::to_string(100 + index) +
",\"grid\":{\"width\":2,\"height\":1},\"bounds\":{\"west\":0,\"south\":0,\"east\":1,\"north\":1},\"validity\":{\"encoding\":\"uint8\",\"byteLength\":2}";
if (scalar) result += ",\"scalar\":{\"encoding\":\"float32-le\",\"byteLength\":8,\"noData\":-9999}";
if (vector) result += std::string(",\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"byteLength\":16,\"noData\":-9999,\"semantics\":\"") + semantics + "\"}";
return result + "}";
}
std::string v2LegacyCompatibleHostMetadata(const int index) {
return "{\"schema\":\"maplibre-particle-field/v2\",\"fieldId\":\"v2-compatible-t" +
std::to_string(index) + "\",\"generation\":" + std::to_string(index + 1) +
",\"validTime\":" + std::to_string(1704067200000ULL + index * 3600000ULL) +
",\"grid\":{\"width\":16,\"height\":16},\"bounds\":{\"west\":-180,\"south\":-85,\"east\":180,\"north\":85},\"validity\":{\"encoding\":\"uint8\",\"byteLength\":256},\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"byteLength\":2048,\"noData\":-9999,\"semantics\":\"VELOCITY\"}}";
}
} // namespace
using namespace mbgl;
@@ -545,6 +562,73 @@ TEST(Source, HostFieldSubmissionBuildsOneCoreOwnedTimeline) {
EXPECT_EQ(source.impl().timeline->frame().t0Index, 2u);
}
TEST(Source, HostFieldV2BundleUsesTheSameTimelineIngress) {
SourceTest test;
style::RasterArraySource source{"host-submission"};
source.setObserver(&test.styleObserver);
source.loadDescription(*test.fileSource);
const auto valid = std::vector<char>{1, 1};
const auto scalarT0 = floatBytes({2.0f, 3.0f});
const auto scalarT1 = floatBytes({2.5f, 3.5f});
const auto vectorT0 = floatBytes({1.0f, 0.0f, 0.0f, 1.0f});
const auto vectorT1 = floatBytes({0.5f, 0.0f, 0.0f, 1.0f});
EXPECT_EQ(source.submitHostField(v2HostMetadata(0, true, false), v2Payload(scalarT0, std::nullopt, valid)),
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
EXPECT_EQ(source.submitHostField(v2HostMetadata(1, true, false), v2Payload(scalarT1, std::nullopt, valid)),
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
EXPECT_TRUE(source.impl().field->hasScalar());
EXPECT_FALSE(source.impl().field->hasVector());
EXPECT_TRUE(source.clearHostFields());
EXPECT_EQ(source.submitHostField(v2HostMetadata(2, false, true), v2Payload(std::nullopt, vectorT0, valid)),
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
EXPECT_EQ(source.submitHostField(v2HostMetadata(3, false, true), v2Payload(std::nullopt, vectorT1, valid)),
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
EXPECT_TRUE(source.impl().field->hasVector());
EXPECT_EQ(source.impl().field->vectorSemantics, particle::VectorSemantics::Velocity);
EXPECT_TRUE(source.clearHostFields());
EXPECT_EQ(source.submitHostField(v2HostMetadata(4, true, true, "DIRECTION"), v2Payload(scalarT0, vectorT0, valid)),
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
EXPECT_EQ(source.submitHostField(v2HostMetadata(5, true, true, "DIRECTION"), v2Payload(scalarT1, vectorT1, valid)),
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
EXPECT_EQ(source.impl().field->vectorSemantics, particle::VectorSemantics::Direction);
EXPECT_EQ(source.submitHostField(v2HostMetadata(6, true, true), v2Payload(scalarT1, vectorT1, valid)),
style::RasterArrayHostFieldSubmitResult::RejectedIncompatibleSnapshot);
EXPECT_TRUE(source.clearHostFields());
EXPECT_EQ(source.submitHostField(v2HostMetadata(7, false, true),
particle::ParticleFieldPayloadBundle{.scalar = std::nullopt,
.vector = vectorT0,
.validity = std::nullopt}),
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
EXPECT_EQ(source.submitHostField(v2HostMetadata(8, false, false), v2Payload(std::nullopt, std::nullopt, valid)),
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
}
TEST(Source, HostFieldLegacyAndCompatibleV2VectorOnlyShareOneTimeline) {
SourceTest test;
style::RasterArraySource source{"host-submission"};
source.setObserver(&test.styleObserver);
source.loadDescription(*test.fileSource);
const auto legacyBytes = fixtureBytes("uniform-east", "field.f32");
const std::vector<char> legacyPayload(legacyBytes.begin(), legacyBytes.end());
const std::vector<char> validity(16 * 16, 1);
const auto northBytes = fixtureBytes("uniform-north", "field.f32");
const std::vector<char> nextPayload(northBytes.begin(), northBytes.end());
EXPECT_EQ(source.submitHostField(localMetadata(0), legacyPayload),
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
EXPECT_EQ(source.submitHostField(v2LegacyCompatibleHostMetadata(1),
v2Payload(std::nullopt, nextPayload, validity)),
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
ASSERT_TRUE(source.impl().timeline);
EXPECT_EQ(source.impl().timeline->size(), 2u);
EXPECT_EQ(source.impl().field->vectorSemantics, particle::VectorSemantics::Velocity);
EXPECT_EQ(source.impl().nextField->vectorSemantics, particle::VectorSemantics::Velocity);
}
TEST(Source, HostFieldClearAndTimestampSeekAreFailClosed) {
SourceTest test;
style::RasterArraySource source{"host-submission"};