feat(step098): single-frame display and keyed rolling cache
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
using namespace mbgl;
|
||||
@@ -63,7 +64,8 @@ struct Plane {
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
std::vector<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
|
||||
std::vector<uint8_t> qbin(uint64_t validTime, bool scalar = false, uint8_t validity = 0x0f,
|
||||
uint64_t referenceTime = Base) {
|
||||
std::vector<Plane> planes;
|
||||
if (scalar)
|
||||
planes.push_back({99, 77, 0, 3, 0, 0, {1, 2, 3, 4}});
|
||||
@@ -71,7 +73,15 @@ std::vector<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
|
||||
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}});
|
||||
for (auto& plane : planes) {
|
||||
if (plane.type == 6) continue;
|
||||
for (size_t cell = 0; cell < 4; ++cell) {
|
||||
if (validity & (1u << cell)) continue;
|
||||
const size_t bytesPerCell = plane.type == 1 || plane.type == 2 ? 2 : plane.type == 5 ? 4 : 1;
|
||||
std::fill_n(plane.bytes.begin() + cell * bytesPerCell, bytesPerCell, uint8_t{0});
|
||||
}
|
||||
}
|
||||
planes.push_back({3, 0, 0, 6, 3, 0, {validity}});
|
||||
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());
|
||||
@@ -84,7 +94,7 @@ std::vector<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
|
||||
u16(bytes, 12, planes.size());
|
||||
u32(bytes, 16, 2);
|
||||
u32(bytes, 20, 2);
|
||||
i64(bytes, 24, static_cast<int64_t>(validTime / 1000));
|
||||
i64(bytes, 24, static_cast<int64_t>(referenceTime / 1000));
|
||||
i64(bytes, 32, static_cast<int64_t>(validTime / 1000));
|
||||
f64(bytes, 40, -10);
|
||||
f64(bytes, 48, -10);
|
||||
@@ -117,6 +127,11 @@ std::vector<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void recomputeQbinCrc(std::vector<uint8_t>& bytes) {
|
||||
u32(bytes, 76, 0);
|
||||
u32(bytes, 76, crc(bytes));
|
||||
}
|
||||
|
||||
particle::ingress::EncodedFieldRuntimeMetadata metadata(const char* id, uint64_t generation, uint64_t version = 1) {
|
||||
return {id, generation, version};
|
||||
}
|
||||
@@ -296,6 +311,62 @@ TEST(Source, RasterArrayV4HostApiRejectsKeepPublicProjectionStable) {
|
||||
expectNoPartialMutation();
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4RejectsNumericOverflowBeforeReplacingAcceptedRevision) {
|
||||
style::RasterArraySource source{"v4-numeric-transaction"};
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
submit(source, "wind", "wind", Base + Hour, 2);
|
||||
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 materialized = source.impl().v4RendererState.particle;
|
||||
ASSERT_TRUE(materialized);
|
||||
const auto countersBefore = particle::rendererDebugCounterSnapshot();
|
||||
|
||||
auto overflowing = qbin(Base + Hour);
|
||||
const size_t firstPlaneDescriptor = Header;
|
||||
f32(overflowing, firstPlaneDescriptor + 4, std::numeric_limits<float>::max());
|
||||
f32(overflowing, firstPlaneDescriptor + 8, std::numeric_limits<float>::max());
|
||||
recomputeQbinCrc(overflowing);
|
||||
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3, 2), windContract(), overflowing),
|
||||
style::RasterArrayEncodedFieldSubmitResult::RejectedContract);
|
||||
|
||||
const auto after = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(after.accepted());
|
||||
expectSameParticleProjection(*baseline.projection, *after.projection);
|
||||
EXPECT_EQ(after.projection->t1Frame, baseline.projection->t1Frame);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, materialized);
|
||||
const auto countersAfter = particle::rendererDebugCounterSnapshot();
|
||||
EXPECT_EQ(countersAfter.v4ProjectionMaterialize, countersBefore.v4ProjectionMaterialize);
|
||||
EXPECT_EQ(countersAfter.fieldTextureUpload, countersBefore.fieldTextureUpload);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4KeepsValidityZeroCellsAsLegalNoData) {
|
||||
style::RasterArraySource source{"v4-validity-nodata"};
|
||||
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), qbin(Base, false, 0x0e)),
|
||||
style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted);
|
||||
submit(source, "wind", "wind", Base + Hour, 2);
|
||||
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 projection = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(projection.accepted());
|
||||
const auto snapshots = particle::materializeParticleProjection(*projection.projection);
|
||||
ASSERT_TRUE(snapshots);
|
||||
ASSERT_EQ(snapshots->t0->valid.size(), 4u);
|
||||
EXPECT_EQ(snapshots->t0->valid[0], 0);
|
||||
EXPECT_EQ(snapshots->t0->valid[1], 1);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4HostApiResolvesIndependentBindingsFromOneForecastClock) {
|
||||
style::RasterArraySource source{"v4"};
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
@@ -573,6 +644,128 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching
|
||||
EXPECT_FALSE(source.impl().v4RendererState.particle);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.color);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4SingleExactFrameMaterializesAndSecondFrameEnablesInterpolation) {
|
||||
style::RasterArraySource source{"v4-single-first"};
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
||||
"wind",
|
||||
{1, 1, particle::FieldComponent::UEast},
|
||||
{2, 1, particle::FieldComponent::VNorth},
|
||||
{},
|
||||
1});
|
||||
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
particle::FieldValueClass::VelocityComponent, 1});
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base));
|
||||
ASSERT_TRUE(source.resolveParticleProjection().accepted());
|
||||
ASSERT_TRUE(source.resolveColorProjection().accepted());
|
||||
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
||||
ASSERT_TRUE(source.impl().v4RendererState.color);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->t0, source.impl().v4RendererState.particle->t1);
|
||||
EXPECT_EQ(source.impl().v4RendererState.color->t0, source.impl().v4RendererState.color->t1);
|
||||
EXPECT_FLOAT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.0f);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->t0->validTime, Base);
|
||||
EXPECT_TRUE(source.impl().v4RendererState.particle->t0->hasVector());
|
||||
|
||||
submit(source, "wind", "wind", Base + Hour, 2);
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
|
||||
const auto pair = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(pair.accepted());
|
||||
EXPECT_NE(pair.projection->t0Frame, pair.projection->t1Frame);
|
||||
EXPECT_FLOAT_EQ(pair.projection->sampleState.mix, 0.5f);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->t0->validTime, Base);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->t1->validTime, Base + Hour);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4RollingWindowPreservesAnUnchangedActivePair) {
|
||||
style::RasterArraySource source{"v4-rolling-window"};
|
||||
for (uint64_t index = 0; index < 8; ++index)
|
||||
submit(source, "wind", "wind", Base + index * Hour, index + 1);
|
||||
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));
|
||||
ASSERT_TRUE(source.resolveParticleProjection().accepted());
|
||||
// Source-level test: verify public projection behavior; Core tests own the
|
||||
// internal FieldSeries size/order assertions.
|
||||
const auto retainedProjection = source.impl().v4RendererState.particle;
|
||||
ASSERT_TRUE(retainedProjection);
|
||||
|
||||
submit(source, "wind", "wind", Base + 8 * Hour, 9);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->sampleState.requestedForecastTimeMs,
|
||||
Base + Hour + Hour / 2);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, retainedProjection);
|
||||
|
||||
submit(source, "wind", "wind", Base - Hour, 10);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, retainedProjection);
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base - Hour));
|
||||
const auto pastProjection = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(pastProjection.accepted());
|
||||
ASSERT_TRUE(pastProjection.projection);
|
||||
EXPECT_EQ(pastProjection.projection->t0Frame->frameContentIdentity.validTimeMs, Base - Hour);
|
||||
EXPECT_EQ(pastProjection.projection->t1Frame->frameContentIdentity.validTimeMs, Base - Hour);
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + 8 * Hour));
|
||||
EXPECT_FALSE(source.resolveParticleProjection().accepted());
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4EvictedActiveFrameClearsItsMaterializedProjection) {
|
||||
style::RasterArraySource source{"v4-evicted-active-frame"};
|
||||
for (uint64_t index = 0; index < 8; ++index)
|
||||
submit(source, "wind", "wind", Base + index * Hour, index + 1);
|
||||
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
||||
"wind",
|
||||
{1, 1, particle::FieldComponent::UEast},
|
||||
{2, 1, particle::FieldComponent::VNorth},
|
||||
{},
|
||||
1});
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base));
|
||||
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->t0->validTime, Base);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->t0, source.impl().v4RendererState.particle->t1);
|
||||
|
||||
submit(source, "wind", "wind", Base + 8 * Hour, 9);
|
||||
EXPECT_FALSE(source.resolveParticleProjection().accepted());
|
||||
EXPECT_FALSE(source.impl().v4RendererState.particle);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4StoresNewReferenceAndProfileAsSingleFrameWithoutCrossBatchMix) {
|
||||
style::RasterArraySource source{"v4-batch-boundary"};
|
||||
ASSERT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), qbin(Base)),
|
||||
style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted);
|
||||
auto nextProfile = windContract();
|
||||
nextProfile.profileID = "test.wind.v2";
|
||||
ASSERT_EQ(source.submitEncodedField("wind", metadata("wind", 2), nextProfile,
|
||||
qbin(Base + Hour, false, 0x0f, Base + Hour)),
|
||||
style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted);
|
||||
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_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::WindowUnavailable);
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour));
|
||||
const auto latestExact = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(latestExact.accepted());
|
||||
EXPECT_EQ(latestExact.projection->t0Frame, latestExact.projection->t1Frame);
|
||||
EXPECT_EQ(latestExact.projection->t0Frame->resolvedProfileID, "test.wind.v2");
|
||||
|
||||
ASSERT_EQ(source.submitEncodedField("wind", metadata("wind", 3), nextProfile,
|
||||
qbin(Base + 2 * Hour, false, 0x0f, Base + Hour)),
|
||||
style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted);
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour + Hour / 2));
|
||||
const auto nextPair = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(nextPair.accepted());
|
||||
EXPECT_FLOAT_EQ(nextPair.projection->sampleState.mix, 0.5f);
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base));
|
||||
const auto returnedToEarlierBatch = source.resolveParticleProjection();
|
||||
ASSERT_TRUE(returnedToEarlierBatch.accepted());
|
||||
EXPECT_EQ(returnedToEarlierBatch.projection->t0Frame, returnedToEarlierBatch.projection->t1Frame);
|
||||
}
|
||||
TEST(Source, RasterArrayV4VelocityColorReconstructsDecodedUVIndependentlyOfParticles) {
|
||||
style::RasterArraySource source{"v4-vector-color"};
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
|
||||
Reference in New Issue
Block a user