183 lines
7.2 KiB
C++
183 lines
7.2 KiB
C++
#pragma once
|
|
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace mbgl::particle {
|
|
|
|
enum class VectorSemantics : uint8_t { Velocity, Direction };
|
|
|
|
constexpr float DirectionAdvectionEpsilon = 0.00001f;
|
|
|
|
// Mirrors the shader's canonical component-wise T0/T1 resolution. Keeping
|
|
// this small, provider-neutral helper in Core gives fixture tests an exact
|
|
// CPU oracle; the renderer still performs the authoritative GPU operation.
|
|
inline bool resolveParticleAdvection(const VectorSemantics semantics,
|
|
const std::array<float, 2>& t0,
|
|
const std::array<float, 2>& t1,
|
|
const float timeMix,
|
|
const float advectionScale,
|
|
std::array<float, 2>& result) {
|
|
const std::array<float, 2> mixed{{t0[0] + (t1[0] - t0[0]) * timeMix,
|
|
t0[1] + (t1[1] - t0[1]) * timeMix}};
|
|
if (!std::isfinite(mixed[0]) || !std::isfinite(mixed[1])) return false;
|
|
const float length = std::hypot(mixed[0], mixed[1]);
|
|
if (!std::isfinite(length) || length <= DirectionAdvectionEpsilon) return false;
|
|
if (semantics == VectorSemantics::Direction) {
|
|
if (!std::isfinite(advectionScale) || advectionScale <= 0.0f) return false;
|
|
result = {{mixed[0] / length * advectionScale, mixed[1] / length * advectionScale}};
|
|
} else {
|
|
// VELOCITY intentionally does not consume advectionScale.
|
|
result = mixed;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// A v2 payload is deliberately split into unambiguous planes. vector is
|
|
// interleaved eastward-U/northward-V; scalar is one float per cell; validity
|
|
// is the one shared logical mask for every present channel.
|
|
struct ParticleFieldPayloadBundle {
|
|
std::optional<std::vector<char>> scalar;
|
|
std::optional<std::vector<char>> vector;
|
|
std::optional<std::vector<char>> validity;
|
|
};
|
|
|
|
struct ParticleFieldPlaneDescriptor {
|
|
uint64_t byteLength = 0;
|
|
std::string encoding = "float32-le";
|
|
std::string layout;
|
|
std::optional<float> noData;
|
|
};
|
|
|
|
// Provider-neutral description of the canonical field bytes consumed by Core.
|
|
// v1 fields below are retained only for the thin vector-only compatibility
|
|
// ingress. v2 uses scalar/vector/validity plane descriptors instead.
|
|
struct ParticleFieldDescriptor {
|
|
std::string fieldID;
|
|
std::string fieldType;
|
|
std::string displayDomain;
|
|
// Assigned by the producer before decoding. The adapter never invents a revision.
|
|
uint64_t generation = 0;
|
|
uint64_t validTime = 0;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
double west = -180.0;
|
|
double south = -85.0;
|
|
double east = 180.0;
|
|
double north = 85.0;
|
|
float noData = -9999.0f;
|
|
uint64_t dataVersion = 0;
|
|
uint64_t byteLength = 0;
|
|
uint32_t stride = 0;
|
|
std::string layout = "interleaved-uv";
|
|
std::string encoding = "float32-le";
|
|
std::string fieldSha256;
|
|
std::optional<ParticleFieldPlaneDescriptor> scalar;
|
|
std::optional<ParticleFieldPlaneDescriptor> vector;
|
|
std::optional<ParticleFieldPlaneDescriptor> validity;
|
|
std::optional<VectorSemantics> vectorSemantics;
|
|
};
|
|
|
|
struct ParticleFieldSnapshot {
|
|
std::string fieldID;
|
|
std::string fieldType;
|
|
std::string displayDomain;
|
|
uint64_t validTime = 0;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
double west = -180.0;
|
|
double south = -85.0;
|
|
double east = 180.0;
|
|
double north = 85.0;
|
|
float noData = -9999.0f;
|
|
uint64_t dataVersion = 0;
|
|
uint64_t generation = 0;
|
|
std::string fieldSha256;
|
|
std::vector<float> scalar;
|
|
std::vector<float> vector;
|
|
std::vector<uint8_t> valid;
|
|
std::optional<VectorSemantics> vectorSemantics;
|
|
// Internal interpretation identity. Never part of the public field ABI.
|
|
std::string gpuEncodingIdentity;
|
|
|
|
bool isValid() const;
|
|
bool isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot&) const;
|
|
bool hasScalar() const { return !scalar.empty(); }
|
|
bool hasVector() const { return !vector.empty(); }
|
|
};
|
|
|
|
inline bool fieldIdentityChanged(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) {
|
|
return current.generation != next.generation || current.fieldID != next.fieldID ||
|
|
current.dataVersion != next.dataVersion;
|
|
}
|
|
|
|
inline bool sameFieldGpuContent(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) {
|
|
// Content identity is the decoded plane bundle, not producer identity,
|
|
// generation, dataVersion, or an optional legacy aggregate hash.
|
|
return current.isTimeInterpolationCompatibleWith(next) && current.scalar == next.scalar &&
|
|
current.vector == next.vector && current.valid == next.valid;
|
|
}
|
|
|
|
// A GPU slot may be reused only for the same immutable endpoint. A matching
|
|
// producer revision alone is insufficient, and equal bytes at another valid
|
|
// time must not silently inherit the previous endpoint's provenance.
|
|
inline bool sameFieldGpuEndpoint(const ParticleFieldSnapshot& a, const ParticleFieldSnapshot& b) {
|
|
return a.fieldID == b.fieldID && a.generation == b.generation && a.dataVersion == b.dataVersion &&
|
|
a.validTime == b.validTime && a.fieldType == b.fieldType && a.displayDomain == b.displayDomain &&
|
|
a.vectorSemantics == b.vectorSemantics && a.noData == b.noData &&
|
|
a.fieldSha256 == b.fieldSha256 && a.gpuEncodingIdentity == b.gpuEncodingIdentity &&
|
|
a.width == b.width && a.height == b.height && a.west == b.west && a.south == b.south &&
|
|
a.east == b.east && a.north == b.north && a.vector == b.vector && a.valid == b.valid;
|
|
}
|
|
|
|
enum class GpuEndpointSource : uint8_t { Upload, OldT0, OldT1 };
|
|
|
|
inline GpuEndpointSource gpuEndpointSource(const ParticleFieldSnapshot& oldT0,
|
|
const ParticleFieldSnapshot& oldT1,
|
|
const ParticleFieldSnapshot& wanted) {
|
|
if (sameFieldGpuEndpoint(oldT0, wanted)) return GpuEndpointSource::OldT0;
|
|
if (sameFieldGpuEndpoint(oldT1, wanted)) return GpuEndpointSource::OldT1;
|
|
return GpuEndpointSource::Upload;
|
|
}
|
|
|
|
struct ParticleFieldTimeWindow {
|
|
ParticleFieldSnapshot t0;
|
|
ParticleFieldSnapshot t1;
|
|
|
|
bool isValid() const { return t0.isValid() && t1.isValid() && t0.isTimeInterpolationCompatibleWith(t1); }
|
|
};
|
|
|
|
enum class ParticleFieldAdapterError : uint8_t {
|
|
None,
|
|
InvalidDescriptor,
|
|
UnsupportedFormat,
|
|
DimensionMismatch,
|
|
PayloadSizeMismatch,
|
|
InvalidTimestampOrIdentity,
|
|
NonFiniteData,
|
|
DecodeValidationFailure,
|
|
};
|
|
|
|
const char* particleFieldAdapterErrorName(ParticleFieldAdapterError);
|
|
|
|
struct ParticleFieldAdapterResult {
|
|
std::optional<ParticleFieldSnapshot> snapshot;
|
|
ParticleFieldAdapterError error = ParticleFieldAdapterError::None;
|
|
std::string message;
|
|
|
|
explicit operator bool() const { return snapshot.has_value(); }
|
|
};
|
|
|
|
class ParticleFieldDataAdapter final {
|
|
public:
|
|
static ParticleFieldAdapterResult decode(const ParticleFieldDescriptor&, const ParticleFieldPayloadBundle&);
|
|
// Legacy v1 vector-only normalization entry point.
|
|
static ParticleFieldAdapterResult decode(const ParticleFieldDescriptor&, const std::vector<char>&, uint64_t generation);
|
|
};
|
|
|
|
} // namespace mbgl::particle
|