merge(upgrade): port particle extensions onto official native 13.6.1 (unverified)

This commit is contained in:
tei
2026-10-09 09:37:07 +09:00
767 changed files with 85435 additions and 153 deletions

View File

@@ -82,6 +82,10 @@ public:
virtual void reduceMemoryUsage() = 0;
virtual std::unique_ptr<OffscreenTexture> createOffscreenTexture(Size, TextureChannelDataType) = 0;
virtual std::unique_ptr<OffscreenTexture> createOffscreenTexture(Size,
TextureChannelDataType,
bool,
bool) = 0;
template <RenderbufferPixelType pixelType>
Renderbuffer<pixelType> createRenderbuffer(const Size size) {

View File

@@ -0,0 +1,16 @@
#pragma once
#include <mln/layermanager/layer_factory.hpp>
namespace mln {
// VALIDATION ONLY: factory for the 30-01 Core RenderLayer experiment.
class ParticleValidationLayerFactory final : public LayerFactory {
public:
const style::LayerTypeInfo* getTypeInfo() const noexcept override;
std::unique_ptr<style::Layer> createLayer(const std::string&,
const style::conversion::Convertible&) noexcept override;
std::unique_ptr<RenderLayer> createRenderLayer(Immutable<style::Layer::Impl>) noexcept override;
};
} // namespace mln

View File

@@ -0,0 +1,15 @@
#pragma once
#include <mln/layermanager/layer_factory.hpp>
namespace mln {
class RasterParticleLayerFactory final : public LayerFactory {
public:
const style::LayerTypeInfo* getTypeInfo() const noexcept override;
std::unique_ptr<style::Layer> createLayer(const std::string&,
const style::conversion::Convertible&) noexcept override;
std::unique_ptr<RenderLayer> createRenderLayer(Immutable<style::Layer::Impl>) noexcept override;
};
} // namespace mln

View File

@@ -0,0 +1,10 @@
#pragma once
#include <mln/layermanager/layer_factory.hpp>
namespace mln {
class WeatherColorLayerFactory final : public LayerFactory {
public:
const style::LayerTypeInfo* getTypeInfo() const noexcept override;
std::unique_ptr<style::Layer> createLayer(const std::string&, const style::conversion::Convertible&) noexcept override;
std::unique_ptr<RenderLayer> createRenderLayer(Immutable<style::Layer::Impl>) noexcept override;
};
} // namespace mln

View File

@@ -14,6 +14,8 @@
#include <mln/storage/resource_options.hpp>
#include <mln/util/client_options.hpp>
#include <mln/util/action_journal_options.hpp>
#include <mln/renderer/retained_underlay.hpp>
#include <mln/renderer/render_segment.hpp>
#include <cstdint>
#include <string>
@@ -55,6 +57,14 @@ public:
/// Triggers a repaint.
void triggerRepaint();
/** Enables or disables the map-scoped retained static underlay optimization. */
void setRetainedUnderlayEnabled(bool);
bool setContinuousLayerRange(std::string startLayerId, std::string endLayerId);
bool setRenderSegmentPlan(RenderSegmentPlan);
RenderSegmentDiagnostics getRenderSegmentDiagnostics() const;
bool isRetainedUnderlayEnabled() const;
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() const;
style::Style& getStyle();
const style::Style& getStyle() const;

View File

@@ -106,7 +106,10 @@ public:
void setDirtyState() override;
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool);
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size,
gfx::TextureChannelDataType,
bool,
bool) override;
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType) override;

View File

@@ -17,6 +17,19 @@ class BufferResource;
class CommandEncoder;
class Context;
namespace detail {
struct ClearedAttachments {
bool color = false;
bool depth = false;
bool stencil = false;
};
ClearedAttachments applyRenderPassClears(MTL::RenderPassDescriptor&,
const std::optional<Color>&,
const std::optional<float>&,
const std::optional<int32_t>&);
} // namespace detail
class RenderPass final : public gfx::RenderPass {
public:
RenderPass(CommandEncoder&, const char* name, const gfx::RenderPassDescriptor&);
@@ -36,10 +49,16 @@ public:
/// Apply the given stencil reference value, if different from the current value
void setStencilReference(int32_t referenceValue);
/// Bind a texture to the fragment location
/// Bind a texture to the vertex location.
void setVertexTexture(const MTLTexturePtr&, int32_t location);
/// Set the sampler for a vertex texture binding.
void setVertexSamplerState(const MTLSamplerStatePtr&, int32_t location);
/// Bind a texture to the fragment location.
void setFragmentTexture(const MTLTexturePtr&, int32_t location);
/// Set the sampler for a texture binding
/// Set the sampler for a fragment texture binding.
void setFragmentSamplerState(const MTLSamplerStatePtr&, int32_t location);
/// Set the render pipeline state
@@ -90,6 +109,8 @@ private:
std::array<std::optional<BindInfo>, maxBinds> vertexBinds;
std::array<std::optional<BindInfo>, maxBinds> fragmentBinds;
std::array<MTLTexturePtr, maxBinds> vertexTextureBindings;
std::array<MTLSamplerStatePtr, maxBinds> vertexSamplerStates;
std::array<MTLTexturePtr, maxBinds> fragmentTextureBindings;
std::array<MTLSamplerStatePtr, maxBinds> fragmentSamplerStates;

View File

@@ -0,0 +1,132 @@
#pragma once
#include <mln/particle/field/field_runtime.hpp>
#include <memory>
#include <optional>
#include <string>
namespace mln::particle {
// Host-owned binding intent. FieldPlaneKey identity is deliberately only its
// semanticID/groupID pair; component is validated from the selected plane.
struct ColorBinding {
std::string fieldKey;
FieldPlaneKey plane;
FieldValueClass expectedValueClass = FieldValueClass::Scalar;
uint16_t expectedUnitID = FieldUnitNone;
bool operator==(const ColorBinding&) const;
};
enum class ParticleBindingMode : uint8_t { VectorComponents, DirectionAngle };
struct ParticleBinding {
ParticleBindingMode mode = ParticleBindingMode::VectorComponents;
std::string fieldKey;
FieldPlaneKey uPlane;
FieldPlaneKey vPlane;
FieldPlaneKey directionPlane;
uint16_t expectedUnitID = FieldUnitNone;
// Equality includes only the selectors consumed by mode; selector identity
// remains semanticID/groupID, while component is a resolved-plane assertion.
bool operator==(const ParticleBinding&) const;
};
struct FieldSampleState {
uint64_t requestedForecastTimeMs = 0;
float mix = 0.0f;
};
// The stable coordinate of a FieldSeries interpolation endpoint. Revisions
// are deliberately excluded: projection identities add only the content that
// their binding actually consumes.
struct ProjectionFrameCoordinate {
std::string fieldID;
uint64_t validTimeMs = 0;
bool operator==(const ProjectionFrameCoordinate&) const;
};
struct ColorProjectionContentIdentity {
ColorBinding binding;
ProjectionFrameCoordinate t0Coordinate;
ProjectionFrameCoordinate t1Coordinate;
std::string t0Plane;
std::string t1Plane;
std::string t0Validity;
std::string t1Validity;
bool operator==(const ColorProjectionContentIdentity&) const;
};
struct ParticleProjectionContentIdentity {
ParticleBinding binding;
ProjectionFrameCoordinate t0Coordinate;
ProjectionFrameCoordinate t1Coordinate;
std::string t0FirstPlane;
std::string t1FirstPlane;
std::string t0SecondPlane;
std::string t1SecondPlane;
std::string t0Validity;
std::string t1Validity;
bool operator==(const ParticleProjectionContentIdentity&) const;
};
struct ColorProjection {
ColorProjectionContentIdentity contentIdentity;
FieldSampleState sampleState;
std::shared_ptr<const DecodedFieldFrame> t0Frame;
std::shared_ptr<const DecodedFieldFrame> t1Frame;
std::shared_ptr<const std::vector<uint8_t>> t0PlaneStorage;
std::shared_ptr<const std::vector<uint8_t>> t1PlaneStorage;
std::shared_ptr<const std::vector<uint8_t>> t0ValidityStorage;
std::shared_ptr<const std::vector<uint8_t>> t1ValidityStorage;
};
struct ParticleProjection {
ParticleProjectionContentIdentity contentIdentity;
FieldSampleState sampleState;
std::shared_ptr<const DecodedFieldFrame> t0Frame;
std::shared_ptr<const DecodedFieldFrame> t1Frame;
std::shared_ptr<const std::vector<uint8_t>> t0FirstPlaneStorage;
std::shared_ptr<const std::vector<uint8_t>> t1FirstPlaneStorage;
std::shared_ptr<const std::vector<uint8_t>> t0SecondPlaneStorage;
std::shared_ptr<const std::vector<uint8_t>> t1SecondPlaneStorage;
std::shared_ptr<const std::vector<uint8_t>> t0ValidityStorage;
std::shared_ptr<const std::vector<uint8_t>> t1ValidityStorage;
};
enum class BindingProjectionError : uint8_t {
Accepted,
InvalidBinding,
FieldSeriesMissing,
WindowUnavailable,
PlaneMissing,
PlaneClassMismatch,
PlaneUnitMismatch,
PlaneComponentMismatch,
PlaneGroupMismatch,
};
struct ColorProjectionResult {
BindingProjectionError error = BindingProjectionError::InvalidBinding;
std::optional<ColorProjection> projection;
bool accepted() const { return error == BindingProjectionError::Accepted; }
};
struct ParticleProjectionResult {
BindingProjectionError error = BindingProjectionError::InvalidBinding;
std::optional<ParticleProjection> projection;
bool accepted() const { return error == BindingProjectionError::Accepted; }
};
class BindingProjectionResolver final {
public:
static ColorProjectionResult resolve(const EncodedFieldStore&, const ColorBinding&, uint64_t requestedForecastTimeMs);
static ParticleProjectionResult resolve(const EncodedFieldStore&, const ParticleBinding&, uint64_t requestedForecastTimeMs);
};
} // namespace mln::particle

View File

@@ -0,0 +1,182 @@
#pragma once
#include <array>
#include <cmath>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace mln::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 mln::particle

View File

@@ -0,0 +1,56 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace mln::particle {
enum class FieldValueClass : uint8_t {
Scalar = 0,
VelocityComponent = 1,
DirectionAngle = 2,
Validity = 3,
};
enum class FieldComponent : uint8_t {
None = 0,
UEast = 1,
VNorth = 2,
WUp = 3,
};
enum class FieldTimeRelation : uint8_t {
Any = 0,
ValidGteReference = 1,
ValidEqReference = 2,
};
// Core unit identifiers reserved by Contract Schema v1. Scalar unit identifiers
// deliberately remain opaque to the Native runtime.
constexpr uint16_t FieldUnitNone = 0;
constexpr uint16_t FieldUnitMetersPerSecond = 1;
constexpr uint16_t FieldUnitDegrees = 4;
struct FieldPlaneRequirement {
uint16_t semanticID = 0;
FieldValueClass valueClass = FieldValueClass::Scalar;
uint16_t unitID = 0;
uint16_t groupID = 0;
FieldComponent component = FieldComponent::None;
bool requiredFlag = true;
};
struct ResolvedFieldProfileContract {
uint16_t contractSchemaVersion = 1;
std::string profileID;
uint32_t profileVersion = 0;
bool allowAdditionalPlanes = true;
bool sharedValidityRequired = true;
FieldTimeRelation timeRelation = FieldTimeRelation::Any;
std::vector<FieldPlaneRequirement> requiredPlanes;
bool isStructurallyValid() const;
};
} // namespace mln::particle

View File

@@ -0,0 +1,14 @@
#pragma once
#include <mln/particle/field/field_profile_contract.hpp>
#include <optional>
#include <string>
namespace mln::particle {
// Parses only the frozen V4 control-plane JSON schema. QBIN decoding and
// profile/frame business validation intentionally remain in their Core owners.
std::optional<ResolvedFieldProfileContract> parseResolvedFieldProfileContractJson(const std::string& json);
} // namespace mln::particle

View File

@@ -0,0 +1,41 @@
#pragma once
#include <mln/particle/field/field_runtime.hpp>
#include <string>
namespace mln::particle {
// Structured outcome of Contract Schema v1 validation. This API deliberately
// validates only generic field metadata; it has no provider or product logic.
enum class FieldProfileValidationCode : uint8_t {
Accepted,
UnsupportedContractSchema,
InvalidContract,
InvalidFrame,
MissingRequiredPlane,
PlaneDescriptorMismatch,
UndeclaredRequiredPlane,
AdditionalPlaneNotAllowed,
InvalidVelocityGroup,
// A reserved Native Core semanticID was declared with a descriptor that
// conflicts with its protocol-defined physical meaning.
SemanticContractMismatch,
InvalidCoreUnit,
UnsupportedSharedValidityMode,
TimeRelationMismatch,
};
struct FieldProfileValidationResult {
FieldProfileValidationCode code = FieldProfileValidationCode::InvalidContract;
std::string detail;
bool accepted() const { return code == FieldProfileValidationCode::Accepted; }
};
class FieldProfileValidator final {
public:
static FieldProfileValidationResult validate(const ResolvedFieldProfileContract&, const DecodedFieldFrame&);
};
} // namespace mln::particle

View File

@@ -0,0 +1,37 @@
#pragma once
#include <mln/particle/field/field_binding.hpp>
#include <mln/particle/field/field_data_adapter.hpp>
#include <optional>
#include <memory>
#include <variant>
namespace mln::particle {
// Renderer-neutral canonical snapshots. QBIN stays confined to ingress and
// field Core; renderers receive only the existing canonical field shape.
enum class MaterializedColorValueSource : uint8_t {
Scalar,
VectorMagnitude,
};
struct MaterializedColorProjection {
std::shared_ptr<const ParticleFieldSnapshot> t0;
std::shared_ptr<const ParticleFieldSnapshot> t1;
ColorProjectionContentIdentity contentIdentity;
FieldSampleState sampleState;
MaterializedColorValueSource valueSource = MaterializedColorValueSource::Scalar;
};
struct MaterializedParticleProjection {
std::shared_ptr<const ParticleFieldSnapshot> t0;
std::shared_ptr<const ParticleFieldSnapshot> t1;
ParticleProjectionContentIdentity contentIdentity;
FieldSampleState sampleState;
};
std::optional<MaterializedColorProjection> materializeColorProjection(const ColorProjection&);
std::optional<MaterializedParticleProjection> materializeParticleProjection(const ParticleProjection&);
} // namespace mln::particle

View File

@@ -0,0 +1,177 @@
#pragma once
#include <mln/particle/field/field_profile_contract.hpp>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace mln::particle {
enum class FieldDType : uint8_t {
Int16 = 1,
UInt16 = 2,
UInt8 = 3,
Int8 = 4,
Float32 = 5,
Bitset = 6,
};
struct FrameContentIdentity {
std::string fieldID;
uint64_t generation = 0;
uint64_t dataVersion = 0;
uint64_t validTimeMs = 0;
bool isValid() const;
bool operator==(const FrameContentIdentity&) const;
bool operator!=(const FrameContentIdentity& other) const { return !(*this == other); }
};
struct FieldPlaneKey {
uint16_t semanticID = 0;
uint16_t groupID = 0;
// Component is a plane descriptor property, not part of the QBIN V2
// plane identity. A Field may contain only one (semanticID, groupID).
FieldComponent component = FieldComponent::None;
bool operator==(const FieldPlaneKey&) const;
bool operator<(const FieldPlaneKey&) const;
};
struct DecodedFieldPlane {
FieldPlaneKey key;
FieldDType dtype = FieldDType::Float32;
FieldValueClass valueClass = FieldValueClass::Scalar;
uint16_t unitID = 0;
// REQUIRED is transport metadata retained by the provider-neutral runtime.
bool requiredFlag = false;
uint32_t count = 0;
float scale = 1.0f;
float offset = 0.0f;
std::string contentIdentity;
std::shared_ptr<const std::vector<uint8_t>> storage;
bool isStructurallyValid(uint64_t cellCount) const;
bool isInterpolationCompatibleWith(const DecodedFieldPlane&) const;
};
struct DecodedFieldGrid {
uint8_t layout = 1;
uint8_t projection = 0;
uint8_t sampleOrder = 0;
uint8_t gridRegistration = 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;
bool isValid() const;
bool operator==(const DecodedFieldGrid&) const;
};
struct DecodedFieldFrame {
FrameContentIdentity frameContentIdentity;
uint64_t referenceTimeMs = 0;
// Validation provenance retained so a stored frame cannot be interpolated
// across two otherwise byte-compatible but semantically different profiles.
std::string resolvedProfileID;
uint32_t resolvedProfileVersion = 0;
DecodedFieldGrid grid;
// One frame-wide validity domain. The descriptor and payload share storage;
// projections can retain the same payload without copying it.
DecodedFieldPlane validityPlane;
std::string validityContentIdentity;
std::shared_ptr<const std::vector<uint8_t>> validityBits;
std::vector<DecodedFieldPlane> planes;
bool isStructurallyValid() const;
bool isSeriesCompatibleWith(const DecodedFieldFrame&) const;
bool isInterpolationCompatibleWith(const DecodedFieldFrame&) const;
bool hasSameContentAs(const DecodedFieldFrame&) const;
// Raw decoded plane payload plus the shared validity bitset; excludes
// separately materialized CPU snapshots and renderer/GPU resources.
uint64_t decodedByteSize() const;
};
enum class FieldSeriesSubmitResult : uint8_t {
Inserted,
ReplacedRevision,
Duplicate,
RejectedInvalidFieldKey,
RejectedInvalidFrame,
RejectedIdentityConflict,
RejectedStaleRevision,
RejectedIncompatibleFrame,
RejectedCapacity,
};
struct FieldSeriesWindow {
std::shared_ptr<const DecodedFieldFrame> t0;
std::shared_ptr<const DecodedFieldFrame> t1;
float mix = 0.0f;
uint64_t requestedForecastTimeMs = 0;
};
class FieldSeries final {
public:
explicit FieldSeries(size_t capacity = 8);
FieldSeriesSubmitResult submit(std::shared_ptr<const DecodedFieldFrame>);
std::optional<FieldSeriesWindow> resolve(uint64_t requestedForecastTimeMs) const;
size_t size() const { return frames.size(); }
size_t capacity() const { return capacity_; }
uint64_t decodedByteSize() const;
const std::vector<std::shared_ptr<const DecodedFieldFrame>>& entries() const { return frames; }
private:
size_t capacity_;
std::vector<std::shared_ptr<const DecodedFieldFrame>> frames;
};
enum class ForecastPlaybackState : uint8_t { Paused, Playing };
class ForecastClock final {
public:
bool setRequestedForecastTime(uint64_t timestampMs);
bool setPlaybackSpeed(float value);
void play() { state_ = ForecastPlaybackState::Playing; }
void pause() { state_ = ForecastPlaybackState::Paused; }
bool advance(uint64_t elapsedMs);
uint64_t requestedForecastTime() const { return requestedForecastTimeMs_; }
float playbackSpeed() const { return playbackSpeed_; }
ForecastPlaybackState playbackState() const { return state_; }
private:
uint64_t requestedForecastTimeMs_ = 0;
// The supported playback rates are exact multiples of half a millisecond.
// Keep the sub-millisecond half-unit so short HALF ticks do not round up.
uint8_t fractionalHalfMillisecond_ = 0;
float playbackSpeed_ = 1.0f;
ForecastPlaybackState state_ = ForecastPlaybackState::Paused;
};
class EncodedFieldStore final {
public:
explicit EncodedFieldStore(size_t perSeriesCapacity = 8) : perSeriesCapacity_(perSeriesCapacity) {}
FieldSeriesSubmitResult submit(const std::string& fieldKey, std::shared_ptr<const DecodedFieldFrame>);
std::optional<FieldSeriesWindow> resolve(const std::string& fieldKey, uint64_t requestedForecastTimeMs) const;
const FieldSeries* find(const std::string& fieldKey) const;
bool clear(const std::string& fieldKey);
void clearAll() { series.clear(); }
size_t fieldCount() const { return series.size(); }
uint64_t decodedByteSize() const;
private:
size_t perSeriesCapacity_;
std::unordered_map<std::string, FieldSeries> series;
};
} // namespace mln::particle

View File

@@ -0,0 +1,18 @@
#pragma once
#include <mln/particle/field/field_data_adapter.hpp>
#include <string>
#include <vector>
namespace mln::particle {
// Decodes the small provider-neutral local feed contract. File/asset loading
// remains outside this class; callers provide metadata text and payload bytes.
class LocalParticleFieldFeed final {
public:
static ParticleFieldAdapterResult load(const std::string& metadataText, const std::vector<char>& payload);
static ParticleFieldAdapterResult load(const std::string& metadataText, const ParticleFieldPayloadBundle& payloads);
};
} // namespace mln::particle

View File

@@ -0,0 +1,33 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
namespace mln::particle {
// Internal RGBA8 UNORM field representation. Nearest sampling and highp
// shader arithmetic reconstruct the byte with floor(sample*255 + 0.5).
inline uint8_t encodeSignedVelocity8(const float value, const float scale, const bool valid) {
if (!valid || !std::isfinite(value) || !std::isfinite(scale) || scale <= 0.0f) return 128;
return static_cast<uint8_t>(128 + static_cast<int>(
std::lround(std::clamp(value / scale, -1.0f, 1.0f) * 127.0f)));
}
inline float decodeSignedVelocity8(const uint8_t byte, const float scale) {
return static_cast<float>(static_cast<int>(byte) - 128) * scale / 127.0f;
}
inline std::array<double, 2> mercatorVelocityDelta(const double eastMps,
const double northMps,
const double latitudeDegrees,
const double visualSeconds) {
constexpr double circumference = 40075016.68557849;
constexpr double radiansPerDegree = 3.14159265358979323846 / 180.0;
const double cosine = std::max(std::cos(latitudeDegrees * radiansPerDegree), 0.01);
return {{eastMps * visualSeconds / (circumference * cosine),
-northMps * visualSeconds / (circumference * cosine)}};
}
} // namespace mln::particle

View File

@@ -0,0 +1,14 @@
#pragma once
#include <cstddef>
namespace mln::particle::ingress {
// Transport-only bound for JNI byte-array copying; decoder limits remain independent.
constexpr std::size_t MaxEncodedFieldTransportBytes = 64u * 1024u * 1024u;
constexpr bool isValidEncodedFieldTransportSize(const std::size_t size) {
return size >= 1u && size <= MaxEncodedFieldTransportBytes;
}
} // namespace mln::particle::ingress

View File

@@ -0,0 +1,79 @@
#pragma once
#include <mln/particle/field/field_profile_validator.hpp>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
namespace mln::particle::ingress {
struct EncodedFieldRuntimeMetadata {
std::string fieldID;
uint64_t generation = 0;
uint64_t dataVersion = 0;
};
enum class QbinDecodeErrorCode : uint8_t {
Accepted,
InvalidRuntimeMetadata,
InputTooSmall,
BadMagic,
UnsupportedVersion,
UnsupportedLayout,
UnsupportedProjection,
UnsupportedSampleOrder,
UnsupportedGridRegistration,
InvalidHeader,
InvalidBounds,
InvalidTime,
LimitExceeded,
LengthMismatch,
ReservedNonZero,
InvalidDescriptor,
NonCanonicalDescriptorOrder,
DuplicatePlaneKey,
NonCanonicalPacking,
NonZeroPadding,
CrcMismatch,
InvalidValidity,
InvalidCellPayload,
InvalidFrame,
MalformedRice,
ResidualOutOfRange,
};
using QbinV2DecodeErrorCode = QbinDecodeErrorCode;
struct QbinV2DecodeResult {
QbinDecodeErrorCode code = QbinDecodeErrorCode::InvalidHeader;
std::string detail;
std::shared_ptr<const DecodedFieldFrame> frame;
bool accepted() const { return code == QbinDecodeErrorCode::Accepted && static_cast<bool>(frame); }
};
class QbinV2Decoder final {
public:
static QbinV2DecodeResult decode(const EncodedFieldRuntimeMetadata&, std::span<const uint8_t>);
};
enum class EncodedFieldIngressCode : uint8_t { Accepted, DecodeRejected, ContractMissing, ContractRejected };
struct EncodedFieldIngressResult {
EncodedFieldIngressCode code = EncodedFieldIngressCode::DecodeRejected;
std::string detail;
QbinDecodeErrorCode decodeCode = QbinDecodeErrorCode::InvalidHeader;
FieldProfileValidationCode contractCode = FieldProfileValidationCode::InvalidContract;
std::shared_ptr<const DecodedFieldFrame> frame;
bool accepted() const { return code == EncodedFieldIngressCode::Accepted && static_cast<bool>(frame); }
};
class EncodedFieldIngress final {
public:
static EncodedFieldIngressResult decodeAndValidate(const EncodedFieldRuntimeMetadata&,
const ResolvedFieldProfileContract* contract,
std::span<const uint8_t>);
};
} // namespace mln::particle::ingress

View File

@@ -0,0 +1,27 @@
#pragma once
#include <mln/particle/ingress/qbin_v2_decoder.hpp>
namespace mln::particle::ingress {
using QbinV3DecodeErrorCode = QbinDecodeErrorCode;
struct QbinV3DecodeResult {
QbinDecodeErrorCode code = QbinDecodeErrorCode::InvalidHeader;
std::string detail;
std::shared_ptr<const DecodedFieldFrame> frame;
bool accepted() const {
return code == QbinDecodeErrorCode::Accepted && static_cast<bool>(frame);
}
};
class QbinV3Decoder final {
public:
static constexpr uint32_t BlockWidth = 16;
static constexpr uint32_t BlockHeight = 16;
static QbinV3DecodeResult decode(const EncodedFieldRuntimeMetadata&, std::span<const uint8_t>);
};
} // namespace mln::particle::ingress

View File

@@ -0,0 +1,109 @@
#pragma once
#include <atomic>
#include <cstddef>
#include <cstdint>
namespace mln::particle {
// Debug/Validation-only cumulative renderer observability. These atomics are
// intentionally not connected to Host APIs and compile to no-ops in release.
enum class RendererDebugCounter : uint8_t {
WeatherColorNeedsRepaint,
ParticleNeedsRepaint,
WeatherColorUpdate,
ParticleUpdate,
ParticleSimulationAdvance,
WeatherColorDrawableRebuild,
ParticleDrawableRebuild,
FieldTextureUpload,
GpuResourceCreate,
GpuResourceRelease,
TrailTargetCreateOrResize,
V4ProjectionMaterialize,
FieldWindowRevision,
FullRendererFrame,
RetainedUnderlayBuild,
RetainedUnderlayReuse,
RetainedUnderlayFallback,
RetainedUnderlayInvalidation,
RetainedUnderlayStaticGroupDraw,
RetainedDynamicGroupDraw,
ParticleFastFrameHit,
ParticleFastFrameMiss,
ParticleFullFrame,
Count,
};
#ifndef NDEBUG
struct RendererDebugCounterSnapshot {
uint64_t weatherColorNeedsRepaint = 0;
uint64_t particleNeedsRepaint = 0;
uint64_t weatherColorUpdate = 0;
uint64_t particleUpdate = 0;
uint64_t particleSimulationAdvance = 0;
uint64_t weatherColorDrawableRebuild = 0;
uint64_t particleDrawableRebuild = 0;
uint64_t fieldTextureUpload = 0;
uint64_t gpuResourceCreate = 0;
uint64_t gpuResourceRelease = 0;
uint64_t trailTargetCreateOrResize = 0;
uint64_t v4ProjectionMaterialize = 0;
uint64_t fieldWindowRevision = 0;
uint64_t fullRendererFrame = 0;
uint64_t retainedUnderlayBuild = 0;
uint64_t retainedUnderlayReuse = 0;
uint64_t retainedUnderlayFallback = 0;
uint64_t retainedUnderlayInvalidation = 0;
uint64_t retainedUnderlayStaticGroupDraw = 0;
uint64_t retainedDynamicGroupDraw = 0;
uint64_t particleFastFrameHit = 0;
uint64_t particleFastFrameMiss = 0;
uint64_t particleFullFrame = 0;
};
inline std::atomic_uint64_t* rendererDebugCounterStorage() {
static std::atomic_uint64_t counters[static_cast<size_t>(RendererDebugCounter::Count)]{};
return counters;
}
inline void incrementRendererDebugCounter(const RendererDebugCounter counter) {
rendererDebugCounterStorage()[static_cast<size_t>(counter)].fetch_add(1, std::memory_order_relaxed);
}
inline RendererDebugCounterSnapshot rendererDebugCounterSnapshot() {
const auto* values = rendererDebugCounterStorage();
RendererDebugCounterSnapshot result;
#define MLN_LOAD_RENDERER_COUNTER(field, counter) \
result.field = values[static_cast<size_t>(RendererDebugCounter::counter)].load(std::memory_order_relaxed)
MLN_LOAD_RENDERER_COUNTER(weatherColorNeedsRepaint, WeatherColorNeedsRepaint);
MLN_LOAD_RENDERER_COUNTER(particleNeedsRepaint, ParticleNeedsRepaint);
MLN_LOAD_RENDERER_COUNTER(weatherColorUpdate, WeatherColorUpdate);
MLN_LOAD_RENDERER_COUNTER(particleUpdate, ParticleUpdate);
MLN_LOAD_RENDERER_COUNTER(particleSimulationAdvance, ParticleSimulationAdvance);
MLN_LOAD_RENDERER_COUNTER(weatherColorDrawableRebuild, WeatherColorDrawableRebuild);
MLN_LOAD_RENDERER_COUNTER(particleDrawableRebuild, ParticleDrawableRebuild);
MLN_LOAD_RENDERER_COUNTER(fieldTextureUpload, FieldTextureUpload);
MLN_LOAD_RENDERER_COUNTER(gpuResourceCreate, GpuResourceCreate);
MLN_LOAD_RENDERER_COUNTER(gpuResourceRelease, GpuResourceRelease);
MLN_LOAD_RENDERER_COUNTER(trailTargetCreateOrResize, TrailTargetCreateOrResize);
MLN_LOAD_RENDERER_COUNTER(v4ProjectionMaterialize, V4ProjectionMaterialize);
MLN_LOAD_RENDERER_COUNTER(fieldWindowRevision, FieldWindowRevision);
MLN_LOAD_RENDERER_COUNTER(fullRendererFrame, FullRendererFrame);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayBuild, RetainedUnderlayBuild);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayReuse, RetainedUnderlayReuse);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayFallback, RetainedUnderlayFallback);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayInvalidation, RetainedUnderlayInvalidation);
MLN_LOAD_RENDERER_COUNTER(retainedUnderlayStaticGroupDraw, RetainedUnderlayStaticGroupDraw);
MLN_LOAD_RENDERER_COUNTER(retainedDynamicGroupDraw, RetainedDynamicGroupDraw);
MLN_LOAD_RENDERER_COUNTER(particleFastFrameHit, ParticleFastFrameHit);
MLN_LOAD_RENDERER_COUNTER(particleFastFrameMiss, ParticleFastFrameMiss);
MLN_LOAD_RENDERER_COUNTER(particleFullFrame, ParticleFullFrame);
#undef MLN_LOAD_RENDERER_COUNTER
return result;
}
#else
inline void incrementRendererDebugCounter(RendererDebugCounter) {}
#endif
} // namespace mln::particle

View File

@@ -0,0 +1,14 @@
#pragma once
namespace mln {
template <class Texture>
constexpr bool weatherColorDrawableBindingsChanged(const Texture* previous0,
const Texture* previous1,
const Texture* next0,
const Texture* next1,
bool wrapsChanged) noexcept {
return wrapsChanged || previous0 != next0 || previous1 != next1;
}
} // namespace mln

View File

@@ -0,0 +1,321 @@
#pragma once
#include <cstdint>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
namespace mln {
enum class RenderSegmentStrategy : uint8_t { Retained, Continuous };
inline const char* renderSegmentStrategyText(RenderSegmentStrategy strategy) noexcept {
return strategy == RenderSegmentStrategy::Retained ? "RETAINED" : "CONTINUOUS";
}
struct RenderSegmentDefinition {
RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous;
std::optional<std::string> startLayerId;
bool operator==(const RenderSegmentDefinition&) const = default;
};
struct RenderSegmentPlan {
uint32_t schemaVersion = 1;
std::vector<RenderSegmentDefinition> segments;
bool operator==(const RenderSegmentPlan&) const = default;
};
struct StyleLayerDiagnostics {
int32_t layerIndex = -1;
std::string layerId;
std::string layerType;
bool visible = false;
std::optional<std::string> sourceId;
std::optional<std::string> sourceLayer;
bool operator==(const StyleLayerDiagnostics&) const = default;
};
// The style layer list is the single dependency source for diagnostics and rendering.
struct RenderSegmentDependencies {
std::vector<std::string> sourceIds;
std::vector<std::string> globalPrerequisites;
bool unknown = false;
};
inline RenderSegmentDependencies renderSegmentDependencies(const std::vector<StyleLayerDiagnostics>& layers,
int32_t begin,
int32_t endExclusive) {
RenderSegmentDependencies result;
if (begin < 0 || endExclusive < begin || endExclusive > static_cast<int32_t>(layers.size())) {
result.unknown = true;
return result;
}
std::set<std::string> sources;
std::set<std::string> prerequisites;
for (int32_t i = begin; i < endExclusive; ++i) {
const auto& layer = layers[static_cast<std::size_t>(i)];
if (layer.sourceId) sources.insert(*layer.sourceId);
if (layer.layerType == "symbol") prerequisites.insert("symbol/glyph/image/placement");
if (layer.layerType == "custom" || layer.layerType == "custom-drawable") result.unknown = true;
if (layer.layerType != "background" && layer.layerType != "custom" &&
layer.layerType != "custom-drawable" && !layer.sourceId) result.unknown = true;
}
result.sourceIds.assign(sources.begin(), sources.end());
result.globalPrerequisites.assign(prerequisites.begin(), prerequisites.end());
return result;
}
// Placement capability is a property of the style layer, not of this frame's
// buckets or placementData. Invalid ranges fail closed.
inline bool renderSegmentHasPlacementCapableLayer(const std::vector<StyleLayerDiagnostics>& layers,
int32_t begin,
int32_t endExclusive) {
if (begin < 0 || endExclusive < begin || endExclusive > static_cast<int32_t>(layers.size())) return true;
for (int32_t i = begin; i < endExclusive; ++i) {
if (layers[static_cast<std::size_t>(i)].layerType == "symbol") return true;
}
return false;
}
inline bool renderSegmentPlacementGatePassed(bool placementCapable,
bool placementChanged,
float symbolFadeChange,
bool placementTransitionActive) noexcept {
return !placementCapable ||
(!placementChanged && symbolFadeChange == 0.0f && !placementTransitionActive);
}
struct RenderSegmentGlobalLiveGates {
bool retainedMode = false;
bool has3D = false;
bool debugOptions = false;
bool continuousMode = false;
bool spriteLoaded = false;
bool globalResourcesReady = false;
bool globalLightTransition = false;
};
inline const char* renderSegmentGlobalLiveFallbackReason(const RenderSegmentGlobalLiveGates& gates) noexcept {
if (!gates.retainedMode) return "no-boundary";
if (gates.has3D) return "3d";
if (gates.debugOptions) return "debug-options";
if (!gates.continuousMode) return "map-mode";
if (!gates.spriteLoaded) return "sprite-not-loaded";
if (!gates.globalResourcesReady) return "global-resource-not-ready";
if (gates.globalLightTransition) return "global-light-transition";
return "none";
}
class RenderSegmentRevisionBook {
public:
void advanceGlobal() noexcept { ++globalEpoch; }
void advanceSource(const std::string& sourceId) { sourceRevisions[sourceId] = ++sequence; }
std::optional<uint64_t> revisionFor(const RenderSegmentDependencies& dependencies) const {
if (dependencies.unknown) return std::nullopt;
uint64_t revision = globalEpoch;
for (const auto& id : dependencies.sourceIds) {
const auto found = sourceRevisions.find(id);
if (found == sourceRevisions.end()) return std::nullopt;
for (const unsigned char ch : id) revision = (revision ^ ch) * 1099511628211ULL;
revision = (revision ^ found->second) * 1099511628211ULL;
}
return revision;
}
private:
uint64_t globalEpoch = 1;
uint64_t sequence = 1;
// Retain tombstones: remove + recreate of the same ID always receives a newer identity.
std::unordered_map<std::string, uint64_t> sourceRevisions;
};
struct RenderSegmentSourceStatus { bool loaded = false; bool fading = false; };
struct RenderSegmentSourceAssessment {
bool ready = false;
bool stable = false;
const char* reason = "unknown-dependency";
};
template <typename Lookup>
RenderSegmentSourceAssessment assessRenderSegmentSources(const RenderSegmentDependencies& dependencies,
Lookup lookup) {
RenderSegmentSourceAssessment result;
if (dependencies.unknown) return result;
for (const auto& id : dependencies.sourceIds) {
const std::optional<RenderSegmentSourceStatus> source = lookup(id);
if (!source) { result.reason = "missing-source"; return result; }
if (!source->loaded) { result.reason = "source-not-loaded"; return result; }
if (source->fading) { result.ready = true; result.reason = "source-fading"; return result; }
}
result.ready = true;
result.stable = true;
result.reason = "none";
return result;
}
struct RenderSourceReadinessDiagnostics {
std::string sourceId;
std::string sourceType;
bool enabled = false;
bool isLoaded = false;
bool tileBacked = false;
std::optional<uint32_t> tileCount;
std::optional<uint32_t> incompleteTileCount;
std::optional<uint32_t> loadedTileCount;
bool hasFadingTiles = false;
uint64_t lastSourceMutationSequence = 0;
uint64_t lastTileChangedSequence = 0;
uint64_t lastTileActionSequence = 0;
std::string lastTileAction{"none"};
std::string lastMutationReason{"none"};
bool operator==(const RenderSourceReadinessDiagnostics&) const = default;
};
struct RenderMutationDiagnostics {
uint64_t sequence = 0;
uint64_t contentRevision = 0;
std::string eventType{"none"};
std::string reason{"none"};
std::optional<std::string> sourceId;
std::vector<uint32_t> affectedSegmentIndices;
std::string dependencyScope{"none"};
bool operator==(const RenderMutationDiagnostics&) const = default;
};
struct RendererReadinessDiagnostics {
bool styleLoaded = false;
bool imageManagerLoaded = false;
bool renderOrchestratorLoaded = false;
bool spriteLoaded = false;
bool hasFadingTiles = false;
uint64_t contentRevision = 0;
std::vector<RenderSourceReadinessDiagnostics> sources;
RenderMutationDiagnostics lastMutation;
bool operator==(const RendererReadinessDiagnostics&) const = default;
};
struct RenderSegmentCacheDiagnostics {
std::string cacheState{"disabled"};
uint64_t contentRevision = 0;
uint64_t build = 0;
uint64_t reuse = 0;
uint64_t invalidation = 0;
uint64_t fallbackCount = 0;
std::string finalDecision{"disabled"};
std::string lastFallbackReason{"none"};
bool sourceReady = false;
bool sourceStable = false;
bool supported = false;
uint64_t sourceRevision = 0;
std::string lastInvalidationReason{"none"};
uint64_t textureCreate = 0;
uint64_t compositeCreate = 0;
uint64_t resourceRelease = 0;
};
struct RenderSegmentDiagnosticsEntry {
uint32_t segmentIndex = 0;
RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous;
std::optional<std::string> configuredStartLayerId;
int32_t beginIndex = -1;
int32_t endExclusive = -1;
std::optional<std::string> resolvedStartLayerId;
std::optional<std::string> resolvedEndLayerId;
std::vector<StyleLayerDiagnostics> layers;
std::vector<std::string> dependentSourceIds;
std::vector<std::string> unknownDependencies;
bool placementCapable = false;
bool placementGatePassed = false;
std::string actualExecution{"not-rendered"};
std::optional<RenderSegmentCacheDiagnostics> cache;
};
struct RenderSegmentDiagnostics {
uint32_t schemaVersion = 1;
bool enabled = true;
bool configured = false;
bool valid = false;
std::string configSource{"unconfigured/legacy"};
uint64_t configRevision = 0;
uint64_t topologyRevision = 0;
uint32_t styleLayerCount = 0;
uint32_t segmentCount = 0;
uint32_t retainedSegmentCount = 0;
uint32_t continuousSegmentCount = 0;
bool coverageComplete = false;
bool fallback = false;
std::string lastReason{"not-configured"};
bool fastPathEligible = false;
bool fastPathActive = false;
std::string fastPathFallbackReason{"no-prepared-tree"};
uint64_t fastPathHit = 0;
uint64_t fastPathMiss = 0;
uint64_t fullCpuFrame = 0;
bool placementChanged = false;
float symbolFadeChange = 0.0f;
bool placementTransitionActive = false;
bool crossSourceCollisions = true;
std::string mainBufferExecutionMode{"not-rendered"};
std::string globalLiveFallbackReason{"none"};
bool styleLoaded = false;
bool imageManagerLoaded = false;
bool renderOrchestratorLoaded = false;
bool spriteLoaded = false;
bool hasFadingTiles = false;
uint64_t contentRevision = 0;
std::vector<RenderSourceReadinessDiagnostics> sources;
RenderMutationDiagnostics lastMutation;
std::vector<StyleLayerDiagnostics> styleLayers;
std::vector<RenderSegmentDiagnosticsEntry> segments;
};
struct ResolvedRenderSegment {
uint32_t segmentIndex = 0;
RenderSegmentStrategy strategy = RenderSegmentStrategy::Continuous;
std::optional<std::string> configuredStartLayerId;
int32_t beginIndex = 0;
int32_t endExclusive = 0;
bool operator==(const ResolvedRenderSegment&) const = default;
};
struct ResolvedRenderSegmentPlan {
bool configured = false;
bool valid = false;
std::string configSource{"unconfigured/legacy"};
uint64_t configRevision = 0;
std::string reason{"not-configured"};
int32_t layerCount = 0;
std::vector<ResolvedRenderSegment> segments;
bool operator==(const ResolvedRenderSegmentPlan&) const = default;
};
// This is the main-buffer traversal used by the renderer. A failed retained range
// is drawn live exactly once; neighboring cached ranges remain composites.
template <typename Cached, typename Composite, typename Live>
void visitRenderSegmentExecution(const ResolvedRenderSegmentPlan& plan,
Cached cached,
Composite composite,
Live live) {
if (!plan.valid) {
live(0, plan.layerCount);
return;
}
for (const auto& segment : plan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained && cached(segment.segmentIndex)) {
composite(segment.segmentIndex);
} else {
live(segment.beginIndex, segment.endExclusive);
}
}
}
} // namespace mln

View File

@@ -34,6 +34,9 @@ public:
/// Get the render target texture
const gfx::Texture2DPtr& getTexture();
/// Create and validate the backing offscreen framebuffer.
bool isRenderable();
/// @brief Add a layer group to the render target
/// @param replace Flag to replace if exists
/// @return whether added

View File

@@ -0,0 +1,74 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
namespace mln {
struct RetainedUnderlayDiagnostics {
bool enabled = true;
uint64_t build = 0;
uint64_t reuse = 0;
uint64_t fallback = 0;
uint64_t invalidation = 0;
std::string lastReason;
bool rangeConfigured = false;
bool rangeValid = false;
std::string startLayerId;
std::string endLayerId;
int32_t startIndex = -1;
int32_t endIndex = -1;
int32_t continuousBegin = -1;
int32_t continuousEndExclusive = -1;
bool bottomPresent = false;
std::string bottomCacheState{"disabled"};
uint64_t bottomBuild = 0;
uint64_t bottomReuse = 0;
uint64_t bottomInvalidation = 0;
std::string bottomLastInvalidationReason{"none"};
uint64_t bottomTextureCreate = 0;
uint64_t bottomCompositeCreate = 0;
uint64_t bottomResourceRelease = 0;
bool topPresent = false;
std::string topCacheState{"disabled"};
uint64_t topBuild = 0;
uint64_t topReuse = 0;
uint64_t topInvalidation = 0;
std::string topLastInvalidationReason{"none"};
uint64_t topTextureCreate = 0;
uint64_t topCompositeCreate = 0;
uint64_t topResourceRelease = 0;
bool rangeFallback = false;
bool fastPathEligible = false;
bool fastPathActive = false;
std::string fastPathFallbackReason{"no-prepared-tree"};
uint64_t fastPathHit = 0;
uint64_t fastPathMiss = 0;
uint64_t fullCpuFrame = 0;
};
struct ContinuousLayerRangeConfig {
bool configured = false;
std::string startLayerId;
std::string endLayerId;
uint64_t revision = 0;
};
struct ContinuousLayerRangeResolution {
bool configured = false;
bool valid = false;
std::string startLayerId;
std::string endLayerId;
int32_t startIndex = -1;
int32_t endIndex = -1;
uint64_t configRevision = 0;
std::string reason{"not-configured"};
int32_t begin() const noexcept { return startIndex; }
int32_t endExclusive() const noexcept { return valid ? endIndex + 1 : -1; }
bool bottomPresent() const noexcept { return valid && startIndex > 0; }
bool topPresent(int32_t layerCount) const noexcept { return valid && endExclusive() < layerCount; }
};
} // namespace mln

View File

@@ -0,0 +1,417 @@
// Generated code, do not modify this file!
#pragma once
#include <mln/shaders/shader_source.hpp>
namespace mln {
namespace shaders {
template <>
struct ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::OpenGL> {
static constexpr const char* name = "ParticleGpuValidationShader";
static constexpr const char* vertex = R"(layout (location = 0) in vec2 a_pos;
out vec2 v_uv;
out vec2 v_world;
out vec2 v_particle_velocity;
out float v_particle_valid;
layout (std140) uniform ParticleGpuValidationUBO {
highp vec4 u_pass;
highp vec4 u_marker;
highp vec4 u_mask;
highp vec4 u_visual;
highp vec4 u_field_bounds;
highp vec4 u_respawn_bounds;
highp vec4 u_field_scale;
highp vec4 u_advection;
highp vec4 u_particle_color;
highp vec4 u_trail_color;
highp vec4 u_weather_stops[8];
highp vec4 u_weather_colors[8];
highp vec4 u_weather_ramp;
highp mat4 u_matrix;
};
uniform sampler2D u_state_texture;
uniform sampler2D u_previous;
uniform highp sampler2D u_field_texture;
uniform highp sampler2D u_next_field_texture;
vec2 fieldUv(vec2 world) {
return vec2((world.x - u_field_bounds.x) / (u_field_bounds.z - u_field_bounds.x),
(world.y - u_field_bounds.y) / (u_field_bounds.w - u_field_bounds.y));
}
bool validFieldUv(vec2 uv) {
return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
}
bool resolveAdvection(vec4 field0, vec4 field1, out vec2 advection) {
vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
u_marker.x);
if (any(isnan(mixed)) || any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
if (u_advection.x > 0.5) {
float directionLength = length(mixed);
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
advection = mixed / directionLength * u_advection.y;
} else {
// VELOCITY remains the established component-wise m/s path.
advection = mixed;
}
return true;
}
void main() {
v_uv = a_pos;
v_world = a_pos;
v_particle_velocity = vec2(0.0);
v_particle_valid = 1.0;
if (u_pass.x > 4.5) {
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
} else if (u_pass.x < 1.5) {
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
} else if (u_pass.x > 3.5) {
// a_pos.x packs State texel X plus one endpoint. The formal particle
// path draws the StateRead -> StateWrite displacement in current map
// clip space; it does not synthesize a fixed world-length streak.
float endpoint = mod(a_pos.x, 2.0) * 2.0 - 1.0;
vec2 stateIndex = vec2(floor(a_pos.x * 0.5), a_pos.y);
vec2 stateUV = (stateIndex + vec2(0.5)) / u_mask.xy;
vec4 previousState = texture(u_previous, stateUV);
vec4 currentState = texture(u_state_texture, stateUV);
vec2 currentPosition = currentState.rg;
vec2 uv = fieldUv(currentPosition);
vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv);
vec2 advection;
bool valid = validFieldUv(uv) && field0.a >= 0.5 && field1.a >= 0.5 &&
resolveAdvection(field0, field1, advection);
vec2 mercatorVelocity = vec2(advection.x, -advection.y);
float speed = length(mercatorVelocity);
vec2 segmentPosition = endpoint < 0.0 ? previousState.rg : currentPosition;
v_uv = stateUV;
v_world = currentPosition;
v_particle_velocity = mercatorVelocity;
// A zero alpha is written by State on a respawn frame. This suppresses
// oldPosition -> newRandomPosition cross-screen segments.
v_particle_valid = valid && speed > 0.00001 && currentState.a > 0.5 &&
distance(previousState.rg, currentPosition) > 0.000000001
? 1.0
: 0.0;
if (v_particle_valid < 0.5) {
gl_Position = vec4(-2.0, -2.0, 0.0, 1.0);
} else {
gl_Position = u_matrix * vec4(segmentPosition * 8192.0, 0.0, 1.0);
}
} else {
if (u_mask.z > 0.5) {
// Trail targets are viewport-sized screen-space history buffers.
gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
} else {
// VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix
// is PaintParameters::matrixForTile() for the selected world copy.
gl_Position = u_matrix * vec4(a_pos * 8192.0, 0.0, 1.0);
}
}
}
)";
static constexpr const char* fragment = R"(// The shared GL prelude defaults fragment float to mediump. Byte reconstruction
// from RGBA8 UNORM and Mercator state require highp on ES 3.x.
precision highp float;
layout (std140) uniform ParticleGpuValidationUBO {
highp vec4 u_pass;
highp vec4 u_marker;
highp vec4 u_mask;
highp vec4 u_visual;
highp vec4 u_field_bounds;
highp vec4 u_respawn_bounds;
highp vec4 u_field_scale;
highp vec4 u_advection;
highp vec4 u_particle_color;
highp vec4 u_trail_color;
highp vec4 u_weather_stops[8];
highp vec4 u_weather_colors[8];
highp vec4 u_weather_ramp;
highp mat4 u_matrix;
};
uniform sampler2D u_previous;
uniform sampler2D u_mask_texture;
uniform highp sampler2D u_field_texture;
uniform highp sampler2D u_next_field_texture;
uniform sampler2D u_state_texture;
in vec2 v_uv;
in vec2 v_world;
in vec2 v_particle_velocity;
in float v_particle_valid;
vec3 particleSpeedColor(float speed) {
float t = clamp(speed * 0.75, 0.0, 1.0);
vec3 slow = vec3(0.10, 0.45, 1.00);
vec3 medium = vec3(0.10, 0.95, 0.72);
vec3 fast = vec3(1.00, 0.48, 0.12);
return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t));
}
vec2 fieldUv(vec2 world) {
return vec2((world.x - u_field_bounds.x) / (u_field_bounds.z - u_field_bounds.x),
(world.y - u_field_bounds.y) / (u_field_bounds.w - u_field_bounds.y));
}
bool validFieldUv(vec2 uv) {
return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));
}
const float EARTH_CIRCUMFERENCE_METERS = 40075016.68557849;
float mercatorLatitudeRadians(float worldY) {
return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY)));
}
vec3 weatherDefaultValueColor(float value) {
// Historical default ramp endpoints are 0 and 30 generic field-value
// units. Hosts provide explicit stops whenever another scale is needed.
float t = clamp(value / 30.0, 0.0, 1.0);
vec3 slow = vec3(0.08, 0.18, 0.65);
vec3 medium = vec3(0.08, 0.80, 0.78);
vec3 fast = vec3(0.98, 0.22, 0.10);
return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t));
}
float particleHash(vec2 value) {
return fract(sin(dot(value, vec2(127.1, 311.7))) * 43758.5453123);
}
vec2 respawnSeed(vec2 particleUV, vec2 entropy, float attempt) {
vec2 salt = entropy * vec2(13.17, 19.91) + particleUV * vec2(97.23, 53.71) + attempt;
vec2 random = vec2(particleHash(salt), particleHash(salt.yx + 41.29));
return mix(u_respawn_bounds.xy, u_respawn_bounds.zw, random);
}
bool outsideRespawnBounds(vec2 position) {
return position.x < u_respawn_bounds.x || position.x > u_respawn_bounds.z ||
position.y < u_respawn_bounds.y || position.y > u_respawn_bounds.w;
}
bool fieldAdvectionAt(vec2 position, out vec2 advection) {
vec2 uv = fieldUv(position);
vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv);
vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
u_marker.x);
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5 || any(isnan(mixed)) ||
any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
if (u_advection.x > 0.5) {
float directionLength = length(mixed);
if (isnan(directionLength) || directionLength > 3.4e38 || directionLength <= u_advection.z) return false;
advection = mixed / directionLength * u_advection.y;
return true;
}
// VELOCITY keeps its previous component-wise interpolation and magnitude.
advection = mixed;
return length(advection) > 0.00001;
}
vec4 weatherRampColor(float value) {
int count = int(u_weather_ramp.x);
if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0);
vec4 color = u_weather_colors[0];
for (int i = 1; i < 8; ++i) {
if (i >= count) break;
float lo = u_weather_stops[i - 1].x;
float hi = u_weather_stops[i].x;
if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0));
color = u_weather_colors[i];
}
return color;
}
void main() {
float mode = u_pass.x;
if (mode > 4.5) {
vec2 uv = fieldUv(v_world);
if (!validFieldUv(uv)) discard;
vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv);
if (field0.a < 0.5 || field1.a < 0.5) discard;
float fieldValue;
if (u_weather_ramp.y > 0.5) {
vec2 vectorValue = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
u_marker.x);
if (any(isnan(vectorValue))) discard;
fieldValue = length(vectorValue);
} else {
fieldValue = mix(((floor(field0.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
((floor(field1.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
u_marker.x);
if (isnan(fieldValue)) discard;
}
vec4 color = weatherRampColor(fieldValue);
float alpha = color.a * u_visual.x;
fragColor = vec4(color.rgb * alpha, alpha);
return;
}
vec4 previous = texture(u_previous, v_uv);
if (mode < 0.5) {
// State A/B samples the uploaded canonical Particle Field.
// RG stores Web Mercator world position, B stores age, and the Field
// texture encodes canonical U-east/V-north at that geographic sample.
// u_mask.w is the explicit State lifecycle flag: new RenderTargets are
// cleared opaque by MapLibre, so sampled alpha cannot identify a seed.
if (u_mask.z > 0.5) {
// Formal particles use GPU-resident lifecycle state. Each slot keeps
// its age in B; A=0 marks this frame as a respawn so Trail does not
// connect the retired position to the new randomized one.
bool initialSeed = u_mask.w > 0.5;
float lifecycleHash = particleHash(v_uv + vec2(17.31, 83.17));
float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash);
float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47));
bool respawn = initialSeed;
// Lifecycle is wall-clock based: zoom-aware visual advection and
// forecast playback must not change respawn cadence.
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.w;
vec2 position = previous.rg;
vec2 advection;
bool valid = !respawn && fieldAdvectionAt(position, advection);
// The field bounds remain the only sampling domain. Once the
// camera is stable, slots already outside the expanded viewport
// may recycle into its separate preferred respawn domain.
if (!valid || age >= maxAge ||
(u_field_scale.z > 0.5 && !initialSeed && outsideRespawnBounds(position))) {
respawn = true;
}
if (!respawn) {
float latitude = mercatorLatitudeRadians(position.y);
vec2 displacement = vec2(advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)),
-advection.y * u_pass.y /
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
vec2 nextPosition = position + displacement;
if (fieldAdvectionAt(nextPosition, advection)) {
position = nextPosition;
} else {
respawn = true;
}
}
if (respawn) {
bool found = false;
// Fixed, bounded retries keep the State pass deterministic and
// avoid a CPU upload or an unbounded shader loop.
for (int attempt = 0; attempt < 3; ++attempt) {
vec2 candidate = respawnSeed(v_uv, previous.rg, float(attempt));
if (fieldAdvectionAt(candidate, advection)) {
position = candidate;
found = true;
break;
}
}
if (!found) {
// Keep a deterministic in-bounds candidate and retry next
// frame; alpha still prevents an invalid visible segment.
position = respawnSeed(v_uv, previous.rg, 3.0);
}
if (!initialSeed) {
age = 0.0;
}
}
fragColor = vec4(position, age, respawn ? 0.0 : 1.0);
return;
}
vec2 seed = mix(u_field_bounds.xy, u_field_bounds.zw, v_uv);
vec2 position = u_mask.w > 0.5 ? seed : previous.rg;
// 40-02: decode each independent Field texture, then interpolate the
// velocity on GPU. u_marker.x is the clamped Layer timeMix property.
vec2 uv = fieldUv(position);
vec4 field0 = texture(u_field_texture, uv);
vec4 field1 = texture(u_next_field_texture, uv);
vec2 advection;
if (!fieldAdvectionAt(position, advection)) {
position = seed;
advection = vec2(0.0);
}
// U/V remain canonical metres per second. u_pass.y is the explicit
// visual simulation delta in seconds; the validation clock may run
// faster than wall time, but it never changes the field contract.
// Both east and north metres scale with sec(latitude) in Web Mercator.
// V is north-positive while world y is southward.
float latitude = mercatorLatitudeRadians(position.y);
float dx = advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
float dy = -advection.y * u_pass.y /
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
position = position + vec2(dx, dy);
if (!validFieldUv(fieldUv(position)) || any(isnan(advection))) position = seed;
fragColor = vec4(position, fract(previous.b + 0.03125 * u_pass.y), 1.0);
} else if (mode < 1.5) {
// Formal Trail first fades the previous target. Its full-particle point
// draw follows in the same target and consumes every State texel.
if (u_mask.z > 0.5) {
// trailFade remains the UI's retention-at-60Hz value. Raising it
// to elapsed wall time makes visual persistence FPS-independent.
float fadeFactor = pow(clamp(u_visual.w, 0.0001, 1.0), u_pass.y * u_marker.w);
// R is history coverage, G marks the current segment, B its speed.
// Apply independent Host colours only at composite time.
fragColor = vec4(previous.r * fadeFactor, 0.0, 0.0, 1.0);
return;
}
// LEGACY VALIDATION DISPLAY: retain the original four deterministic
// samples for the historical ParticleValidationLayer only.
float marker = 0.0;
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.125, 0.125)).rg)));
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.375, 0.375)).rg)));
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.625, 0.625)).rg)));
marker = max(marker, 1.0 - smoothstep(0.0, u_marker.z, distance(v_uv, texture(u_state_texture, vec2(0.875, 0.875)).rg)));
vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0);
fragColor = vec4(min(color, vec3(1.0)), 1.0);
} else if (mode > 3.5) {
if (v_particle_valid < 0.5) discard;
float speed = length(v_particle_velocity);
fragColor = vec4(1.0, 1.0, clamp(speed / 30.0, 0.0, 1.0), 1.0);
} else {
// The formal RasterParticle layer displays the Trail texture written by
// the current State -> Trail pass. Legacy ParticleValidation keeps its
// independent 30-03 stripe/anchor diagnostic display below.
float intensity = max(previous.r, max(previous.g, previous.b));
float ocean = texture(u_mask_texture, v_world).a;
if (u_pass.z > 0.5 && ocean < 0.5) discard;
if (u_mask.z > 0.5) {
bool current = previous.g > 0.5;
vec4 base = current ? u_particle_color : u_trail_color;
if (current && u_marker.y > 0.5) base.rgb = particleSpeedColor(previous.b * 30.0);
float alpha = clamp(previous.r * base.a * (current ? u_visual.x : u_visual.z), 0.0, 1.0);
fragColor = vec4(base.rgb * alpha, alpha);
return;
}
// LEGACY VALIDATION DISPLAY: preserve the frozen 30-03
// stripe/anchor/world-wrap diagnostic for ParticleValidationLayer.
float stripe = 0.55 + 0.45 * sin((v_world.x + v_world.y) * 100.0 + u_pass.y * 0.07);
// A=(0°,0°), B=(-90°,approximately 0°), C/D straddle ±180°.
float anchors = 0.0;
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.50, 0.50))));
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.25, 0.50))));
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.02, 0.50))));
anchors = max(anchors, 1.0 - smoothstep(0.0, 0.008, distance(v_world, vec2(0.98, 0.50))));
vec3 color = u_pass.w < 1.5 ? vec3(0.10, 0.70, 1.00) : vec3(0.35, 1.00, 0.45);
fragColor = vec4(mix(color * stripe, vec3(1.00, 0.82, 0.15), anchors),
0.28 + min(intensity, 0.35) * 0.35 + anchors * 0.35);
}
#ifdef OVERDRAW_INSPECTOR
fragColor = vec4(1.0);
#endif
}
)";
};
} // namespace shaders
} // namespace mln

View File

@@ -0,0 +1,29 @@
// Generated code, do not modify this file!
#pragma once
#include <mln/shaders/shader_source.hpp>
namespace mln {
namespace shaders {
template <>
struct ShaderSource<BuiltIn::RetainedUnderlayCompositeShader, gfx::Backend::Type::OpenGL> {
static constexpr const char* name = "RetainedUnderlayCompositeShader";
static constexpr const char* vertex = R"(layout (location = 0) in vec2 a_pos;
out vec2 v_uv;
void main() {
gl_Position = vec4(a_pos.x * 2.0 - 1.0, 1.0 - a_pos.y * 2.0, 0.0, 1.0);
v_uv = vec2(a_pos.x, 1.0 - a_pos.y);
}
)";
static constexpr const char* fragment = R"(in vec2 v_uv;
uniform sampler2D u_image;
void main() {
fragColor = texture(u_image, v_uv);
}
)";
};
} // namespace shaders
} // namespace mln

View File

@@ -149,6 +149,20 @@ struct ShaderInfo<BuiltIn::HeatmapTextureShader, gfx::Backend::Type::OpenGL> {
static const std::vector<TextureInfo> textures;
};
template <>
struct ShaderInfo<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::OpenGL> {
static const std::vector<AttributeInfo> attributes;
static const std::vector<UniformBlockInfo> uniformBlocks;
static const std::vector<TextureInfo> textures;
};
template <>
struct ShaderInfo<BuiltIn::RetainedUnderlayCompositeShader, gfx::Backend::Type::OpenGL> {
static const std::vector<AttributeInfo> attributes;
static const std::vector<UniformBlockInfo> uniformBlocks;
static const std::vector<TextureInfo> textures;
};
template <>
struct ShaderInfo<BuiltIn::HillshadePrepareShader, gfx::Backend::Type::OpenGL> {
static const std::vector<AttributeInfo> attributes;

View File

@@ -0,0 +1,142 @@
#pragma once
#include <mln/shaders/particle_gpu_validation_ubo.hpp>
#include <mln/shaders/shader_source.hpp>
#include <mln/shaders/mtl/shader_program.hpp>
#include <array>
namespace mln::shaders {
// This is intentionally a backend-only translation of the formal particle
// shader. ParticleGpuValidationUBO remains the single C++ ABI shared by GL and
// Metal; the MSL declaration below is kept vec4/mat4-only to preserve its
// alignas(16), 31 * 16-byte contract.
constexpr auto particleGpuValidationShaderPrelude = R"(
enum {
idParticleGpuValidationUBO = drawableReservedUBOCount,
particleGpuValidationUBOCount
};
struct alignas(16) ParticleGpuValidationUBO {
float4 pass; float4 marker; float4 mask; float4 visual;
float4 fieldBounds; float4 respawnBounds; float4 fieldScale; float4 advection;
float4 particleColor; float4 trailColor; float4 weatherStops[8];
float4 weatherColors[8]; float4 weatherRamp; float4x4 matrix;
};
static_assert(sizeof(ParticleGpuValidationUBO) == 31 * 16, "wrong size");
)";
template <>
struct ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Metal> {
static constexpr auto name = "ParticleGpuValidationShader";
static constexpr auto vertexMainFunction = "vertexMain";
static constexpr auto fragmentMainFunction = "fragmentMain";
static const std::array<AttributeInfo, 1> attributes;
static constexpr std::array<AttributeInfo, 0> instanceAttributes{};
static const std::array<TextureInfo, 5> textures;
static constexpr auto prelude = particleGpuValidationShaderPrelude;
static constexpr auto source = R"(
struct VertexStage { short2 pos [[attribute(0)]]; };
struct FragmentStage { float4 position [[position, invariant]]; float2 uv; float2 world; float2 velocity; float valid; };
float2 fieldUv(float2 world, device const ParticleGpuValidationUBO& u) {
return float2((world.x-u.fieldBounds.x)/(u.fieldBounds.z-u.fieldBounds.x),
(world.y-u.fieldBounds.y)/(u.fieldBounds.w-u.fieldBounds.y));
}
bool validFieldUv(float2 uv) { return all(uv >= float2(0.0)) && all(uv <= float2(1.0)); }
bool finite2(float2 value) { return all(isfinite(value)) && all(abs(value) <= float2(3.4e38)); }
bool resolveAdvection(float4 a, float4 b, device const ParticleGpuValidationUBO& u, thread float2& out) {
float2 mixed=mix(((floor(a.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.x,((floor(b.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.y,u.marker.x);
if (!finite2(mixed)) return false;
if (u.advection.x > .5) { float l=length(mixed); if (!isfinite(l)||l>3.4e38||l<=u.advection.z) return false; out=mixed/l*u.advection.y; }
else { out=mixed; return length(out)>0.00001; }
return true;
}
float particleHash(float2 v) { return fract(sin(dot(v,float2(127.1,311.7)))*43758.5453123); }
float2 respawnSeed(float2 uv, float2 entropy, float attempt, device const ParticleGpuValidationUBO& u) {
float2 salt=entropy*float2(13.17,19.91)+uv*float2(97.23,53.71)+attempt;
return mix(u.respawnBounds.xy,u.respawnBounds.zw,float2(particleHash(salt),particleHash(salt.yx+41.29)));
}
float mercatorLatitudeRadians(float y) { return atan(sinh(M_PI_F*(1.0-2.0*y))); }
FragmentStage vertex vertexMain(thread const VertexStage in [[stage_in]],
device const ParticleGpuValidationUBO& u [[buffer(idParticleGpuValidationUBO)]],
texture2d<float> previous [[texture(0)]], texture2d<float> field [[texture(2)]],
texture2d<float> nextField [[texture(3)]], texture2d<float> state [[texture(4)]],
sampler previousSampler [[sampler(0)]], sampler fieldSampler [[sampler(2)]],
sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) {
// a_pos is an unnormalized short2, matching the OpenGL contract.
float2 pos=float2(in.pos);
FragmentStage out={}; out.uv=pos; out.world=pos; out.valid=1.0;
if (u.pass.x > 5.5) {
float corner=glMod(pos.x,4.0);
float2 index=float2(floor(pos.x*.25),pos.y);
float2 stateUv=(index+.5)/u.mask.xy;
float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv);
out.uv=stateUv; out.world=current.rg;
bool stateOk=current.a>.5&&distance(old.rg,current.rg)>1e-9;
float2 uv=fieldUv(current.rg,u);
float4 a=field.sample(fieldSampler,uv), b=nextField.sample(nextFieldSampler,uv);
float2 advection;
bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);
out.velocity=float2(advection.x,-advection.y);
out.valid=stateOk&&ok&&length(out.velocity)>0.00001?1.0:0.0;
float4 clip=u.matrix*float4(current.rg*8192.0,0,1);
float2 cornerOffset=corner<.5?float2(-1,-1):(corner<1.5?float2(1,-1):(corner<2.5?float2(-1,1):float2(1,1)));
float2 halfPixelNdc=float2(1.0/max(u.fieldScale.w,1.0),1.0/max(u.advection.w,1.0));
clip.xy+=cornerOffset*halfPixelNdc*clip.w;
out.position=out.valid<.5?float4(-2,-2,0,1):clip;
} else if (u.pass.x > 4.5) {
out.position=u.matrix*float4(pos*8192.0,0,1);
} else if (u.pass.x < 1.5) {
// Metal render targets are addressed from the top-left. Flip the
// fullscreen varying so the same logical State/Trail texel is sampled
// on the next ping-pong pass. Particle texel indices remain unflipped.
out.uv=float2(pos.x,1.0-pos.y);
out.position=float4(pos*2.0-1.0,0,1);
} else if (u.pass.x > 3.5) {
float endpoint=glMod(pos.x,2.0)*2.0-1.0;
float2 index=float2(floor(pos.x*.5),pos.y);
float2 stateUv=(index+.5)/u.mask.xy;
float4 old=previous.sample(previousSampler,stateUv), current=state.sample(stateSampler,stateUv);
out.uv=stateUv; out.world=current.rg;
bool stateOk=current.a>.5&&distance(old.rg,current.rg)>1e-9;
float2 uv=fieldUv(current.rg,u); float4 a=field.sample(fieldSampler,uv), b=nextField.sample(nextFieldSampler,uv); float2 advection;
bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);
out.velocity=float2(advection.x,-advection.y);
out.valid=stateOk&&ok&&length(out.velocity)>0.00001?1.0:0.0;
out.position=out.valid<.5?float4(-2,-2,0,1):u.matrix*float4((endpoint<0?old.rg:current.rg)*8192.0,0,1);
} else {
if (u.mask.z>.5) {
// Composite samples the viewport-sized Trail render target.
out.uv=float2(pos.x,1.0-pos.y);
out.position=float4(pos*2.0-1.0,0,1);
} else {
out.position=u.matrix*float4(pos*8192.0,0,1);
}
}
return out;
}
float3 speedColor(float speed) { float t=clamp(speed*.75,0.0,1.0); return mix(mix(float3(.10,.45,1),float3(.10,.95,.72),smoothstep(0.0,.55,t)),float3(1,.48,.12),smoothstep(.45,1,t)); }
float4 ramp(float value, device const ParticleGpuValidationUBO& u) { int count=int(u.weatherRamp.x); if(count<2){float t=clamp(value/30.0,0.0,1.0);return float4(mix(mix(float3(.08,.18,.65),float3(.08,.80,.78),smoothstep(0.,.55,t)),float3(.98,.22,.10),smoothstep(.45,1.,t)),1); } float4 c=u.weatherColors[0]; for(int i=1;i<8&&i<count;i++){float lo=u.weatherStops[i-1].x,hi=u.weatherStops[i].x;if(value<=hi)return mix(c,u.weatherColors[i],clamp((value-lo)/(hi-lo),0.,1.));c=u.weatherColors[i];}return c; }
float4 fragment fragmentMain(FragmentStage in [[stage_in]], device const ParticleGpuValidationUBO& u [[buffer(idParticleGpuValidationUBO)]],
texture2d<float> previous [[texture(0)]], texture2d<float> mask [[texture(1)]], texture2d<float> field [[texture(2)]], texture2d<float> nextField [[texture(3)]], texture2d<float> state [[texture(4)]],
sampler previousSampler [[sampler(0)]], sampler maskSampler [[sampler(1)]], sampler fieldSampler [[sampler(2)]], sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) {
float mode=u.pass.x;
if(mode>5.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);}
if(mode>4.5){float2 uv=fieldUv(in.world,u); if(!validFieldUv(uv)) discard_fragment(); float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(a.a<.5||b.a<.5)discard_fragment();float value=u.weatherRamp.y>.5?length(mix(((floor(a.rg*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.rg*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x)):mix(((floor(a.r*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.r*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x);if(!isfinite(value))discard_fragment();float4 c=ramp(value,u);float alpha=c.a*u.visual.x;return float4(c.rgb*alpha,alpha);}
float4 old=previous.sample(previousSampler,in.uv);
if(mode<.5){
float2 position=u.mask.w>.5?mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv):old.rg; float age=old.b; bool initial=u.mask.w>.5; bool respawn=initial; float2 advection; float maxAge=u.marker.z*mix(.75,1.25,particleHash(in.uv+float2(17.31,83.17)));
if(u.mask.z>.5){age=initial?particleHash(in.uv+float2(71.19,29.47))*maxAge:old.b+u.pass.w;float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);respawn=initial||!ok||age>=maxAge||(u.fieldScale.z>.5&&!initial&&(position.x<u.respawnBounds.x||position.x>u.respawnBounds.z||position.y<u.respawnBounds.y||position.y>u.respawnBounds.w));if(!respawn){float lat=mercatorLatitudeRadians(position.y);float2 n=position+float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));float2 nuv=fieldUv(n,u);float4 na=field.sample(fieldSampler,nuv),nb=nextField.sample(nextFieldSampler,nuv);if(validFieldUv(nuv)&&na.a>=.5&&nb.a>=.5&&resolveAdvection(na,nb,u,advection))position=n;else respawn=true;}if(respawn){bool found=false;for(int i=0;i<3;i++){float2 c=respawnSeed(in.uv,old.rg,float(i),u);float2 cuv=fieldUv(c,u);float4 ca=field.sample(fieldSampler,cuv),cb=nextField.sample(nextFieldSampler,cuv);if(validFieldUv(cuv)&&ca.a>=.5&&cb.a>=.5&&resolveAdvection(ca,cb,u,advection)){position=c;found=true;break;}}if(!found)position=respawnSeed(in.uv,old.rg,3.,u);if(!initial)age=0.;}return float4(position.x,position.y,age,respawn?0.:1.);}
float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(!validFieldUv(uv)||a.a<.5||b.a<.5||!resolveAdvection(a,b,u,advection)){position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);advection=float2(0);}float lat=mercatorLatitudeRadians(position.y);position+=float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));if(!validFieldUv(fieldUv(position,u))||!finite2(advection))position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);return float4(position.x,position.y,fract(old.b+.03125*u.pass.y),1.);
}
if(mode<1.5){if(u.mask.z>.5){float f=pow(clamp(u.visual.w,.0001,1.),u.pass.y*u.marker.w);return float4(old.r*f,0,0,1);}float marker=0.;float2 samples[4]={float2(.125),float2(.375),float2(.625),float2(.875)};for(int i=0;i<4;i++)marker=max(marker,1.-smoothstep(0.,u.marker.z,distance(in.uv,state.sample(stateSampler,samples[i]).rg)));return float4(min(old.rgb*u.marker.w+marker*float3(.08,.55,1),float3(1)),1);}
if(mode>3.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);}
float ocean=mask.sample(maskSampler,in.world).a;if(u.pass.z>.5&&ocean<.5)discard_fragment();if(u.mask.z>.5){bool current=old.g>.5;float4 base=current?u.particleColor:u.trailColor;if(current&&u.marker.y>.5)base.rgb=speedColor(old.b*30.);float alpha=clamp(old.r*base.a*(current?u.visual.x:u.visual.z),0.,1.);return float4(base.rgb*alpha,alpha);}float intensity=max(old.r,max(old.g,old.b));float stripe=.55+.45*sin((in.world.x+in.world.y)*100.+u.pass.y*.07);float anchors=0.;float2 a[4]={float2(.5,.5),float2(.25,.5),float2(.02,.5),float2(.98,.5)};for(int i=0;i<4;i++)anchors=max(anchors,1.-smoothstep(0.,.008,distance(in.world,a[i])));float3 color=u.pass.w<1.5?float3(.10,.70,1.):float3(.35,1.,.45);return float4(mix(color*stripe,float3(1.,.82,.15),anchors),.28+min(intensity,.35)*.35+anchors*.35);
}
)";
};
} // namespace mln::shaders

View File

@@ -0,0 +1,34 @@
#pragma once
#include <mln/shaders/shader_source.hpp>
#include <mln/shaders/mtl/shader_program.hpp>
namespace mln::shaders {
template <>
struct ShaderSource<BuiltIn::RetainedUnderlayCompositeShader, gfx::Backend::Type::Metal> {
static constexpr auto name = "RetainedUnderlayCompositeShader";
static constexpr auto vertexMainFunction = "vertexMain";
static constexpr auto fragmentMainFunction = "fragmentMain";
static const std::array<AttributeInfo, 1> attributes;
static constexpr std::array<AttributeInfo, 0> instanceAttributes{};
static const std::array<TextureInfo, 1> textures;
static constexpr auto prelude = R"()";
static constexpr auto source = R"(
struct VertexStage { short2 pos [[attribute(0)]]; };
struct FragmentStage { float4 position [[position, invariant]]; float2 uv; };
FragmentStage vertex vertexMain(thread const VertexStage in [[stage_in]]) {
const float2 p = float2(in.pos);
return { .position = float4(p.x * 2.0 - 1.0, 1.0 - p.y * 2.0, 0.0, 1.0), .uv = p };
}
half4 fragment fragmentMain(FragmentStage in [[stage_in]],
texture2d<float, access::sample> image [[texture(0)]],
sampler imageSampler [[sampler(0)]]) {
return half4(image.sample(imageSampler, in.uv));
}
)";
};
} // namespace mln::shaders

View File

@@ -0,0 +1,40 @@
#pragma once
#include <array>
namespace mln {
namespace shaders {
// VALIDATION ONLY: 30-02 State/Trail and 30-03 Camera/Mask proof parameters.
struct alignas(16) ParticleGpuValidationUBO {
std::array<float, 4> pass;
std::array<float, 4> marker;
std::array<float, 4> mask;
// Formal RasterParticle visual controls: particle opacity, canonical
// streak length, trail opacity, and Trail A/B fade multiplier.
std::array<float, 4> visual;
// west, north, east, south in normalized Web Mercator world coordinates.
// Field textures are regional; they must never be sampled as global UVs.
std::array<float, 4> fieldBounds;
// Preferred respawn domain. It is the expanded stable viewport intersected
// with fieldBounds, or fieldBounds when that intersection is unusable.
std::array<float, 4> respawnBounds;
// Per-texture signed vector decoding scales. WeatherColor also uses these
// for its explicitly selected generic scalar/value plane.
// z is viewportDensityActive for the formal validation path.
std::array<float, 4> fieldScale;
// x: Direction semantics flag; y: Direction-only advection scale;
// z: mixed-direction fail-closed epsilon; w: reserved.
std::array<float, 4> advection;
// Host-owned base colours. They are independent from WeatherColor stops.
std::array<float, 4> particleColor;
std::array<float, 4> trailColor;
std::array<std::array<float, 4>, 8> weatherStops;
std::array<std::array<float, 4>, 8> weatherColors;
std::array<float, 4> weatherRamp;
std::array<float, 16> matrix;
};
static_assert(sizeof(ParticleGpuValidationUBO) == 31 * 16);
} // namespace shaders
} // namespace mln

View File

@@ -127,6 +127,11 @@ enum {
heatmapTextureUBOCount
};
enum {
idParticleGpuValidationUBO = getEnumValue(drawableReservedUBOCount, layerUBOStartId),
particleGpuValidationUBOCount
};
enum {
idHillshadeEvaluatedPropsUBO = getEnumValue(hillshadeLayerSSBOCount, layerUBOStartId),
hillshadeLayerUBOCount
@@ -279,6 +284,7 @@ static constexpr uint32_t maxUBOCountPerShader = std::max({static_cast<uint32_t>
static_cast<uint32_t>(fillUBOCount),
static_cast<uint32_t>(fillExtrusionUBOCount),
static_cast<uint32_t>(heatmapTextureUBOCount),
static_cast<uint32_t>(particleGpuValidationUBOCount),
static_cast<uint32_t>(heatmapUBOCount),
static_cast<uint32_t>(hillshadePrepareUBOCount),
static_cast<uint32_t>(hillshadeUBOCount),
@@ -343,6 +349,20 @@ enum {
heatmapTextureCount
};
enum {
idParticleGpuValidationPreviousTexture,
idParticleGpuValidationMaskTexture,
idParticleGpuValidationFieldTexture,
idParticleGpuValidationNextFieldTexture,
idParticleGpuValidationStateTexture,
particleGpuValidationTextureCount
};
enum {
idRetainedUnderlayCompositeTexture,
retainedUnderlayCompositeTextureCount
};
enum {
idHillshadeImageTexture,
hillshadeTextureCount
@@ -387,6 +407,8 @@ static constexpr uint32_t maxTextureCountPerShader = std::max({static_cast<uint3
static_cast<uint32_t>(fillTextureCount),
static_cast<uint32_t>(fillExtrusionTextureCount),
static_cast<uint32_t>(heatmapTextureCount),
static_cast<uint32_t>(particleGpuValidationTextureCount),
static_cast<uint32_t>(retainedUnderlayCompositeTextureCount),
static_cast<uint32_t>(hillshadeTextureCount),
static_cast<uint32_t>(colorReliefTextureCount),
static_cast<uint32_t>(lineTextureCount),
@@ -400,6 +422,16 @@ enum {
backgroundVertexAttributeCount
};
enum {
idParticleGpuValidationPosVertexAttribute,
particleGpuValidationVertexAttributeCount
};
enum {
idRetainedUnderlayCompositePosVertexAttribute,
retainedUnderlayCompositeVertexAttributeCount
};
enum {
idCirclePosVertexAttribute,
@@ -581,6 +613,7 @@ enum {
static constexpr uint32_t maxAttributeCountPerShader = std::max({
static_cast<uint32_t>(backgroundVertexAttributeCount),
static_cast<uint32_t>(particleGpuValidationVertexAttributeCount),
static_cast<uint32_t>(circleVertexAttributeCount),
static_cast<uint32_t>(clippingMaskVertexAttributeCount),
static_cast<uint32_t>(collisionVertexAttributeCount),

View File

@@ -24,6 +24,8 @@
#include <mln/shaders/gl/fill_extrusion_pattern_instanced.hpp>
#include <mln/shaders/gl/heatmap.hpp>
#include <mln/shaders/gl/heatmap_texture.hpp>
#include <mln/shaders/gl/particle_gpu_validation.hpp>
#include <mln/shaders/gl/retained_underlay_composite.hpp>
#include <mln/shaders/gl/hillshade_prepare.hpp>
#include <mln/shaders/gl/color_relief.hpp>
#include <mln/shaders/gl/hillshade.hpp>

View File

@@ -30,6 +30,8 @@ enum class BuiltIn {
FillExtrusionPatternInstancedShader,
HeatmapShader,
HeatmapTextureShader,
ParticleGpuValidationShader,
RetainedUnderlayCompositeShader,
HillshadePrepareShader,
ColorReliefShader,
HillshadeShader,

View File

@@ -0,0 +1,36 @@
#pragma once
#include <mln/style/layer.hpp>
namespace mln {
namespace style {
// VALIDATION ONLY: minimal source-less Core layer for 30-01 architecture validation.
class ParticleValidationLayer final : public Layer {
public:
explicit ParticleValidationLayer(const std::string& layerID);
~ParticleValidationLayer() override;
bool isActive() const;
void setActive(bool);
uint8_t getMaskMode() const;
void setMaskMode(uint8_t);
uint8_t getFixtureMode() const;
void setFixtureMode(uint8_t);
class Impl;
const Impl& impl() const;
Mutable<Impl> mutableImpl() const;
private:
explicit ParticleValidationLayer(Immutable<Impl>);
std::optional<conversion::Error> setPropertyInternal(const std::string&, const conversion::Convertible&) override;
StyleProperty getProperty(const std::string&) const override;
Value serialize() const override;
std::unique_ptr<Layer> cloneRef(const std::string& id) const override;
Mutable<Layer::Impl> mutableBaseImpl() const override;
};
} // namespace style
} // namespace mln

View File

@@ -0,0 +1,53 @@
#pragma once
#include <mln/style/layer.hpp>
#include <cstdint>
#include <array>
namespace mln::style {
class RasterParticleLayer final : public Layer {
public:
RasterParticleLayer(const std::string& layerID, const std::string& sourceID);
~RasterParticleLayer() override;
bool isActive() const;
void setActive(bool);
uint8_t getMaskMode() const;
void setMaskMode(uint8_t);
float getTimeMix() const;
void setTimeMix(float);
uint32_t getParticleCount() const;
void setParticleCount(uint32_t);
float getLineWidth() const;
void setLineWidth(float);
float getParticleLength() const;
void setParticleLength(float);
float getParticleOpacity() const;
void setParticleOpacity(float);
float getTrailOpacity() const;
void setTrailOpacity(float);
float getTrailFade() const;
void setTrailFade(float);
float getAdvectionScale() const;
void setAdvectionScale(float);
bool isSpeedColorEnabled() const;
void setSpeedColorEnabled(bool);
std::array<float, 4> getParticleColor() const;
void setParticleColor(std::array<float, 4>);
std::array<float, 4> getTrailColor() const;
void setTrailColor(std::array<float, 4>);
class Impl;
const Impl& impl() const;
Mutable<Impl> mutableImpl() const;
private:
explicit RasterParticleLayer(Immutable<Impl>);
std::optional<conversion::Error> setPropertyInternal(const std::string&, const conversion::Convertible&) override;
StyleProperty getProperty(const std::string&) const override;
Value serialize() const override;
std::unique_ptr<Layer> cloneRef(const std::string&) const override;
Mutable<Layer::Impl> mutableBaseImpl() const override;
};
} // namespace mln::style

View File

@@ -0,0 +1,40 @@
#pragma once
#include <mln/style/layer.hpp>
#include <mln/style/layers/weather_color_stop.hpp>
#include <cstdint>
namespace mln::style {
// The renderer must be told which canonical plane produces color values.
// Defaulting to VectorMagnitude preserves the v1 Wind color behavior.
enum class FieldValueSource : uint8_t { Scalar, VectorMagnitude };
// A field-speed colour display. It deliberately remains distinct from
// RasterParticleLayer: both consume the same canonical field source but own
// separate GPU resources and draw passes.
class WeatherColorLayer final : public Layer {
public:
WeatherColorLayer(const std::string& layerID, const std::string& sourceID);
~WeatherColorLayer() override;
float getOpacity() const;
void setOpacity(float);
const std::vector<WeatherColorStop>& getColorStops() const;
bool setColorStops(std::vector<WeatherColorStop>);
FieldValueSource getFieldValueSource() const;
void setFieldValueSource(FieldValueSource);
class Impl;
const Impl& impl() const;
Mutable<Impl> mutableImpl() const;
private:
explicit WeatherColorLayer(Immutable<Impl>);
std::optional<conversion::Error> setPropertyInternal(const std::string&, const conversion::Convertible&) override;
StyleProperty getProperty(const std::string&) const override;
Value serialize() const override;
std::unique_ptr<Layer> cloneRef(const std::string&) const override;
Mutable<Layer::Impl> mutableBaseImpl() const override;
};
} // namespace mln::style

View File

@@ -0,0 +1,30 @@
#pragma once
#include <array>
#include <cmath>
#include <vector>
namespace mln::style {
// Straight normalized RGBA validation. Particle colours do not depend on ramps.
inline bool validWeatherRGBA(const std::array<float, 4>& color) {
for (float c : color) if (!std::isfinite(c) || c < 0.0f || c > 1.0f) return false;
return true;
}
struct WeatherColorStop {
// A generic field value. Its unit and meaning come from the explicitly
// selected field value source, never from this color ramp.
float value;
std::array<float, 4> color;
bool operator==(const WeatherColorStop& other) const { return value == other.value && color == other.color; }
};
constexpr size_t MaxWeatherColorStops = 8;
inline bool validWeatherColorStops(const std::vector<WeatherColorStop>& stops) {
if (stops.size() < 2 || stops.size() > MaxWeatherColorStops) return false;
for (size_t i = 0; i < stops.size(); ++i) {
if (!std::isfinite(stops[i].value) || !validWeatherRGBA(stops[i].color) ||
(i && stops[i - 1].value >= stops[i].value)) return false;
}
return true;
}
} // namespace mln::style

View File

@@ -0,0 +1,327 @@
#pragma once
#include <mln/particle/field/field_binding.hpp>
#include <mln/particle/field/offline_fixture_particle_adapter.hpp>
#include <mln/particle/ingress/qbin_v2_decoder.hpp>
#include <mln/style/source.hpp>
#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
namespace mln {
class AsyncRequest;
namespace style {
// A Source-owned, preloaded validation candidate. Production runtimes can use
// updateRuntimeFieldWindow() directly with an already decoded snapshot instead.
struct RasterArrayRuntimeFieldDescriptor {
std::string fixtureID;
std::string metadataURL;
std::string fieldURL;
// Validation-only logical time in milliseconds. A production adapter owns
// its own timeline metadata; this keeps fixtures deterministic.
uint64_t timestamp = 0;
};
// A provider-neutral local feed pair. The source only transports the two
// resources; LocalParticleFieldFeed owns metadata decoding and validation.
struct RasterArrayLocalFieldDescriptor {
std::string metadataURL;
std::string fieldURL;
};
enum class RasterArrayPlaybackState : uint8_t {
Ready,
Playing,
Paused,
Waiting,
Ended
};
enum class RasterArrayTargetFps : uint8_t {
Auto = 0,
Fps30 = 30,
Fps60 = 60
};
struct RasterArrayTimelineFrame {
std::shared_ptr<const particle::ParticleFieldSnapshot> t0;
std::shared_ptr<const particle::ParticleFieldSnapshot> t1;
float mix = 0.0f;
size_t t0Index = 0;
size_t t1Index = 0;
uint64_t windowRevision = 0;
uint64_t cadenceRevision = 0;
RasterArrayPlaybackState state = RasterArrayPlaybackState::Ready;
};
struct RasterArrayCadenceAdvance {
RasterArrayTimelineFrame frame;
bool advanced = false;
};
// A backend-neutral cadence gate for renderer-owned visual simulation. Unlike
// the legacy timeline it has no field entries and never advances forecast
// time; Android/iOS may merely request a future MapLibre frame at its deadline.
class RasterArrayRendererCadence final {
public:
bool consumeIfDue(std::chrono::steady_clock::time_point);
bool cadenceDue(std::chrono::steady_clock::time_point) const;
void setTargetFps(RasterArrayTargetFps);
RasterArrayTargetFps targetFps() const;
uint64_t revision() const;
private:
mutable std::mutex mutex;
RasterArrayTargetFps targetFps_ = RasterArrayTargetFps::Auto;
std::chrono::steady_clock::time_point lastTick{};
uint64_t revision_ = 0;
};
// Source-owned playback state. Android only sends low-frequency commands;
// render frames obtain an immutable T0/T1 pair and scalar mix from Core.
class RasterArrayTimelineRuntime {
public:
struct Entry {
std::shared_ptr<const particle::ParticleFieldSnapshot> field;
uint64_t timestamp = 0;
};
explicit RasterArrayTimelineRuntime(std::vector<Entry>);
bool append(Entry);
// Atomically claims one shared cadence deadline and advances wall-time
// playback at most once. Either weather renderer may consume it.
RasterArrayCadenceAdvance advanceIfDue(std::chrono::steady_clock::time_point);
RasterArrayTimelineFrame tick();
bool play();
bool pause();
bool setSpeed(float);
bool seek(size_t);
bool seekTimestamp(uint64_t);
bool step(int);
bool injectGap();
bool injectInvalid();
bool recover();
bool setTargetFps(RasterArrayTargetFps);
RasterArrayTargetFps targetFps() const;
bool cadenceDue(std::chrono::steady_clock::time_point) const;
void markCadenceTick(std::chrono::steady_clock::time_point);
RasterArrayTimelineFrame frame() const;
float playbackSpeed() const;
size_t size() const;
Entry lastEntry() const;
size_t cacheCapacity() const { return cacheCapacity_; }
private:
RasterArrayTimelineFrame frameLocked() const;
bool moveToLocked(size_t);
mutable std::mutex mutex;
std::vector<Entry> entries;
size_t t0 = 0;
float mix = 0.0f;
float speed = 1.0f;
RasterArrayPlaybackState state = RasterArrayPlaybackState::Ready;
bool gap = false;
bool invalid = false;
RasterArrayPlaybackState stateBeforeWait = RasterArrayPlaybackState::Paused;
uint64_t windowRevision = 1;
uint64_t cadenceRevision = 0;
std::chrono::steady_clock::time_point lastTick{};
std::chrono::steady_clock::time_point lastCadenceTick{};
std::chrono::steady_clock::time_point lastCadenceLog{};
uint64_t cadenceTickCount = 0;
RasterArrayTargetFps targetFps_ = RasterArrayTargetFps::Auto;
const size_t cacheCapacity_ = 8;
};
enum class RasterArrayRuntimeFieldUpdateResult : uint8_t {
Applied,
Noop,
RejectedNotLoaded,
RejectedInvalidSnapshot,
RejectedIncompatibleSnapshot,
RejectedNonMonotonicGeneration,
RejectedCandidateNotReady,
RejectedNoCandidate,
};
// Stable low-frequency Host -> Core submission result. Positive values are
// accepted outcomes, while negative values are structured fail-closed errors.
enum class RasterArrayHostFieldSubmitResult : int8_t {
RejectedSourceMode = -1,
RejectedAdapter = -2,
RejectedIncompatibleSnapshot = -3,
RejectedNonMonotonicSnapshot = -4,
RejectedCapacity = -5,
AcceptedPendingInitialWindow = 1,
InitialWindowReady = 2,
AcceptedNextSnapshot = 3,
Duplicate = 4,
};
const char* rasterArrayHostFieldSubmitResultName(RasterArrayHostFieldSubmitResult);
// Stable outcome for the isolated QBIN V4 Host ingress. This is deliberately
// separate from the legacy V3 host-field result above: V4 does not populate a
// renderer-facing T0/T1 timeline.
enum class RasterArrayEncodedFieldSubmitResult : int8_t {
RejectedDecode = -1,
RejectedContract = -2,
RejectedInvalidFieldKey = -3,
RejectedIdentityConflict = -4,
RejectedStaleRevision = -5,
RejectedIncompatible = -6,
RejectedCapacity = -7,
AcceptedInserted = 1,
AcceptedReplacedRevision = 2,
Duplicate = 3,
};
const char* rasterArrayEncodedFieldSubmitResultName(RasterArrayEncodedFieldSubmitResult);
class RasterArraySource final : public Source {
public:
RasterArraySource(std::string id,
std::string fixtureID,
std::string metadataURL,
std::string fieldURL,
std::string nextFixtureID = {},
std::string nextMetadataURL = {},
std::string nextFieldURL = {},
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields = {});
RasterArraySource(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFields);
// Creates an empty Source ingress. The Host submits one canonical Field at
// a time as metadata plus contiguous interleaved U/V bytes.
explicit RasterArraySource(std::string id);
~RasterArraySource() override;
class Impl;
const Impl& impl() const;
void loadDescription(FileSource&) override;
// Advances the authoritative Source time window from T0/T1 to T1/newT1.
// The caller transfers an immutable, already decoded Field snapshot; the
// renderer observes the resulting Source change and owns GPU reuse/upload.
RasterArrayRuntimeFieldUpdateResult updateRuntimeFieldWindow(particle::ParticleFieldSnapshot newT1);
// Thin Core-owned submit point for a producer-supplied initial window.
RasterArrayRuntimeFieldUpdateResult submitInitialRuntimeFieldWindow(particle::ParticleFieldSnapshot t0,
particle::ParticleFieldSnapshot t1);
// Validation convenience: advance using the next Source-owned preloaded
// candidate. Android supplies no T0/T1 state and no GPU resource here.
RasterArrayRuntimeFieldUpdateResult advanceRuntimeFieldWindow();
// Validation convenience: resubmit the current T1 through the formal
// update API. This must return Noop without changing the Source window.
RasterArrayRuntimeFieldUpdateResult repeatRuntimeFieldWindow();
RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText, const std::vector<char>& payload);
// v2 ingress preserves explicit scalar/vector/validity plane presence.
RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText,
const particle::ParticleFieldPayloadBundle& payloads);
// Clears producer-owned Host ingress state while retaining the Source and
// its Style layers for a subsequent T0/T1 submission.
bool clearHostFields();
// V4 QBIN ingress is a parallel, Core-only owner. It retains per-field
// series and a requested forecast clock but never writes V3 T0/T1/MIX or
// renderer snapshots.
RasterArrayEncodedFieldSubmitResult submitEncodedField(
const std::string& fieldKey,
const particle::ingress::EncodedFieldRuntimeMetadata& runtimeMetadata,
const particle::ResolvedFieldProfileContract& contract,
const std::vector<uint8_t>& rawQbinBytes);
void setColorBinding(particle::ColorBinding);
void setParticleBinding(particle::ParticleBinding);
bool setRequestedForecastTime(uint64_t timestampMs);
void setForecastPlaybackState(particle::ForecastPlaybackState);
bool setForecastPlaybackSpeed(float);
bool advanceForecastClock(uint64_t elapsedMs);
// Called once per Map update on the owner thread; render layers only read
// the resulting immutable projection and never advance this clock.
bool tickForecastClock(std::chrono::steady_clock::time_point now);
particle::ColorProjectionResult resolveColorProjection() const;
particle::ParticleProjectionResult resolveParticleProjection() const;
bool clearEncodedField(const std::string& fieldKey);
void clearAllEncodedFields();
// Explicit terminal cleanup for an attached WeatherRuntime. Unlike an ordinary
// V4 window clear, this also drops bindings, forecast clock and V3 host ingress.
// Source and Style layers remain installed for the next runtime generation.
bool resetWeatherRuntimeState();
// Producer-facing timestamp seek. Core alone selects T0/T1 and MIX.
bool seekRuntimeTimelineToTimestamp(uint64_t timestamp);
// Low-frequency JNI commands for the Source-owned validation timeline.
bool controlRuntimeTimeline(int command, float value = 0.0f);
bool setRuntimeTargetFps(uint8_t targetFps);
uint8_t runtimeTargetFps() const;
bool supportsLayerType(const LayerTypeInfo*) const override;
mapbox::base::WeakPtr<Source> makeWeakPtr() override { return weakFactory.makeWeakPtr(); }
protected:
Mutable<Source::Impl> createMutable() const noexcept override;
private:
RasterArrayHostFieldSubmitResult submitDecodedHostField(particle::ParticleFieldSnapshot snapshot);
struct RuntimeFieldCandidate {
RasterArrayRuntimeFieldDescriptor descriptor;
std::unique_ptr<AsyncRequest> metadataRequest;
std::unique_ptr<AsyncRequest> fieldRequest;
std::optional<std::string> metadata;
std::optional<std::vector<char>> fieldBytes;
std::optional<particle::ParticleFieldSnapshot> snapshot;
};
struct LocalFieldCandidate {
RasterArrayLocalFieldDescriptor descriptor;
std::unique_ptr<AsyncRequest> metadataRequest;
std::unique_ptr<AsyncRequest> fieldRequest;
std::optional<std::string> metadata;
std::optional<std::vector<char>> fieldBytes;
std::optional<particle::ParticleFieldSnapshot> snapshot;
};
void tryLoad();
void tryLoadRuntimeField(size_t);
void tryLoadLocalField(size_t);
void tryInitializeTimeline();
// Re-resolve and materialize the independent V4 render view. This never
// writes legacy timeline/field state.
void refreshV4RendererState();
RasterArrayRuntimeFieldUpdateResult rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult,
const std::string& reason) const;
std::unique_ptr<AsyncRequest> metadataRequest;
std::unique_ptr<AsyncRequest> fieldRequest;
std::unique_ptr<AsyncRequest> nextMetadataRequest;
std::unique_ptr<AsyncRequest> nextFieldRequest;
std::optional<std::string> metadata;
std::optional<std::vector<char>> fieldBytes;
std::optional<std::string> nextMetadata;
std::optional<std::vector<char>> nextFieldBytes;
std::vector<RuntimeFieldCandidate> runtimeFields;
std::vector<LocalFieldCandidate> localFields;
std::optional<particle::ParticleFieldSnapshot> pendingHostField;
size_t nextRuntimeField = 0;
uint64_t runtimeUpdateSequence = 0;
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
RasterArrayTargetFps targetFps = RasterArrayTargetFps::Auto;
// Deliberately independent from `timeline`: V4 bindings retain Host intent
// across clear operations and resolve their own field-keyed windows.
particle::EncodedFieldStore encodedFieldStore;
particle::ForecastClock forecastClock;
std::chrono::steady_clock::time_point lastForecastTick{};
// A Source change synchronously invokes Style::onUpdate, which re-enters
// Map::onUpdate. Prevent a second forecast tick during the first notification.
bool forecastTickInProgress = false;
std::optional<particle::ColorBinding> colorBinding;
std::optional<particle::ParticleBinding> particleBinding;
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
};
template <>
inline bool Source::is<RasterArraySource>() const {
return getType() == SourceType::RasterArray;
}
} // namespace style
} // namespace mln

View File

@@ -16,7 +16,8 @@ enum class SourceType : uint8_t {
Annotations,
Image,
CustomVector,
CustomMVTVector
CustomMVTVector,
RasterArray
};
enum class VisibilityType : bool {

View File

@@ -106,7 +106,10 @@ public:
void setDirtyState() override {}
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool);
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size,
gfx::TextureChannelDataType,
bool,
bool) override;
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType) override;

View File

@@ -43,7 +43,10 @@ public:
void reduceMemoryUsage() override;
// Resource creation
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType, bool, bool);
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size,
gfx::TextureChannelDataType,
bool,
bool) override;
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType) override;
std::unique_ptr<gfx::CommandEncoder> createCommandEncoder() override;
gfx::VertexAttributeArrayPtr createVertexAttributeArray() const override;