merge(upgrade): port particle extensions onto official native 13.6.1 (unverified)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
15
include/mln/layermanager/raster_particle_layer_factory.hpp
Normal file
15
include/mln/layermanager/raster_particle_layer_factory.hpp
Normal 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
|
||||
10
include/mln/layermanager/weather_color_layer_factory.hpp
Normal file
10
include/mln/layermanager/weather_color_layer_factory.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
132
include/mln/particle/field/field_binding.hpp
Normal file
132
include/mln/particle/field/field_binding.hpp
Normal 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
|
||||
182
include/mln/particle/field/field_data_adapter.hpp
Normal file
182
include/mln/particle/field/field_data_adapter.hpp
Normal 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
|
||||
56
include/mln/particle/field/field_profile_contract.hpp
Normal file
56
include/mln/particle/field/field_profile_contract.hpp
Normal 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
|
||||
14
include/mln/particle/field/field_profile_contract_json.hpp
Normal file
14
include/mln/particle/field/field_profile_contract_json.hpp
Normal 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
|
||||
41
include/mln/particle/field/field_profile_validator.hpp
Normal file
41
include/mln/particle/field/field_profile_validator.hpp
Normal 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
|
||||
@@ -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
|
||||
177
include/mln/particle/field/field_runtime.hpp
Normal file
177
include/mln/particle/field/field_runtime.hpp
Normal 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
|
||||
18
include/mln/particle/field/local_particle_field_feed.hpp
Normal file
18
include/mln/particle/field/local_particle_field_feed.hpp
Normal 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
|
||||
33
include/mln/particle/field/particle_gpu_math.hpp
Normal file
33
include/mln/particle/field/particle_gpu_math.hpp
Normal 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
|
||||
14
include/mln/particle/ingress/encoded_field_transport.hpp
Normal file
14
include/mln/particle/ingress/encoded_field_transport.hpp
Normal 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
|
||||
79
include/mln/particle/ingress/qbin_v2_decoder.hpp
Normal file
79
include/mln/particle/ingress/qbin_v2_decoder.hpp
Normal 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
|
||||
27
include/mln/particle/ingress/qbin_v3_decoder.hpp
Normal file
27
include/mln/particle/ingress/qbin_v3_decoder.hpp
Normal 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
|
||||
109
include/mln/particle/renderer_debug_counters.hpp
Normal file
109
include/mln/particle/renderer_debug_counters.hpp
Normal 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
|
||||
@@ -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
|
||||
321
include/mln/renderer/render_segment.hpp
Normal file
321
include/mln/renderer/render_segment.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
74
include/mln/renderer/retained_underlay.hpp
Normal file
74
include/mln/renderer/retained_underlay.hpp
Normal 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
|
||||
417
include/mln/shaders/gl/particle_gpu_validation.hpp
Normal file
417
include/mln/shaders/gl/particle_gpu_validation.hpp
Normal 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
|
||||
29
include/mln/shaders/gl/retained_underlay_composite.hpp
Normal file
29
include/mln/shaders/gl/retained_underlay_composite.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
142
include/mln/shaders/mtl/particle_gpu_validation.hpp
Normal file
142
include/mln/shaders/mtl/particle_gpu_validation.hpp
Normal 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
|
||||
34
include/mln/shaders/mtl/retained_underlay_composite.hpp
Normal file
34
include/mln/shaders/mtl/retained_underlay_composite.hpp
Normal 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
|
||||
40
include/mln/shaders/particle_gpu_validation_ubo.hpp
Normal file
40
include/mln/shaders/particle_gpu_validation_ubo.hpp
Normal 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
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -30,6 +30,8 @@ enum class BuiltIn {
|
||||
FillExtrusionPatternInstancedShader,
|
||||
HeatmapShader,
|
||||
HeatmapTextureShader,
|
||||
ParticleGpuValidationShader,
|
||||
RetainedUnderlayCompositeShader,
|
||||
HillshadePrepareShader,
|
||||
ColorReliefShader,
|
||||
HillshadeShader,
|
||||
|
||||
36
include/mln/style/layers/particle_validation_layer.hpp
Normal file
36
include/mln/style/layers/particle_validation_layer.hpp
Normal 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
|
||||
53
include/mln/style/layers/raster_particle_layer.hpp
Normal file
53
include/mln/style/layers/raster_particle_layer.hpp
Normal 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
|
||||
40
include/mln/style/layers/weather_color_layer.hpp
Normal file
40
include/mln/style/layers/weather_color_layer.hpp
Normal 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
|
||||
30
include/mln/style/layers/weather_color_stop.hpp
Normal file
30
include/mln/style/layers/weather_color_stop.hpp
Normal 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
|
||||
327
include/mln/style/sources/raster_array_source.hpp
Normal file
327
include/mln/style/sources/raster_array_source.hpp
Normal 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
|
||||
@@ -16,7 +16,8 @@ enum class SourceType : uint8_t {
|
||||
Annotations,
|
||||
Image,
|
||||
CustomVector,
|
||||
CustomMVTVector
|
||||
CustomMVTVector,
|
||||
RasterArray
|
||||
};
|
||||
|
||||
enum class VisibilityType : bool {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user