972 lines
51 KiB
C++
972 lines
51 KiB
C++
#include <mln/particle/ingress/qbin_v2_decoder.hpp>
|
|
#include <mln/particle/field/field_projection_materialization.hpp>
|
|
#include <mln/particle/field/offline_fixture_particle_adapter.hpp>
|
|
#include <mln/particle/renderer_debug_counters.hpp>
|
|
#include <mln/style/sources/raster_array_source.hpp>
|
|
#include <mln/style/sources/raster_array_source_impl.hpp>
|
|
#include <mln/style/source_observer.hpp>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <vector>
|
|
|
|
using namespace mln;
|
|
|
|
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, 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}});
|
|
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}});
|
|
}
|
|
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());
|
|
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>(referenceTime / 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;
|
|
}
|
|
|
|
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};
|
|
}
|
|
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);
|
|
}
|
|
|
|
std::shared_ptr<const particle::DecodedFieldFrame> materializerFrame(const char* fieldID,
|
|
uint64_t validTime,
|
|
uint64_t generation,
|
|
uint64_t dataVersion,
|
|
particle::FieldPlaneKey key,
|
|
particle::FieldDType dtype,
|
|
particle::FieldValueClass valueClass,
|
|
uint16_t unitID,
|
|
float scale,
|
|
float offset,
|
|
std::vector<uint8_t> raw,
|
|
uint8_t validity = 0x0f) {
|
|
auto frame = std::make_shared<particle::DecodedFieldFrame>();
|
|
frame->frameContentIdentity = {fieldID, generation, dataVersion, validTime};
|
|
frame->referenceTimeMs = validTime;
|
|
frame->grid = {1, 1, 1, 1, 2, 2, -10, -10, 10, 10};
|
|
frame->validityContentIdentity = "validity";
|
|
frame->validityBits = std::make_shared<const std::vector<uint8_t>>(std::vector<uint8_t>{validity});
|
|
frame->validityPlane = {{3, 0, particle::FieldComponent::None},
|
|
particle::FieldDType::Bitset,
|
|
particle::FieldValueClass::Validity,
|
|
0,
|
|
true,
|
|
4,
|
|
1.0f,
|
|
0.0f,
|
|
"validity",
|
|
frame->validityBits};
|
|
auto storage = std::make_shared<const std::vector<uint8_t>>(std::move(raw));
|
|
frame->planes.push_back({key, dtype, valueClass, unitID, true, 4, scale, offset, "plane", storage});
|
|
return frame;
|
|
}
|
|
|
|
std::vector<uint8_t> float32LE(std::initializer_list<float> values) {
|
|
std::vector<uint8_t> bytes(values.size() * 4);
|
|
size_t offset = 0;
|
|
for (const float value : values) {
|
|
f32(bytes, offset, value);
|
|
offset += 4;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
std::vector<uint8_t> uint16LE(std::initializer_list<uint16_t> values) {
|
|
std::vector<uint8_t> bytes(values.size() * 2);
|
|
size_t offset = 0;
|
|
for (const uint16_t value : values) {
|
|
u16(bytes, offset, value);
|
|
offset += 2;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
} // 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, 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);
|
|
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, RasterArrayV4ProductionClockTicksOnceForBothBindingsAndResetsOnCommands) {
|
|
style::RasterArraySource source{"v4-clock"};
|
|
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});
|
|
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
|
"wind", {1, 1, particle::FieldComponent::UEast},
|
|
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + 1000));
|
|
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
|
const auto origin = std::chrono::steady_clock::now();
|
|
EXPECT_FALSE(source.tickForecastClock(origin));
|
|
EXPECT_TRUE(source.tickForecastClock(origin + std::chrono::milliseconds(100)));
|
|
EXPECT_FALSE(source.tickForecastClock(origin + std::chrono::milliseconds(100)));
|
|
const auto color = source.resolveColorProjection();
|
|
const auto particle = source.resolveParticleProjection();
|
|
ASSERT_TRUE(color.accepted());
|
|
ASSERT_TRUE(particle.accepted());
|
|
EXPECT_EQ(color.projection->sampleState.requestedForecastTimeMs, Base + 1100);
|
|
EXPECT_EQ(particle.projection->sampleState.requestedForecastTimeMs, Base + 1100);
|
|
source.setForecastPlaybackState(particle::ForecastPlaybackState::Paused);
|
|
EXPECT_FALSE(source.tickForecastClock(origin + std::chrono::seconds(3)));
|
|
EXPECT_EQ(source.resolveParticleProjection().projection->sampleState.requestedForecastTimeMs, Base + 1100);
|
|
ASSERT_TRUE(source.setForecastPlaybackSpeed(2.0f));
|
|
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
|
EXPECT_FALSE(source.tickForecastClock(origin + std::chrono::seconds(4)));
|
|
EXPECT_TRUE(source.tickForecastClock(origin + std::chrono::milliseconds(4100)));
|
|
EXPECT_EQ(source.resolveParticleProjection().projection->sampleState.requestedForecastTimeMs, Base + 1300);
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + 2000));
|
|
EXPECT_FALSE(source.tickForecastClock(origin + std::chrono::seconds(5)));
|
|
EXPECT_EQ(source.resolveParticleProjection().projection->sampleState.requestedForecastTimeMs, Base + 2000);
|
|
source.clearAllEncodedFields();
|
|
EXPECT_FALSE(source.tickForecastClock(origin + std::chrono::seconds(6)));
|
|
EXPECT_FALSE(source.resolveParticleProjection().accepted());
|
|
}
|
|
|
|
TEST(Source, RasterArrayV4ClockSourceNotificationReentryDoesNotAdvanceTwice) {
|
|
style::RasterArraySource source{"v4-reentry"};
|
|
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});
|
|
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
|
"wind", {1, 1, particle::FieldComponent::UEast},
|
|
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + 1000));
|
|
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
|
const auto origin = std::chrono::steady_clock::now();
|
|
EXPECT_FALSE(source.tickForecastClock(origin));
|
|
|
|
struct ReentryObserver final : style::SourceObserver {
|
|
style::RasterArraySource& source;
|
|
std::chrono::steady_clock::time_point nestedTime;
|
|
int notifications = 0;
|
|
bool nestedAdvanced = false;
|
|
ReentryObserver(style::RasterArraySource& src, std::chrono::steady_clock::time_point time)
|
|
: source(src), nestedTime(time) {}
|
|
void onSourceChanged(style::Source&) override {
|
|
++notifications;
|
|
nestedAdvanced = source.tickForecastClock(nestedTime) || nestedAdvanced;
|
|
}
|
|
} observer(source, origin + std::chrono::milliseconds(200));
|
|
source.setObserver(&observer);
|
|
ASSERT_TRUE(source.tickForecastClock(origin + std::chrono::milliseconds(100)));
|
|
EXPECT_GE(observer.notifications, 1);
|
|
EXPECT_FALSE(observer.nestedAdvanced);
|
|
EXPECT_EQ(source.resolveParticleProjection().projection->sampleState.requestedForecastTimeMs, Base + 1100);
|
|
ASSERT_TRUE(source.tickForecastClock(origin + std::chrono::milliseconds(200)));
|
|
EXPECT_FALSE(observer.nestedAdvanced);
|
|
EXPECT_EQ(source.resolveParticleProjection().projection->sampleState.requestedForecastTimeMs, Base + 1200);
|
|
source.setObserver(nullptr);
|
|
}
|
|
|
|
TEST(Source, RasterArrayV4MixTicksReuseImmutableMaterializationsUntilEndpointChanges) {
|
|
style::RasterArraySource source{"v4-materialization-cache"};
|
|
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});
|
|
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
|
"wind", {1, 1, particle::FieldComponent::UEast},
|
|
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + 1000));
|
|
|
|
const auto beforeTicks = particle::rendererDebugCounterSnapshot();
|
|
const auto initialColor = source.impl().v4RendererState.color;
|
|
const auto initialParticle = source.impl().v4RendererState.particle;
|
|
ASSERT_TRUE(initialColor);
|
|
ASSERT_TRUE(initialParticle);
|
|
ASSERT_TRUE(initialColor->t0);
|
|
ASSERT_TRUE(initialColor->t1);
|
|
ASSERT_TRUE(initialParticle->t0);
|
|
ASSERT_TRUE(initialParticle->t1);
|
|
|
|
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
|
const auto origin = std::chrono::steady_clock::now();
|
|
EXPECT_FALSE(source.tickForecastClock(origin));
|
|
for (uint64_t tick = 1; tick <= 50; ++tick)
|
|
EXPECT_TRUE(source.tickForecastClock(origin + std::chrono::milliseconds(tick * 16)));
|
|
|
|
const auto afterTicks = particle::rendererDebugCounterSnapshot();
|
|
const auto mixedColor = source.impl().v4RendererState.color;
|
|
const auto mixedParticle = source.impl().v4RendererState.particle;
|
|
ASSERT_TRUE(mixedColor);
|
|
ASSERT_TRUE(mixedParticle);
|
|
EXPECT_EQ(afterTicks.v4ProjectionMaterialize, beforeTicks.v4ProjectionMaterialize);
|
|
EXPECT_EQ(afterTicks.fieldTextureUpload, beforeTicks.fieldTextureUpload);
|
|
EXPECT_EQ(afterTicks.gpuResourceCreate, beforeTicks.gpuResourceCreate);
|
|
EXPECT_EQ(afterTicks.gpuResourceRelease, beforeTicks.gpuResourceRelease);
|
|
EXPECT_EQ(mixedColor->t0, initialColor->t0);
|
|
EXPECT_EQ(mixedColor->t1, initialColor->t1);
|
|
EXPECT_EQ(mixedParticle->t0, initialParticle->t0);
|
|
EXPECT_EQ(mixedParticle->t1, initialParticle->t1);
|
|
EXPECT_EQ(mixedParticle->sampleState.requestedForecastTimeMs, Base + 1800);
|
|
|
|
submit(source, "wind", "wind", Base + 2 * Hour, 3);
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour + 1000));
|
|
const auto afterEndpointChange = particle::rendererDebugCounterSnapshot();
|
|
const auto advancedParticle = source.impl().v4RendererState.particle;
|
|
ASSERT_TRUE(advancedParticle);
|
|
EXPECT_EQ(afterEndpointChange.v4ProjectionMaterialize, afterTicks.v4ProjectionMaterialize + 2);
|
|
EXPECT_NE(advancedParticle->t0, initialParticle->t0);
|
|
EXPECT_NE(advancedParticle->t1, initialParticle->t1);
|
|
|
|
const auto beforeRevision = afterEndpointChange.v4ProjectionMaterialize;
|
|
EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 4), windContract(), qbin(Base + Hour)),
|
|
style::RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision);
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour + 1000));
|
|
const auto afterRevision = particle::rendererDebugCounterSnapshot();
|
|
const auto revisedParticle = source.impl().v4RendererState.particle;
|
|
ASSERT_TRUE(revisedParticle);
|
|
EXPECT_EQ(afterRevision.v4ProjectionMaterialize, beforeRevision + 2);
|
|
EXPECT_NE(revisedParticle->t0, advancedParticle->t0);
|
|
EXPECT_NE(revisedParticle->t1, advancedParticle->t1);
|
|
|
|
source.clearAllEncodedFields();
|
|
EXPECT_FALSE(source.impl().v4RendererState.color);
|
|
EXPECT_FALSE(source.impl().v4RendererState.particle);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouchingV3) {
|
|
style::RasterArraySource source{"v4-render"};
|
|
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 + Hour, 2, true);
|
|
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
|
"wind",
|
|
{1, 1, particle::FieldComponent::UEast},
|
|
{2, 1, particle::FieldComponent::VNorth},
|
|
{},
|
|
1});
|
|
source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77});
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
|
|
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
|
ASSERT_TRUE(source.impl().v4RendererState.color);
|
|
const auto& particles = *source.impl().v4RendererState.particle;
|
|
const auto& color = *source.impl().v4RendererState.color;
|
|
EXPECT_FLOAT_EQ(particles.sampleState.mix, 0.5f);
|
|
EXPECT_FLOAT_EQ(color.sampleState.mix, 0.5f);
|
|
ASSERT_TRUE(particles.t0);
|
|
ASSERT_TRUE(color.t0);
|
|
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(color.t0->scalar, (std::vector<float>{1, 2, 3, 4}));
|
|
EXPECT_EQ(color.valueSource, particle::MaterializedColorValueSource::Scalar);
|
|
const auto particleIdentity = particles.contentIdentity;
|
|
const auto retainedParticleT0 = particles.t0;
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 4));
|
|
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
|
EXPECT_EQ(source.impl().v4RendererState.particle->contentIdentity, particleIdentity);
|
|
EXPECT_FLOAT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.25f);
|
|
ASSERT_TRUE(retainedParticleT0);
|
|
EXPECT_EQ(retainedParticleT0->vector, (std::vector<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
|
EXPECT_FALSE(source.impl().field);
|
|
EXPECT_FALSE(source.impl().nextField);
|
|
EXPECT_FALSE(source.impl().timeline);
|
|
source.clearAllEncodedFields();
|
|
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);
|
|
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) {
|
|
const auto t0 = materializerFrame("scalar",
|
|
Base,
|
|
17,
|
|
23,
|
|
{99, 0, particle::FieldComponent::None},
|
|
particle::FieldDType::Float32,
|
|
particle::FieldValueClass::Scalar,
|
|
77,
|
|
2.0f,
|
|
1.0f,
|
|
float32LE({1.5f, -2.0f, 3.25f, 4.0f}),
|
|
0x0d);
|
|
const auto t1 = materializerFrame("scalar",
|
|
Base + Hour,
|
|
18,
|
|
24,
|
|
{99, 0, particle::FieldComponent::None},
|
|
particle::FieldDType::Float32,
|
|
particle::FieldValueClass::Scalar,
|
|
77,
|
|
2.0f,
|
|
1.0f,
|
|
float32LE({2.0f, -1.5f, 3.5f, 5.0f}),
|
|
0x0f);
|
|
particle::ColorBinding binding{
|
|
"scalar", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77};
|
|
particle::ColorProjection projection;
|
|
projection.contentIdentity = {
|
|
binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"};
|
|
projection.sampleState = {Base + Hour / 2, 0.5f};
|
|
projection.t0Frame = t0;
|
|
projection.t1Frame = t1;
|
|
auto materialized = particle::materializeColorProjection(projection);
|
|
ASSERT_TRUE(materialized);
|
|
ASSERT_TRUE(materialized->t0);
|
|
ASSERT_TRUE(materialized->t1);
|
|
EXPECT_EQ(materialized->t0->scalar, (std::vector<float>{4.0f, -3.0f, 7.5f, 9.0f}));
|
|
EXPECT_EQ(materialized->t0->valid, (std::vector<uint8_t>{1, 0, 1, 1}));
|
|
EXPECT_EQ(materialized->t0->generation, 17u);
|
|
EXPECT_EQ(materialized->t0->dataVersion, 23u);
|
|
EXPECT_EQ(materialized->t0->fieldSha256, "plane-t0");
|
|
EXPECT_EQ(materialized->t1->generation, 18u);
|
|
EXPECT_EQ(materialized->t1->dataVersion, 24u);
|
|
const auto retainedT0 = materialized->t0;
|
|
materialized.reset();
|
|
ASSERT_TRUE(retainedT0);
|
|
EXPECT_EQ(retainedT0->scalar, (std::vector<float>{4.0f, -3.0f, 7.5f, 9.0f}));
|
|
}
|
|
|
|
TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) {
|
|
const auto t0 = materializerFrame("direction",
|
|
Base,
|
|
31,
|
|
41,
|
|
{10, 1, particle::FieldComponent::None},
|
|
particle::FieldDType::UInt16,
|
|
particle::FieldValueClass::DirectionAngle,
|
|
particle::FieldUnitDegrees,
|
|
0.01f,
|
|
0.0f,
|
|
uint16LE({0, 9000, 18000, 27000}));
|
|
const auto t1 = materializerFrame("direction",
|
|
Base + Hour,
|
|
32,
|
|
42,
|
|
{10, 1, particle::FieldComponent::None},
|
|
particle::FieldDType::UInt16,
|
|
particle::FieldValueClass::DirectionAngle,
|
|
particle::FieldUnitDegrees,
|
|
0.01f,
|
|
0.0f,
|
|
uint16LE({0, 9000, 18000, 27000}));
|
|
particle::ParticleBinding binding{particle::ParticleBindingMode::DirectionAngle,
|
|
"direction",
|
|
{},
|
|
{},
|
|
{10, 1, particle::FieldComponent::None},
|
|
particle::FieldUnitDegrees};
|
|
particle::ParticleProjection projection;
|
|
projection.contentIdentity = {binding,
|
|
{"direction", Base},
|
|
{"direction", Base + Hour},
|
|
"dir-t0",
|
|
"dir-t1",
|
|
{},
|
|
{},
|
|
"validity-t0",
|
|
"validity-t1"};
|
|
projection.sampleState = {Base + Hour / 2, 0.5f};
|
|
projection.t0Frame = t0;
|
|
projection.t1Frame = t1;
|
|
const auto materialized = particle::materializeParticleProjection(projection);
|
|
ASSERT_TRUE(materialized);
|
|
ASSERT_TRUE(materialized->t0);
|
|
ASSERT_EQ(materialized->t0->vector.size(), 8u);
|
|
EXPECT_NEAR(materialized->t0->vector[0], 0.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[1], 1.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[2], 1.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[3], 0.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[4], 0.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[5], -1.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[6], -1.0f, 1e-5f);
|
|
EXPECT_NEAR(materialized->t0->vector[7], 0.0f, 1e-5f);
|
|
EXPECT_EQ(materialized->t0->vectorSemantics, particle::VectorSemantics::Direction);
|
|
}
|
|
|
|
|
|
// The RN weather source uses Host V3 ingress and V4 QBIN on one Style source.
|
|
// A terminal detach must clear both, drop old bindings, and permit a new owner.
|
|
TEST(Source, RasterArrayV4TerminalResetClearsBothIngressesAndAllowsNewGeneration) {
|
|
style::RasterArraySource source{"v4-detach-reset"};
|
|
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);
|
|
|
|
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});
|
|
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
|
"wind", {1, 1, particle::FieldComponent::UEast},
|
|
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
|
|
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
|
|
ASSERT_TRUE(source.impl().v4RendererState.color);
|
|
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
|
ASSERT_EQ(source.impl().v4RendererState.color->sampleState.mix, 0.5f);
|
|
EXPECT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.5f);
|
|
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
|
ASSERT_EQ(source.impl().hostFieldSubmission, true);
|
|
|
|
ASSERT_TRUE(source.resetWeatherRuntimeState());
|
|
EXPECT_FALSE(source.impl().field);
|
|
EXPECT_FALSE(source.impl().nextField);
|
|
EXPECT_FALSE(source.impl().timeline);
|
|
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
|
|
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
|
|
EXPECT_FALSE(source.impl().v4RendererState.colorInput);
|
|
EXPECT_FALSE(source.impl().v4RendererState.particleInput);
|
|
EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::InvalidBinding);
|
|
EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::InvalidBinding);
|
|
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
|
|
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
|
|
EXPECT_FALSE(source.tickForecastClock(std::chrono::steady_clock::now()));
|
|
EXPECT_TRUE(source.resetWeatherRuntimeState()); // Idempotent.
|
|
|
|
submit(source, "wind", "wind", Base, 3);
|
|
submit(source, "wind", "wind", Base + Hour, 4);
|
|
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
|
|
particle::FieldValueClass::VelocityComponent, 1});
|
|
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.resolveColorProjection().accepted());
|
|
EXPECT_TRUE(source.resolveParticleProjection().accepted());
|
|
EXPECT_TRUE(source.impl().v4RendererState.color);
|
|
EXPECT_TRUE(source.impl().v4RendererState.particle);
|
|
}
|