Files
MapLibreParticleLab/include/mbgl/particle/field/field_data_adapter.hpp
tei f49ffa68a1
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
fix(step097): complete A2 clock and GPU field correctness
2026-09-26 15:17:43 +09:00

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