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

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

View File

@@ -221,6 +221,12 @@ void AnnotationManager::updateData() {
}
}
bool AnnotationManager::hasPendingChanges() {
CHECK_ANNOTATIONS_ENABLED_AND_RETURN(false);
std::scoped_lock lock(mutex);
return dirty;
}
void AnnotationManager::addTile(AnnotationTile& tile) {
CHECK_ANNOTATIONS_ENABLED_AND_RETURN_NOARG();
std::scoped_lock lock(mutex);

View File

@@ -42,6 +42,7 @@ public:
void onStyleLoaded();
void updateData();
bool hasPendingChanges();
void addTile(AnnotationTile&);
void removeTile(AnnotationTile&);

View File

@@ -436,7 +436,15 @@ std::unique_ptr<gfx::OffscreenTexture> Context::createOffscreenTexture(const Siz
const gfx::TextureChannelDataType type) {
MLN_TRACE_FUNC();
return std::make_unique<gl::OffscreenTexture>(*this, size, type);
return std::make_unique<gl::OffscreenTexture>(*this, size, type, false, false);
}
std::unique_ptr<gfx::OffscreenTexture> Context::createOffscreenTexture(const Size size,
const gfx::TextureChannelDataType type,
const bool depth,
const bool stencil) {
MLN_TRACE_FUNC();
return std::make_unique<gl::OffscreenTexture>(*this, size, type, depth, stencil);
}
std::unique_ptr<gfx::DrawScopeResource> Context::createDrawScopeResource() {
@@ -611,6 +619,24 @@ Framebuffer Context::createFramebuffer(const gfx::Texture2D& color) {
return {color.getSize(), std::move(fbo)};
}
Framebuffer Context::createFramebuffer(
const gfx::Texture2D& color,
const gfx::Renderbuffer<gfx::RenderbufferPixelType::DepthStencil>& depthStencil) {
if (color.getSize() != depthStencil.getSize()) {
throw std::runtime_error("Renderbuffer size mismatch");
}
auto fbo = createFramebuffer();
bindFramebuffer = fbo;
MBGL_CHECK_ERROR(glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
static_cast<const gl::Texture2D&>(color).getTextureID(),
0));
bindDepthStencilRenderbuffer(depthStencil);
checkFramebuffer();
return {color.getSize(), std::move(fbo)};
}
gfx::VertexAttributeArrayPtr Context::createVertexAttributeArray() const {
return std::make_shared<VertexAttributeArrayGL>();
}

View File

@@ -134,6 +134,8 @@ public:
RenderTargetPtr createRenderTarget(const Size size, const gfx::TextureChannelDataType type) override;
Framebuffer createFramebuffer(const gfx::Texture2D& color);
Framebuffer createFramebuffer(const gfx::Texture2D& color,
const gfx::Renderbuffer<gfx::RenderbufferPixelType::DepthStencil>&);
gfx::VertexAttributeArrayPtr createVertexAttributeArray() const override;
@@ -212,6 +214,10 @@ private:
public:
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size, gfx::TextureChannelDataType) override;
std::unique_ptr<gfx::OffscreenTexture> createOffscreenTexture(Size,
gfx::TextureChannelDataType,
bool,
bool) override;
private:
std::unique_ptr<gfx::RenderbufferResource> createRenderbufferResource(gfx::RenderbufferPixelType,

View File

@@ -8,7 +8,11 @@ namespace gl {
class OffscreenTextureResource final : public gl::RenderableResource {
public:
OffscreenTextureResource(gl::Context& context_, const Size size_, const gfx::TextureChannelDataType type_)
OffscreenTextureResource(gl::Context& context_,
const Size size_,
const gfx::TextureChannelDataType type_,
bool depth,
bool stencil)
: context(context_),
size(size_),
type(type_) {
@@ -19,6 +23,10 @@ public:
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear,
.wrapU = gfx::TextureWrapType::Clamp,
.wrapV = gfx::TextureWrapType::Clamp});
if (depth || stencil) {
depthStencil = std::make_unique<gfx::Renderbuffer<gfx::RenderbufferPixelType::DepthStencil>>(
context.createRenderbuffer<gfx::RenderbufferPixelType::DepthStencil>(size));
}
}
~OffscreenTextureResource() noexcept override = default;
@@ -27,7 +35,8 @@ public:
if (!framebuffer) {
assert(texture);
texture->create();
framebuffer = context.createFramebuffer(*texture);
framebuffer = depthStencil ? context.createFramebuffer(*texture, *depthStencil)
: context.createFramebuffer(*texture);
} else {
context.bindFramebuffer = framebuffer->framebuffer;
}
@@ -54,10 +63,16 @@ private:
gfx::Texture2DPtr texture;
const gfx::TextureChannelDataType type;
std::optional<gl::Framebuffer> framebuffer;
std::unique_ptr<gfx::Renderbuffer<gfx::RenderbufferPixelType::DepthStencil>> depthStencil;
};
OffscreenTexture::OffscreenTexture(gl::Context& context, const Size size_, const gfx::TextureChannelDataType type)
: gfx::OffscreenTexture(size_, std::make_unique<OffscreenTextureResource>(context, size_, type)) {}
OffscreenTexture::OffscreenTexture(gl::Context& context,
const Size size_,
const gfx::TextureChannelDataType type,
bool depth,
bool stencil)
: gfx::OffscreenTexture(
size_, std::make_unique<OffscreenTextureResource>(context, size_, type, depth, stencil)) {}
bool OffscreenTexture::isRenderable() {
try {

View File

@@ -12,7 +12,9 @@ class OffscreenTexture final : public gfx::OffscreenTexture {
public:
OffscreenTexture(gl::Context&,
Size size,
gfx::TextureChannelDataType type = gfx::TextureChannelDataType::UnsignedByte);
gfx::TextureChannelDataType type = gfx::TextureChannelDataType::UnsignedByte,
bool depth = false,
bool stencil = false);
bool isRenderable() override;

View File

@@ -135,6 +135,8 @@ void RendererBackend::initShaders(gfx::ShaderRegistry& shaders, const ProgramPar
shaders::BuiltIn::FillExtrusionPatternShader,
shaders::BuiltIn::HeatmapShader,
shaders::BuiltIn::HeatmapTextureShader,
shaders::BuiltIn::ParticleGpuValidationShader,
shaders::BuiltIn::RetainedUnderlayCompositeShader,
shaders::BuiltIn::HillshadePrepareShader,
shaders::BuiltIn::HillshadeShader,
shaders::BuiltIn::ColorReliefShader,

View File

@@ -0,0 +1,24 @@
#include <mln/layermanager/particle_validation_layer_factory.hpp>
#include <mln/renderer/layers/render_particle_validation_layer.hpp>
#include <mln/style/layers/particle_validation_layer.hpp>
#include <mln/style/layers/particle_validation_layer_impl.hpp>
namespace mln {
const style::LayerTypeInfo* ParticleValidationLayerFactory::getTypeInfo() const noexcept {
return style::ParticleValidationLayer::Impl::staticTypeInfo();
}
std::unique_ptr<style::Layer> ParticleValidationLayerFactory::createLayer(
const std::string& id, const style::conversion::Convertible&) noexcept {
return std::make_unique<style::ParticleValidationLayer>(id);
}
std::unique_ptr<RenderLayer> ParticleValidationLayerFactory::createRenderLayer(
Immutable<style::Layer::Impl> impl) noexcept {
assert(impl->getTypeInfo() == getTypeInfo());
return std::make_unique<RenderParticleValidationLayer>(std::move(impl));
}
} // namespace mln

View File

@@ -0,0 +1,25 @@
#include <mln/layermanager/raster_particle_layer_factory.hpp>
#include <mln/renderer/layers/render_raster_particle_layer.hpp>
#include <mln/style/layers/raster_particle_layer.hpp>
#include <mln/style/layers/raster_particle_layer_impl.hpp>
namespace mln {
const style::LayerTypeInfo* RasterParticleLayerFactory::getTypeInfo() const noexcept {
return style::RasterParticleLayer::Impl::staticTypeInfo();
}
std::unique_ptr<style::Layer> RasterParticleLayerFactory::createLayer(
const std::string& id, const style::conversion::Convertible& value) noexcept {
const auto source = getSource(value);
return source ? std::make_unique<style::RasterParticleLayer>(id, *source) : nullptr;
}
std::unique_ptr<RenderLayer> RasterParticleLayerFactory::createRenderLayer(
Immutable<style::Layer::Impl> impl) noexcept {
assert(impl->getTypeInfo() == getTypeInfo());
return std::make_unique<RenderRasterParticleLayer>(std::move(impl));
}
} // namespace mln

View File

@@ -0,0 +1,13 @@
#include <mln/layermanager/weather_color_layer_factory.hpp>
#include <mln/renderer/layers/render_weather_color_layer.hpp>
#include <mln/style/layers/weather_color_layer.hpp>
#include <mln/style/layers/weather_color_layer_impl.hpp>
namespace mln {
const style::LayerTypeInfo* WeatherColorLayerFactory::getTypeInfo() const noexcept { return style::WeatherColorLayer::Impl::staticTypeInfo(); }
std::unique_ptr<style::Layer> WeatherColorLayerFactory::createLayer(const std::string& id, const style::conversion::Convertible& value) noexcept {
const auto source = getSource(value); return source ? std::make_unique<style::WeatherColorLayer>(id, *source) : nullptr;
}
std::unique_ptr<RenderLayer> WeatherColorLayerFactory::createRenderLayer(Immutable<style::Layer::Impl> impl) noexcept {
return std::make_unique<RenderWeatherColorLayer>(std::move(impl));
}
} // namespace mln

View File

@@ -94,6 +94,38 @@ void Map::triggerRepaint() {
impl->onUpdate();
}
void Map::setRetainedUnderlayEnabled(bool enabled) {
if (impl->retainedUnderlayState->setEnabled(enabled)) {
impl->onUpdate();
}
}
bool Map::setContinuousLayerRange(std::string startLayerId, std::string endLayerId) {
if (!impl->retainedUnderlayState->setContinuousLayerRange(std::move(startLayerId), std::move(endLayerId))) {
return false;
}
impl->onUpdate();
return true;
}
bool Map::setRenderSegmentPlan(RenderSegmentPlan plan) {
const bool accepted = impl->retainedUnderlayState->setRenderSegmentPlan(std::move(plan));
impl->onUpdate();
return accepted;
}
RenderSegmentDiagnostics Map::getRenderSegmentDiagnostics() const {
return impl->retainedUnderlayState->renderSegmentSnapshot();
}
bool Map::isRetainedUnderlayEnabled() const {
return impl->retainedUnderlayState->isEnabled();
}
RetainedUnderlayDiagnostics Map::getRetainedUnderlayDiagnostics() const {
return impl->retainedUnderlayState->snapshot();
}
// MARK: - Style
style::Style& Map::getStyle() {

View File

@@ -3,6 +3,7 @@
#include <mln/renderer/update_parameters.hpp>
#include <mln/storage/file_source.hpp>
#include <mln/style/style_impl.hpp>
#include <mln/style/sources/raster_array_source.hpp>
#include <mln/util/exception.hpp>
#include <mln/util/logging.hpp>
#include <mln/util/traits.hpp>
@@ -111,6 +112,14 @@ void Map::Impl::onUpdate() {
}
TimePoint timePoint = mode == MapMode::Continuous ? Clock::now() : Clock::time_point::max();
if (mode == MapMode::Continuous) {
const auto now = std::chrono::steady_clock::now();
for (auto* source : style->getSources()) {
if (source->is<style::RasterArraySource>()) {
static_cast<style::RasterArraySource*>(source)->tickForecastClock(now);
}
}
}
transform.updateTransitions(timePoint);
@@ -134,6 +143,7 @@ void Map::Impl::onUpdate() {
.stillImageRequest = bool(stillImageRequest),
.crossSourceCollisions = crossSourceCollisions,
.fastPFOREnabled = fastPFOREnabled,
.retainedUnderlayState = retainedUnderlayState,
.tileLodMinRadius = tileLodMinRadius,
.tileLodScale = tileLodScale,
.tileLodPitchThreshold = tileLodPitchThreshold,

View File

@@ -13,6 +13,7 @@
#include <mln/style/style.hpp>
#include <mln/util/size.hpp>
#include <mln/tile/tile_operation.hpp>
#include <mln/renderer/retained_underlay_runtime_state.hpp>
#include <numbers>
@@ -101,6 +102,8 @@ public:
std::unique_ptr<style::Style> style;
AnnotationManager annotationManager;
std::shared_ptr<RetainedUnderlayRuntimeState> retainedUnderlayState =
std::make_shared<RetainedUnderlayRuntimeState>();
bool cameraMutated = false;

View File

@@ -5,6 +5,7 @@
#include <mln/mtl/texture2d.hpp>
#include <Metal/Metal.hpp>
#include <stdexcept>
namespace mln {
namespace mtl {
@@ -15,10 +16,12 @@ public:
const Size size_,
const gfx::TextureChannelDataType type_,
bool depth,
[[maybe_unused]] bool stencil)
bool stencil)
: context(context_),
size(size_),
type(type_) {
type(type_),
needsDepth(depth),
needsStencil(stencil) {
assert(!size.isEmpty());
colorTexture = context.createTexture2D();
colorTexture->setSize(size);
@@ -29,7 +32,12 @@ public:
static_cast<Texture2D*>(colorTexture.get())
->setUsage(MTL::TextureUsageShaderRead | MTL::TextureUsageShaderWrite | MTL::TextureUsageRenderTarget);
if (depth) {
#if TARGET_OS_SIMULATOR || defined(__x86_64__)
constexpr bool combinedDepthStencil = true;
#else
constexpr bool combinedDepthStencil = false;
#endif
if (depth || (stencil && combinedDepthStencil)) {
depthTexture = context.createTexture2D();
depthTexture->setSize(size);
depthTexture->setFormat(gfx::TexturePixelType::Depth, gfx::TextureChannelDataType::Float);
@@ -41,7 +49,7 @@ public:
}
// On iOS simulator, the depth target is PixelFormatDepth32Float_Stencil8
#if !TARGET_OS_SIMULATOR
#if !TARGET_OS_SIMULATOR && !defined(__x86_64__)
if (stencil) {
stencilTexture = context.createTexture2D();
stencilTexture->setSize(size);
@@ -63,23 +71,45 @@ public:
assert(context.getBackend().getCommandQueue());
commandBuffer = NS::RetainPtr(context.getBackend().getCommandQueue()->commandBuffer());
colorTexture->create();
auto* colorMetalTexture = static_cast<Texture2D*>(colorTexture.get())->getMetalTexture();
if (!commandBuffer || !colorMetalTexture) throw std::runtime_error("Metal offscreen color attachment unavailable");
renderPassDescriptor = NS::TransferPtr(MTL::RenderPassDescriptor::alloc()->init());
if (!renderPassDescriptor) throw std::runtime_error("Metal offscreen render pass descriptor unavailable");
if (auto* colorTarget = renderPassDescriptor->colorAttachments()->object(0)) {
colorTarget->setTexture(static_cast<Texture2D*>(colorTexture.get())->getMetalTexture());
colorTarget->setTexture(colorMetalTexture);
} else {
throw std::runtime_error("Metal offscreen color attachment missing");
}
if (depthTexture) {
depthTexture->create();
auto* depthMetalTexture = static_cast<Texture2D*>(depthTexture.get())->getMetalTexture();
if (!depthMetalTexture) throw std::runtime_error("Metal offscreen depth attachment unavailable");
if (auto* depthTarget = renderPassDescriptor->depthAttachment()) {
depthTarget->setTexture(static_cast<Texture2D*>(depthTexture.get())->getMetalTexture());
depthTarget->setTexture(depthMetalTexture);
} else {
throw std::runtime_error("Metal offscreen depth attachment missing");
}
}
if (stencilTexture) {
stencilTexture->create();
auto* stencilMetalTexture = static_cast<Texture2D*>(stencilTexture.get())->getMetalTexture();
if (!stencilMetalTexture) throw std::runtime_error("Metal offscreen stencil attachment unavailable");
if (auto* stencilTarget = renderPassDescriptor->stencilAttachment()) {
stencilTarget->setTexture(static_cast<Texture2D*>(stencilTexture.get())->getMetalTexture());
stencilTarget->setTexture(stencilMetalTexture);
} else {
throw std::runtime_error("Metal offscreen stencil attachment missing");
}
} else if (needsStencil && depthTexture) {
// Simulator/x86 Metal uses one Depth32Float_Stencil8 texture for both attachments.
if (auto* stencilTarget = renderPassDescriptor->stencilAttachment()) {
stencilTarget->setTexture(static_cast<Texture2D*>(depthTexture.get())->getMetalTexture());
} else {
throw std::runtime_error("Metal offscreen combined stencil attachment missing");
}
} else if (needsStencil) {
throw std::runtime_error("Metal offscreen stencil attachment unavailable");
}
}
@@ -108,6 +138,25 @@ public:
return colorTexture;
}
bool isRenderable() {
try {
colorTexture->create();
if (!static_cast<Texture2D*>(colorTexture.get())->getMetalTexture()) return false;
if (needsDepth || (needsStencil && !stencilTexture)) {
if (!depthTexture) return false;
depthTexture->create();
if (!static_cast<Texture2D*>(depthTexture.get())->getMetalTexture()) return false;
}
if (stencilTexture) {
stencilTexture->create();
if (!static_cast<Texture2D*>(stencilTexture.get())->getMetalTexture()) return false;
}
return true;
} catch (...) {
return false;
}
}
const RendererBackend& getBackend() const override { return context.getBackend(); }
const MTLCommandBufferPtr& getCommandBuffer() const override { return commandBuffer; }
@@ -125,6 +174,8 @@ private:
Context& context;
const Size size;
const gfx::TextureChannelDataType type;
const bool needsDepth;
const bool needsStencil;
gfx::Texture2DPtr colorTexture;
gfx::Texture2DPtr depthTexture;
gfx::Texture2DPtr stencilTexture;
@@ -137,8 +188,7 @@ OffscreenTexture::OffscreenTexture(
: gfx::OffscreenTexture(size_, std::make_unique<OffscreenTextureResource>(context, size_, type, depth, stencil)) {}
bool OffscreenTexture::isRenderable() {
assert(false);
return true;
return getResource<OffscreenTextureResource>().isRenderable();
}
PremultipliedImage OffscreenTexture::readStillImage() {

View File

@@ -6,10 +6,44 @@
#include <mln/util/logging.hpp>
#include <Metal/Metal.hpp>
#include <stdexcept>
namespace mln {
namespace mtl {
detail::ClearedAttachments detail::applyRenderPassClears(MTL::RenderPassDescriptor& rpd,
const std::optional<Color>& clearColor,
const std::optional<float>& clearDepth,
const std::optional<int32_t>& clearStencil) {
ClearedAttachments result;
if (clearColor) {
if (auto* target = rpd.colorAttachments()->object(0); target && target->texture()) {
const auto& c = *clearColor;
target->setLoadAction(MTL::LoadActionClear);
target->setClearColor(MTL::ClearColor::Make(c.r, c.g, c.b, c.a));
target->setStoreAction(MTL::StoreActionStore);
result.color = true;
}
}
if (clearDepth) {
if (auto* target = rpd.depthAttachment(); target && target->texture()) {
target->setLoadAction(MTL::LoadActionClear);
target->setClearDepth(*clearDepth);
target->setStoreAction(MTL::StoreActionStore);
result.depth = true;
}
}
if (clearStencil) {
if (auto* target = rpd.stencilAttachment(); target && target->texture()) {
target->setLoadAction(MTL::LoadActionClear);
target->setClearStencil(static_cast<uint32_t>(*clearStencil));
target->setStoreAction(MTL::StoreActionStore);
result.stencil = true;
}
}
return result;
}
RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const gfx::RenderPassDescriptor& descriptor)
: descriptor(descriptor),
commandEncoder(commandEncoder_) {
@@ -17,28 +51,24 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const
resource.bind();
if (const auto& cmdBuff = resource.getCommandBuffer()) {
commandBuffer = cmdBuff;
if (auto rpd = resource.getRenderPassDescriptor()) {
if (descriptor.clearColor) {
if (auto copy = NS::TransferPtr(rpd->copy())) {
if (auto* colorTarget = copy->colorAttachments()->object(0)) {
const auto& c = *descriptor.clearColor;
colorTarget->setLoadAction(MTL::LoadActionClear);
colorTarget->setClearColor(MTL::ClearColor::Make(c.r, c.g, c.b, c.a));
rpd = std::move(copy);
}
}
}
encoder = NS::RetainPtr(cmdBuff->renderCommandEncoder(rpd.get()));
if (const auto& buffer = resource.getCommandBuffer()) {
commandBuffer = buffer;
if (const auto& original = resource.getRenderPassDescriptor()) {
auto rpd = NS::TransferPtr(original->copy());
if (!rpd) throw std::runtime_error("Metal render pass descriptor copy failed");
detail::applyRenderPassClears(*rpd.get(), descriptor.clearColor, descriptor.clearDepth, descriptor.clearStencil);
encoder = NS::RetainPtr(buffer->renderCommandEncoder(rpd.get()));
if (!encoder) throw std::runtime_error("Metal render command encoder unavailable");
const auto& texture = rpd->colorAttachments()->object(0)->texture();
const auto* colorTarget = rpd->colorAttachments()->object(0);
if (!colorTarget || !colorTarget->texture()) throw std::runtime_error("Metal color attachment unavailable");
const auto& texture = colorTarget->texture();
width = texture->width();
height = texture->height();
}
}
assert(encoder);
if (!encoder) throw std::runtime_error("Metal render pass unavailable");
// Push the groups already accumulated by the encoder
commandEncoder.visitDebugGroups([this](const auto& group) {
@@ -77,6 +107,8 @@ void RenderPass::resetState() {
for (int i = 0; i < maxBinds; ++i) {
vertexBinds[i].reset();
fragmentBinds[i].reset();
vertexTextureBindings[i].reset();
vertexSamplerStates[i].reset();
fragmentTextureBindings[i].reset();
fragmentSamplerStates[i].reset();
}
@@ -180,6 +212,22 @@ void RenderPass::setStencilReference(int32_t referenceValue) {
}
}
void RenderPass::setVertexTexture(const MTLTexturePtr& texture, int32_t location) {
assert(0 <= location && location < maxBinds);
if (0 <= location && location < maxBinds && vertexTextureBindings[location] != texture) {
vertexTextureBindings[location] = texture;
encoder->setVertexTexture(texture.get(), location);
}
}
void RenderPass::setVertexSamplerState(const MTLSamplerStatePtr& state, int32_t location) {
assert(0 <= location && location < maxBinds);
if (0 <= location && location < maxBinds && vertexSamplerStates[location] != state) {
vertexSamplerStates[location] = state;
encoder->setVertexSamplerState(state.get(), location);
}
}
void RenderPass::setFragmentTexture(const MTLTexturePtr& texture, int32_t location) {
assert(0 <= location && location < maxBinds);
if (0 <= location && location < maxBinds) {

View File

@@ -27,6 +27,8 @@
#include <mln/shaders/mtl/hillshade_prepare.hpp>
#include <mln/shaders/mtl/line.hpp>
#include <mln/shaders/mtl/location_indicator.hpp>
#include <mln/shaders/mtl/particle_gpu_validation.hpp>
#include <mln/shaders/mtl/retained_underlay_composite.hpp>
#include <mln/shaders/mtl/raster.hpp>
#include <mln/shaders/mtl/symbol.hpp>
#include <mln/shaders/mtl/widevector.hpp>
@@ -115,6 +117,8 @@ void RendererBackend::initShaders(gfx::ShaderRegistry& shaders, const ProgramPar
shaders::BuiltIn::LinePatternShader,
shaders::BuiltIn::LocationIndicatorShader,
shaders::BuiltIn::LocationIndicatorTexturedShader,
shaders::BuiltIn::ParticleGpuValidationShader,
shaders::BuiltIn::RetainedUnderlayCompositeShader,
shaders::BuiltIn::RasterShader,
shaders::BuiltIn::SymbolIconShader,
shaders::BuiltIn::SymbolSDFShader,

View File

@@ -257,6 +257,10 @@ void Texture2D::bind(RenderPass& renderPass, int32_t location) {
updateSamplerConfiguration();
}
// Particle advection samples field/state textures in the vertex stage.
// Keep fragment bindings as before and mirror the same binding to vertex.
renderPass.setVertexTexture(metalTexture, location);
renderPass.setVertexSamplerState(metalSamplerState, location);
renderPass.setFragmentTexture(metalTexture, location);
renderPass.setFragmentSamplerState(metalSamplerState, location);

View File

@@ -0,0 +1,151 @@
#include <mln/particle/field/field_binding.hpp>
#include <cmath>
namespace mln::particle {
namespace {
bool validValueClass(FieldValueClass value) { return value <= FieldValueClass::Validity; }
bool validComponent(FieldComponent value) { return value <= FieldComponent::WUp; }
bool validKey(const FieldPlaneKey& key) { return key.semanticID != 0 && validComponent(key.component); }
bool sameSelector(const FieldPlaneKey& a, const FieldPlaneKey& b) {
return a.semanticID == b.semanticID && a.groupID == b.groupID;
}
ProjectionFrameCoordinate coordinate(const DecodedFieldFrame& frame) {
return {frame.frameContentIdentity.fieldID, frame.frameContentIdentity.validTimeMs};
}
const DecodedFieldPlane* findPlane(const DecodedFieldFrame& frame, const FieldPlaneKey& selector) {
for (const auto& plane : frame.planes) if (sameSelector(plane.key, selector)) return &plane;
return nullptr;
}
BindingProjectionError validateColorBinding(const ColorBinding& binding) {
if (binding.fieldKey.empty() || !validKey(binding.plane) || !validValueClass(binding.expectedValueClass) ||
binding.expectedValueClass == FieldValueClass::Validity)
return BindingProjectionError::InvalidBinding;
return BindingProjectionError::Accepted;
}
BindingProjectionError validateParticleBinding(const ParticleBinding& binding) {
if (binding.fieldKey.empty()) return BindingProjectionError::InvalidBinding;
if (binding.mode == ParticleBindingMode::VectorComponents) {
if (!validKey(binding.uPlane) || !validKey(binding.vPlane) || sameSelector(binding.uPlane, binding.vPlane))
return BindingProjectionError::InvalidBinding;
} else if (binding.mode == ParticleBindingMode::DirectionAngle) {
if (!validKey(binding.directionPlane)) return BindingProjectionError::InvalidBinding;
} else {
return BindingProjectionError::InvalidBinding;
}
return BindingProjectionError::Accepted;
}
BindingProjectionError validateWindow(const EncodedFieldStore& store, const std::string& fieldKey,
uint64_t requested, std::optional<FieldSeriesWindow>& window) {
if (!store.find(fieldKey)) return BindingProjectionError::FieldSeriesMissing;
window = store.resolve(fieldKey, requested);
return window ? BindingProjectionError::Accepted : BindingProjectionError::WindowUnavailable;
}
BindingProjectionError validateSelected(const DecodedFieldPlane* t0,
const DecodedFieldPlane* t1,
FieldValueClass valueClass,
uint16_t unit,
FieldComponent component) {
if (!t0 || !t1) return BindingProjectionError::PlaneMissing;
if (t0->valueClass != valueClass || t1->valueClass != valueClass) return BindingProjectionError::PlaneClassMismatch;
if (t0->unitID != unit || t1->unitID != unit) return BindingProjectionError::PlaneUnitMismatch;
if (t0->key.component != component || t1->key.component != component)
return BindingProjectionError::PlaneComponentMismatch;
// FieldSeries::submit rejects incompatible neighbours, and resolve only
// returns a validated window. Resolver has no public incompatible-window
// state, so do not expose an unreachable error code here.
return BindingProjectionError::Accepted;
}
} // namespace
bool ColorBinding::operator==(const ColorBinding& other) const {
return fieldKey == other.fieldKey && sameSelector(plane, other.plane) &&
expectedValueClass == other.expectedValueClass && expectedUnitID == other.expectedUnitID;
}
bool ParticleBinding::operator==(const ParticleBinding& other) const {
if (mode != other.mode || fieldKey != other.fieldKey || expectedUnitID != other.expectedUnitID) return false;
if (mode == ParticleBindingMode::VectorComponents)
return sameSelector(uPlane, other.uPlane) && sameSelector(vPlane, other.vPlane);
if (mode == ParticleBindingMode::DirectionAngle) return sameSelector(directionPlane, other.directionPlane);
return false;
}
bool ProjectionFrameCoordinate::operator==(const ProjectionFrameCoordinate& other) const {
return fieldID == other.fieldID && validTimeMs == other.validTimeMs;
}
bool ColorProjectionContentIdentity::operator==(const ColorProjectionContentIdentity& other) const {
return binding == other.binding && t0Coordinate == other.t0Coordinate && t1Coordinate == other.t1Coordinate &&
t0Plane == other.t0Plane && t1Plane == other.t1Plane && t0Validity == other.t0Validity &&
t1Validity == other.t1Validity;
}
bool ParticleProjectionContentIdentity::operator==(const ParticleProjectionContentIdentity& other) const {
return binding == other.binding && t0Coordinate == other.t0Coordinate && t1Coordinate == other.t1Coordinate &&
t0FirstPlane == other.t0FirstPlane && t1FirstPlane == other.t1FirstPlane &&
t0SecondPlane == other.t0SecondPlane && t1SecondPlane == other.t1SecondPlane &&
t0Validity == other.t0Validity && t1Validity == other.t1Validity;
}
ColorProjectionResult BindingProjectionResolver::resolve(const EncodedFieldStore& store,
const ColorBinding& binding,
const uint64_t requested) {
if (const auto error = validateColorBinding(binding); error != BindingProjectionError::Accepted) return {error, {}};
std::optional<FieldSeriesWindow> window;
if (const auto error = validateWindow(store, binding.fieldKey, requested, window); error != BindingProjectionError::Accepted)
return {error, {}};
const auto* t0 = findPlane(*window->t0, binding.plane);
const auto* t1 = findPlane(*window->t1, binding.plane);
if (const auto error = validateSelected(t0, t1, binding.expectedValueClass, binding.expectedUnitID,
t0 ? t0->key.component : FieldComponent::None);
error != BindingProjectionError::Accepted)
return {error, {}};
ColorProjection projection{{binding, coordinate(*window->t0), coordinate(*window->t1),
t0->contentIdentity, t1->contentIdentity, window->t0->validityContentIdentity,
window->t1->validityContentIdentity},
{requested, window->mix}, window->t0, window->t1, t0->storage, t1->storage,
window->t0->validityBits, window->t1->validityBits};
return {BindingProjectionError::Accepted, std::move(projection)};
}
ParticleProjectionResult BindingProjectionResolver::resolve(const EncodedFieldStore& store,
const ParticleBinding& binding,
const uint64_t requested) {
if (const auto error = validateParticleBinding(binding); error != BindingProjectionError::Accepted) return {error, {}};
std::optional<FieldSeriesWindow> window;
if (const auto error = validateWindow(store, binding.fieldKey, requested, window); error != BindingProjectionError::Accepted)
return {error, {}};
const auto firstSelector = binding.mode == ParticleBindingMode::VectorComponents ? binding.uPlane : binding.directionPlane;
const auto secondSelector = binding.mode == ParticleBindingMode::VectorComponents ? binding.vPlane : FieldPlaneKey{};
const auto* t0First = findPlane(*window->t0, firstSelector);
const auto* t1First = findPlane(*window->t1, firstSelector);
const auto expectedClass = binding.mode == ParticleBindingMode::VectorComponents ? FieldValueClass::VelocityComponent
: FieldValueClass::DirectionAngle;
const auto expectedComponent = binding.mode == ParticleBindingMode::VectorComponents ? FieldComponent::UEast
: FieldComponent::None;
if (const auto error = validateSelected(t0First, t1First, expectedClass, binding.expectedUnitID, expectedComponent);
error != BindingProjectionError::Accepted)
return {error, {}};
const DecodedFieldPlane* t0Second = nullptr;
const DecodedFieldPlane* t1Second = nullptr;
if (binding.mode == ParticleBindingMode::VectorComponents) {
t0Second = findPlane(*window->t0, secondSelector); t1Second = findPlane(*window->t1, secondSelector);
if (const auto error = validateSelected(t0Second, t1Second, FieldValueClass::VelocityComponent,
binding.expectedUnitID, FieldComponent::VNorth);
error != BindingProjectionError::Accepted)
return {error, {}};
if (t0First->key.groupID != t0Second->key.groupID || t1First->key.groupID != t1Second->key.groupID)
return {BindingProjectionError::PlaneGroupMismatch, {}};
}
ParticleProjection projection{{binding, coordinate(*window->t0), coordinate(*window->t1),
t0First->contentIdentity, t1First->contentIdentity,
t0Second ? t0Second->contentIdentity : std::string{},
t1Second ? t1Second->contentIdentity : std::string{},
window->t0->validityContentIdentity, window->t1->validityContentIdentity},
{requested, window->mix}, window->t0, window->t1, t0First->storage, t1First->storage,
t0Second ? t0Second->storage : nullptr, t1Second ? t1Second->storage : nullptr,
window->t0->validityBits, window->t1->validityBits};
return {BindingProjectionError::Accepted, std::move(projection)};
}
} // namespace mln::particle

View File

@@ -0,0 +1,117 @@
#include <mln/particle/field/field_data_adapter.hpp>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <utility>
namespace mln::particle {
namespace {
ParticleFieldAdapterResult reject(ParticleFieldAdapterError error, std::string message) {
return {.snapshot = std::nullopt, .error = error, .message = std::move(message)};
}
bool validCommonDescriptor(const ParticleFieldDescriptor& d) {
return !d.fieldID.empty() && d.generation && d.validTime && d.width && d.height && d.west < d.east && d.south < d.north;
}
bool finitePlane(const std::vector<float>& values) {
return std::all_of(values.begin(), values.end(), [](float value) { return std::isfinite(value); });
}
bool isNoData(const std::optional<float>& noData, float value) { return noData && value == *noData; }
} // namespace
const char* particleFieldAdapterErrorName(ParticleFieldAdapterError error) {
switch (error) {
case ParticleFieldAdapterError::None: return "NONE";
case ParticleFieldAdapterError::InvalidDescriptor: return "INVALID_DESCRIPTOR";
case ParticleFieldAdapterError::UnsupportedFormat: return "UNSUPPORTED_FORMAT";
case ParticleFieldAdapterError::DimensionMismatch: return "DIMENSION_MISMATCH";
case ParticleFieldAdapterError::PayloadSizeMismatch: return "PAYLOAD_SIZE_MISMATCH";
case ParticleFieldAdapterError::InvalidTimestampOrIdentity: return "INVALID_TIMESTAMP_OR_IDENTITY";
case ParticleFieldAdapterError::NonFiniteData: return "NON_FINITE_DATA";
case ParticleFieldAdapterError::DecodeValidationFailure: return "DECODE_VALIDATION_FAILURE";
}
return "UNKNOWN";
}
bool ParticleFieldSnapshot::isValid() const {
const auto cells = static_cast<size_t>(width) * height;
if (fieldID.empty() || !generation || !validTime || !width || !height || !(west < east) || !(south < north) ||
valid.size() != cells || (!hasScalar() && !hasVector())) return false;
if (hasScalar() && scalar.size() != cells) return false;
if (hasVector() && (vector.size() != cells * 2 || !vectorSemantics)) return false;
return (!hasVector() ? !vectorSemantics : true) && finitePlane(scalar) && finitePlane(vector);
}
bool ParticleFieldSnapshot::isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot& other) const {
return width == other.width && height == other.height && west == other.west && south == other.south && east == other.east &&
north == other.north && hasScalar() == other.hasScalar() && hasVector() == other.hasVector() &&
vectorSemantics == other.vectorSemantics;
}
ParticleFieldAdapterResult ParticleFieldDataAdapter::decode(const ParticleFieldDescriptor& d,
const ParticleFieldPayloadBundle& payloads) {
if (!validCommonDescriptor(d)) return reject(ParticleFieldAdapterError::InvalidTimestampOrIdentity, "field identity, time or grid is invalid");
const bool scalar = d.scalar.has_value(), vector = d.vector.has_value();
if ((!scalar && !vector) || scalar != payloads.scalar.has_value() || vector != payloads.vector.has_value() || !d.validity || !payloads.validity)
return reject(ParticleFieldAdapterError::InvalidDescriptor, "v2 channel declaration and payload presence differ");
if (vector != d.vectorSemantics.has_value())
return reject(ParticleFieldAdapterError::InvalidDescriptor, "vector semantics must be present exactly with vector");
const uint64_t cells = static_cast<uint64_t>(d.width) * d.height;
const auto planeMatches = [](const ParticleFieldPlaneDescriptor& p, const std::optional<std::vector<char>>& bytes,
uint64_t expected, const char* encoding, const char* layout) {
return p.byteLength == expected && bytes && bytes->size() == expected && p.encoding == encoding &&
(!layout[0] || p.layout == layout) && (!p.noData || std::isfinite(*p.noData));
};
if (scalar && !planeMatches(*d.scalar, payloads.scalar, cells * sizeof(float), "float32-le", ""))
return reject(ParticleFieldAdapterError::PayloadSizeMismatch, "scalar plane length or encoding is invalid");
if (vector && !planeMatches(*d.vector, payloads.vector, cells * 2 * sizeof(float), "float32-le", "interleaved-uv"))
return reject(ParticleFieldAdapterError::PayloadSizeMismatch, "vector plane length or encoding is invalid");
if (d.validity->byteLength != cells || payloads.validity->size() != cells || d.validity->encoding != "uint8")
return reject(ParticleFieldAdapterError::PayloadSizeMismatch, "validity plane length or encoding is invalid");
ParticleFieldSnapshot field;
field.fieldID = d.fieldID; field.fieldType = d.fieldType; field.displayDomain = d.displayDomain; field.validTime = d.validTime;
field.width = d.width; field.height = d.height; field.west = d.west; field.south = d.south; field.east = d.east; field.north = d.north;
field.dataVersion = d.dataVersion; field.generation = d.generation; field.fieldSha256 = d.fieldSha256; field.vectorSemantics = d.vectorSemantics;
field.gpuEncodingIdentity = d.encoding + ":" + d.layout;
if (d.vector) field.gpuEncodingIdentity += ":" + d.vector->encoding + ":" + d.vector->layout;
if (d.validity) field.gpuEncodingIdentity += ":" + d.validity->encoding + ":" + d.validity->layout;
field.valid.assign(payloads.validity->begin(), payloads.validity->end());
if (scalar) { field.scalar.resize(cells); std::memcpy(field.scalar.data(), payloads.scalar->data(), payloads.scalar->size()); }
if (vector) { field.vector.resize(cells * 2); std::memcpy(field.vector.data(), payloads.vector->data(), payloads.vector->size()); }
if (!finitePlane(field.scalar) || !finitePlane(field.vector)) return reject(ParticleFieldAdapterError::NonFiniteData, "field contains non-finite data");
for (size_t cell = 0; cell < field.valid.size(); ++cell) {
if (field.valid[cell] > 1) return reject(ParticleFieldAdapterError::DecodeValidationFailure, "validity values must be 0 or 1");
if (field.valid[cell]) {
if (scalar && isNoData(d.scalar->noData, field.scalar[cell])) return reject(ParticleFieldAdapterError::DecodeValidationFailure, "valid scalar cell contains noData");
if (vector && (isNoData(d.vector->noData, field.vector[cell * 2]) || isNoData(d.vector->noData, field.vector[cell * 2 + 1])))
return reject(ParticleFieldAdapterError::DecodeValidationFailure, "valid vector cell contains noData");
}
}
if (!field.isValid()) return reject(ParticleFieldAdapterError::DecodeValidationFailure, "decoded field is invalid");
return {.snapshot = std::move(field), .error = ParticleFieldAdapterError::None, .message = {}};
}
ParticleFieldAdapterResult ParticleFieldDataAdapter::decode(const ParticleFieldDescriptor& d, const std::vector<char>& bytes, uint64_t generation) {
if (d.layout != "interleaved-uv" || d.encoding != "float32-le")
return reject(ParticleFieldAdapterError::UnsupportedFormat, "legacy v1 only supports interleaved-uv float32-le");
if (!d.generation || !generation || d.generation != generation || (d.stride && d.stride != 2 * sizeof(float)) || !std::isfinite(d.noData))
return reject(ParticleFieldAdapterError::InvalidDescriptor, "legacy v1 descriptor is invalid");
ParticleFieldDescriptor normalized = d;
normalized.vector = ParticleFieldPlaneDescriptor{.byteLength = d.byteLength, .encoding = d.encoding, .layout = d.layout, .noData = d.noData};
normalized.vectorSemantics = VectorSemantics::Velocity;
normalized.validity = ParticleFieldPlaneDescriptor{.byteLength = static_cast<uint64_t>(d.width) * d.height,
.encoding = "uint8", .layout = {}, .noData = std::nullopt};
std::vector<uint8_t> valid(static_cast<size_t>(normalized.validity->byteLength));
if (bytes.size() == static_cast<size_t>(d.width) * d.height * 2 * sizeof(float)) {
for (size_t cell = 0; cell < valid.size(); ++cell) {
float pair[2]; std::memcpy(pair, bytes.data() + cell * sizeof(pair), sizeof(pair));
if (!std::isfinite(pair[0]) || !std::isfinite(pair[1])) return reject(ParticleFieldAdapterError::NonFiniteData, "legacy v1 field contains non-finite data");
valid[cell] = (pair[0] == d.noData || pair[1] == d.noData) ? 0 : 1;
}
}
return decode(normalized, ParticleFieldPayloadBundle{.scalar = std::nullopt,
.vector = bytes,
.validity = std::vector<char>(valid.begin(), valid.end())});
}
} // namespace mln::particle

View File

@@ -0,0 +1,97 @@
#include <mln/particle/field/field_profile_contract_json.hpp>
#include <rapidjson/document.h>
#include <cstdint>
#include <limits>
namespace mln::particle {
namespace {
constexpr std::size_t MaxContractBytes = 256u * 1024u;
constexpr std::size_t MaxProfileIDBytes = 64u;
constexpr std::size_t MaxRequiredPlanes = 64u;
bool uintMember(const rapidjson::Value& value, const char* name, uint64_t maximum, uint64_t& out) {
const auto member = value.FindMember(name);
if (member == value.MemberEnd() || !member->value.IsUint64() || member->value.GetUint64() > maximum) return false;
out = member->value.GetUint64();
return true;
}
bool boolMember(const rapidjson::Value& value, const char* name, bool& out) {
const auto member = value.FindMember(name);
if (member == value.MemberEnd() || !member->value.IsBool()) return false;
out = member->value.GetBool();
return true;
}
bool stringMember(const rapidjson::Value& value, const char* name, std::string& out) {
const auto member = value.FindMember(name);
if (member == value.MemberEnd() || !member->value.IsString()) return false;
out.assign(member->value.GetString(), member->value.GetStringLength());
return true;
}
std::optional<FieldValueClass> valueClass(const std::string& value) {
if (value == "SCALAR") return FieldValueClass::Scalar;
if (value == "VELOCITY_COMPONENT") return FieldValueClass::VelocityComponent;
if (value == "DIRECTION_ANGLE") return FieldValueClass::DirectionAngle;
if (value == "VALIDITY") return FieldValueClass::Validity;
return std::nullopt;
}
std::optional<FieldComponent> component(const std::string& value) {
if (value == "NONE") return FieldComponent::None;
if (value == "U_EAST") return FieldComponent::UEast;
if (value == "V_NORTH") return FieldComponent::VNorth;
if (value == "W_UP") return FieldComponent::WUp;
return std::nullopt;
}
std::optional<FieldTimeRelation> timeRelation(const std::string& value) {
if (value == "ANY") return FieldTimeRelation::Any;
if (value == "VALID_GTE_REFERENCE") return FieldTimeRelation::ValidGteReference;
if (value == "VALID_EQ_REFERENCE") return FieldTimeRelation::ValidEqReference;
return std::nullopt;
}
} // namespace
std::optional<ResolvedFieldProfileContract> parseResolvedFieldProfileContractJson(const std::string& json) {
if (json.empty() || json.size() > MaxContractBytes) return std::nullopt;
rapidjson::Document document;
document.Parse(json.data(), json.size());
if (document.HasParseError() || !document.IsObject()) return std::nullopt;
uint64_t schema = 0, profileVersion = 0;
std::string profileID, relation;
if (!uintMember(document, "contractSchemaVersion", 1, schema) || schema != 1 ||
!uintMember(document, "profileVersion", std::numeric_limits<uint32_t>::max(), profileVersion) ||
!stringMember(document, "profileID", profileID) || profileID.empty() || profileID.size() > MaxProfileIDBytes ||
!stringMember(document, "timeRelation", relation)) return std::nullopt;
const auto planes = document.FindMember("requiredPlanes");
if (planes == document.MemberEnd() || !planes->value.IsArray() || planes->value.Empty() ||
planes->value.Size() > MaxRequiredPlanes) return std::nullopt;
ResolvedFieldProfileContract result;
result.contractSchemaVersion = static_cast<uint16_t>(schema);
result.profileID = std::move(profileID);
result.profileVersion = static_cast<uint32_t>(profileVersion);
if (!boolMember(document, "allowAdditionalPlanes", result.allowAdditionalPlanes) ||
!boolMember(document, "sharedValidityRequired", result.sharedValidityRequired)) return std::nullopt;
const auto parsedRelation = timeRelation(relation);
if (!parsedRelation) return std::nullopt;
result.timeRelation = *parsedRelation;
result.requiredPlanes.reserve(planes->value.Size());
for (const auto& plane : planes->value.GetArray()) {
uint64_t semanticID = 0, unitID = 0, groupID = 0;
bool requiredFlag = false;
std::string className, componentName;
if (!plane.IsObject() || !uintMember(plane, "semanticID", UINT16_MAX, semanticID) || semanticID == 0 ||
!uintMember(plane, "unitID", UINT16_MAX, unitID) || !uintMember(plane, "groupID", UINT16_MAX, groupID) ||
!boolMember(plane, "requiredFlag", requiredFlag) || !requiredFlag ||
!stringMember(plane, "valueClass", className) || !stringMember(plane, "component", componentName)) return std::nullopt;
const auto parsedClass = valueClass(className);
const auto parsedComponent = component(componentName);
if (!parsedClass || !parsedComponent) return std::nullopt;
result.requiredPlanes.push_back({static_cast<uint16_t>(semanticID), *parsedClass, static_cast<uint16_t>(unitID),
static_cast<uint16_t>(groupID), *parsedComponent, requiredFlag});
}
return result.isStructurallyValid() ? std::optional<ResolvedFieldProfileContract>(std::move(result)) : std::nullopt;
}
} // namespace mln::particle

View File

@@ -0,0 +1,252 @@
#include <mln/particle/field/field_profile_validator.hpp>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iterator>
#include <limits>
#include <map>
namespace mln::particle {
namespace {
using PlaneRef = const DecodedFieldPlane*;
// These are protocol-reserved Core semantic contracts, deliberately separate
// from product/profile resolution. Unknown semantic IDs remain extensible.
struct CoreSemanticContract {
uint16_t semanticID;
FieldValueClass valueClass;
uint16_t unitID;
FieldComponent component;
bool groupMustBeZero;
};
constexpr CoreSemanticContract CoreSemanticContracts[] = {
{1, FieldValueClass::VelocityComponent, FieldUnitMetersPerSecond, FieldComponent::UEast, false},
{2, FieldValueClass::VelocityComponent, FieldUnitMetersPerSecond, FieldComponent::VNorth, false},
{3, FieldValueClass::Validity, FieldUnitNone, FieldComponent::None, true},
{10, FieldValueClass::DirectionAngle, FieldUnitDegrees, FieldComponent::None, true},
{11, FieldValueClass::DirectionAngle, FieldUnitDegrees, FieldComponent::None, true},
};
const CoreSemanticContract* coreSemanticContract(const uint16_t semanticID) {
const auto found = std::find_if(std::begin(CoreSemanticContracts), std::end(CoreSemanticContracts),
[semanticID](const CoreSemanticContract& contract) {
return contract.semanticID == semanticID;
});
return found == std::end(CoreSemanticContracts) ? nullptr : &*found;
}
bool matchesCoreSemanticContract(const uint16_t semanticID,
const FieldValueClass valueClass,
const uint16_t unitID,
const uint16_t groupID,
const FieldComponent component) {
const auto* contract = coreSemanticContract(semanticID);
return !contract ||
(contract->valueClass == valueClass && contract->unitID == unitID && contract->component == component &&
(!contract->groupMustBeZero || groupID == 0));
}
bool sameKey(const FieldPlaneRequirement& requirement, const DecodedFieldPlane& plane) {
return requirement.semanticID == plane.key.semanticID && requirement.groupID == plane.key.groupID;
}
bool matches(const FieldPlaneRequirement& requirement, const DecodedFieldPlane& plane) {
return sameKey(requirement, plane) && requirement.valueClass == plane.valueClass &&
requirement.unitID == plane.unitID && requirement.component == plane.key.component &&
requirement.requiredFlag == plane.requiredFlag;
}
FieldProfileValidationResult result(const FieldProfileValidationCode code, const std::string& detail) {
return {code, detail};
}
bool isNumeric(const FieldDType dtype) {
return dtype != FieldDType::Bitset;
}
bool directionScaleIsCanonical(const float scale) {
constexpr float canonical = 360.0f / 65536.0f;
constexpr float tolerance = 4.0f * std::numeric_limits<float>::epsilon();
return std::fabs(scale - canonical) <= tolerance;
}
uint32_t readU32LE(const uint8_t* data) {
return static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
}
bool validNumericSamples(const DecodedFieldFrame& frame, const DecodedFieldPlane& plane) {
const uint64_t cells = static_cast<uint64_t>(frame.grid.width) * frame.grid.height;
size_t bytesPerSample = 0;
switch (plane.dtype) {
case FieldDType::Int16:
case FieldDType::UInt16:
bytesPerSample = 2;
break;
case FieldDType::UInt8:
case FieldDType::Int8:
bytesPerSample = 1;
break;
case FieldDType::Float32:
bytesPerSample = 4;
break;
case FieldDType::Bitset:
return false;
}
if (cells > std::numeric_limits<size_t>::max() / bytesPerSample ||
plane.storage->size() != static_cast<size_t>(cells) * bytesPerSample) return false;
for (uint64_t cell = 0; cell < cells; ++cell) {
// The shared validity mask is the protocol's no-data indicator. Invalid
// cells are deliberately ignored; their zero placeholder is not a field
// value and must not reject an otherwise usable frame.
if (((*frame.validityBits)[static_cast<size_t>(cell >> 3)] & (1u << (cell & 7u))) == 0) continue;
const uint8_t* sample = plane.storage->data();
double raw = 0.0;
switch (plane.dtype) {
case FieldDType::Int16: {
const auto offset = static_cast<size_t>(cell) * 2;
const uint16_t bits = static_cast<uint16_t>(sample[offset]) |
(static_cast<uint16_t>(sample[offset + 1]) << 8);
raw = static_cast<int16_t>(bits);
break;
}
case FieldDType::UInt16: {
const auto offset = static_cast<size_t>(cell) * 2;
raw = static_cast<uint16_t>(sample[offset]) |
(static_cast<uint16_t>(sample[offset + 1]) << 8);
break;
}
case FieldDType::UInt8:
raw = sample[static_cast<size_t>(cell)];
break;
case FieldDType::Int8:
raw = static_cast<int8_t>(sample[static_cast<size_t>(cell)]);
break;
case FieldDType::Float32: {
float value = 0.0f;
const uint32_t bits = readU32LE(sample + static_cast<size_t>(cell) * 4);
std::memcpy(&value, &bits, sizeof(value));
if (!std::isfinite(value)) return false;
raw = value;
break;
}
case FieldDType::Bitset:
return false;
}
if (!std::isfinite(raw)) return false;
const double scaledValue = raw * static_cast<double>(plane.scale);
if (!std::isfinite(scaledValue) ||
std::fabs(scaledValue) > static_cast<double>(std::numeric_limits<float>::max())) return false;
const float scaled = static_cast<float>(scaledValue);
const double decodedValue = static_cast<double>(scaled) + static_cast<double>(plane.offset);
if (!std::isfinite(decodedValue) ||
std::fabs(decodedValue) > static_cast<double>(std::numeric_limits<float>::max()) ||
!std::isfinite(static_cast<float>(decodedValue))) return false;
}
return true;
}
} // namespace
FieldProfileValidationResult FieldProfileValidator::validate(const ResolvedFieldProfileContract& contract,
const DecodedFieldFrame& frame) {
if (contract.contractSchemaVersion != 1)
return result(FieldProfileValidationCode::UnsupportedContractSchema, "contractSchemaVersion");
if (!contract.isStructurallyValid()) return result(FieldProfileValidationCode::InvalidContract, "contract");
if (!frame.isStructurallyValid()) return result(FieldProfileValidationCode::InvalidFrame, "frame");
for (const auto& required : contract.requiredPlanes) {
if (!matchesCoreSemanticContract(required.semanticID, required.valueClass, required.unitID, required.groupID,
required.component))
return result(FieldProfileValidationCode::SemanticContractMismatch, "required Core semantic descriptor");
}
if (!contract.sharedValidityRequired)
return result(FieldProfileValidationCode::UnsupportedSharedValidityMode, "sharedValidityRequired=false");
if ((contract.timeRelation == FieldTimeRelation::ValidGteReference &&
frame.frameContentIdentity.validTimeMs < frame.referenceTimeMs) ||
(contract.timeRelation == FieldTimeRelation::ValidEqReference &&
frame.frameContentIdentity.validTimeMs != frame.referenceTimeMs))
return result(FieldProfileValidationCode::TimeRelationMismatch, "reference/valid time");
std::vector<PlaneRef> actual;
actual.reserve(frame.planes.size() + 1);
actual.push_back(&frame.validityPlane);
for (const auto& plane : frame.planes) actual.push_back(&plane);
// Inspect actual reserved Core semantics before profile matching. Otherwise
// an invalid known semantic can be hidden behind a missing/mismatched
// profile descriptor instead of reporting its protocol violation.
for (const auto plane : actual) {
if (!matchesCoreSemanticContract(plane->key.semanticID, plane->valueClass, plane->unitID,
plane->key.groupID, plane->key.component))
return result(FieldProfileValidationCode::SemanticContractMismatch, "actual Core semantic descriptor");
}
for (const auto& required : contract.requiredPlanes) {
const auto same = std::find_if(actual.begin(), actual.end(), [&required](const PlaneRef plane) {
return sameKey(required, *plane);
});
if (same == actual.end()) return result(FieldProfileValidationCode::MissingRequiredPlane, "required plane absent");
if (!matches(required, **same))
return result(FieldProfileValidationCode::PlaneDescriptorMismatch, "required plane descriptor");
}
for (const auto plane : actual) {
const auto declared = std::find_if(contract.requiredPlanes.begin(), contract.requiredPlanes.end(),
[plane](const FieldPlaneRequirement& requirement) {
return matches(requirement, *plane);
});
if (plane->requiredFlag && declared == contract.requiredPlanes.end())
return result(FieldProfileValidationCode::UndeclaredRequiredPlane, "actual REQUIRED plane");
if (!plane->requiredFlag && !contract.allowAdditionalPlanes && declared == contract.requiredPlanes.end())
return result(FieldProfileValidationCode::AdditionalPlaneNotAllowed, "additional plane");
if (plane->valueClass == FieldValueClass::Validity) {
if (plane->dtype != FieldDType::Bitset || plane->unitID != FieldUnitNone || plane->key.groupID != 0 ||
plane->key.component != FieldComponent::None)
return result(FieldProfileValidationCode::InvalidCoreUnit, "validity descriptor");
} else if (plane->valueClass == FieldValueClass::DirectionAngle) {
if (plane->unitID != FieldUnitDegrees || plane->key.component != FieldComponent::None || plane->key.groupID != 0 ||
plane->dtype != FieldDType::UInt16 || plane->offset != 0.0f || !directionScaleIsCanonical(plane->scale))
return result(FieldProfileValidationCode::InvalidCoreUnit, "direction circular quantization descriptor");
} else if (plane->valueClass == FieldValueClass::VelocityComponent) {
if (plane->unitID != FieldUnitMetersPerSecond || !isNumeric(plane->dtype))
return result(FieldProfileValidationCode::InvalidCoreUnit, "velocity unit/dtype");
} else if (plane->valueClass == FieldValueClass::Scalar && !isNumeric(plane->dtype)) {
return result(FieldProfileValidationCode::InvalidFrame, "scalar dtype");
}
}
std::map<uint16_t, std::vector<PlaneRef>> velocityGroups;
for (const auto plane : actual) {
if (plane->valueClass == FieldValueClass::VelocityComponent) velocityGroups[plane->key.groupID].push_back(plane);
}
for (const auto& entry : velocityGroups) {
if (entry.first == 0) return result(FieldProfileValidationCode::InvalidVelocityGroup, "velocity group zero");
PlaneRef u = nullptr;
PlaneRef v = nullptr;
for (const auto plane : entry.second) {
if (plane->key.component == FieldComponent::UEast) {
if (u) return result(FieldProfileValidationCode::InvalidVelocityGroup, "duplicate U");
u = plane;
} else if (plane->key.component == FieldComponent::VNorth) {
if (v) return result(FieldProfileValidationCode::InvalidVelocityGroup, "duplicate V");
v = plane;
}
}
if (!u || !v || u->count != v->count || u->unitID != v->unitID)
return result(FieldProfileValidationCode::InvalidVelocityGroup, "incomplete velocity group");
}
for (const auto plane : actual) {
if (plane->valueClass != FieldValueClass::Validity && !validNumericSamples(frame, *plane))
return result(FieldProfileValidationCode::InvalidFrame, "non-finite valid-cell numeric value");
}
return result(FieldProfileValidationCode::Accepted, "accepted");
}
} // namespace mln::particle

View File

@@ -0,0 +1,241 @@
#include <mln/particle/field/field_projection_materialization.hpp>
#include <cmath>
#include <cstring>
#include <limits>
namespace mln::particle {
namespace {
bool sameSelector(const FieldPlaneKey& a, const FieldPlaneKey& b) {
return a.semanticID == b.semanticID && a.groupID == b.groupID;
}
const DecodedFieldPlane* findPlane(const DecodedFieldFrame& frame, const FieldPlaneKey& key) {
for (const auto& plane : frame.planes)
if (sameSelector(plane.key, key)) return &plane;
return nullptr;
}
size_t byteWidth(const FieldDType type) {
switch (type) {
case FieldDType::Int16:
case FieldDType::UInt16:
return 2;
case FieldDType::UInt8:
case FieldDType::Int8:
return 1;
case FieldDType::Float32:
return 4;
default:
return 0;
}
}
uint16_t readU16LE(const uint8_t* data) {
return static_cast<uint16_t>(data[0]) | (static_cast<uint16_t>(data[1]) << 8);
}
uint32_t readU32LE(const uint8_t* data) {
return static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
}
bool decodeRaw(const DecodedFieldPlane& plane, const size_t index, float& value) {
if (!plane.storage || !std::isfinite(plane.scale) || !std::isfinite(plane.offset) || index >= plane.count)
return false;
const auto width = byteWidth(plane.dtype);
if (!width || plane.count == 0 || plane.storage->size() != static_cast<size_t>(plane.count) * width) return false;
const uint8_t* data = plane.storage->data() + index * width;
float raw = 0.0f;
switch (plane.dtype) {
case FieldDType::Int16:
raw = static_cast<int16_t>(readU16LE(data));
break;
case FieldDType::UInt16:
raw = readU16LE(data);
break;
case FieldDType::UInt8:
raw = *data;
break;
case FieldDType::Int8:
raw = static_cast<int8_t>(*data);
break;
case FieldDType::Float32: {
const uint32_t bits = readU32LE(data);
std::memcpy(&raw, &bits, sizeof(raw));
break;
}
default:
return false;
}
value = raw * plane.scale + plane.offset;
return std::isfinite(raw) && std::isfinite(value);
}
bool materializeValidity(const DecodedFieldFrame& frame, std::vector<uint8_t>& output) {
const auto cells = static_cast<size_t>(frame.grid.width) * frame.grid.height;
if (!frame.validityBits || frame.validityBits->size() != (cells + 7) / 8) return false;
output.resize(cells);
// QBIN V2 bitset order is least-significant-bit first within each byte.
for (size_t cell = 0; cell < cells; ++cell) output[cell] = ((*frame.validityBits)[cell / 8] >> (cell % 8)) & 1u;
return true;
}
ParticleFieldSnapshot baseSnapshot(const DecodedFieldFrame& frame, const std::string& identity) {
ParticleFieldSnapshot snapshot;
snapshot.fieldID = frame.frameContentIdentity.fieldID + ":" + identity;
snapshot.fieldType = "qbin-v4";
snapshot.displayDomain = "GLOBAL";
snapshot.validTime = frame.frameContentIdentity.validTimeMs;
snapshot.width = frame.grid.width;
snapshot.height = frame.grid.height;
snapshot.west = frame.grid.west;
snapshot.south = frame.grid.south;
snapshot.east = frame.grid.east;
snapshot.north = frame.grid.north;
snapshot.dataVersion = frame.frameContentIdentity.dataVersion;
snapshot.generation = frame.frameContentIdentity.generation;
snapshot.fieldSha256 = identity;
// Include all plane interpretation metadata, including validity and grid
// layout. Decoded floats alone cannot prove that a new revision has the
// same source semantics.
snapshot.gpuEncodingIdentity = std::to_string(frame.grid.layout) + ":" +
std::to_string(frame.grid.projection) + ":" +
std::to_string(frame.grid.sampleOrder) + ":" +
std::to_string(frame.grid.gridRegistration);
const auto appendPlane = [&snapshot](const DecodedFieldPlane& plane) {
uint32_t scaleBits = 0, offsetBits = 0;
std::memcpy(&scaleBits, &plane.scale, sizeof(scaleBits));
std::memcpy(&offsetBits, &plane.offset, sizeof(offsetBits));
snapshot.gpuEncodingIdentity += ":" + std::to_string(plane.key.semanticID) + ":" +
std::to_string(plane.key.groupID) + ":" +
std::to_string(static_cast<int>(plane.key.component)) + ":" +
std::to_string(static_cast<int>(plane.dtype)) + ":" +
std::to_string(static_cast<int>(plane.valueClass)) + ":" +
std::to_string(plane.unitID) + ":" +
std::to_string(scaleBits) + ":" + std::to_string(offsetBits);
};
appendPlane(frame.validityPlane);
for (const auto& plane : frame.planes) appendPlane(plane);
return snapshot;
}
bool compatible(const DecodedFieldFrame& a, const DecodedFieldFrame& b) {
return a.grid == b.grid && a.grid.isValid() && b.grid.isValid();
}
bool scalarSnapshot(const DecodedFieldFrame& frame, const DecodedFieldPlane& plane, ParticleFieldSnapshot& output) {
const auto cells = static_cast<size_t>(frame.grid.width) * frame.grid.height;
if (plane.count != cells || !materializeValidity(frame, output.valid)) return false;
output.scalar.resize(cells);
for (size_t cell = 0; cell < cells; ++cell)
if (!decodeRaw(plane, cell, output.scalar[cell])) return false;
return output.isValid();
}
bool vectorSnapshot(const DecodedFieldFrame& frame, const ParticleBinding& binding, ParticleFieldSnapshot& output) {
const auto cells = static_cast<size_t>(frame.grid.width) * frame.grid.height;
if (!materializeValidity(frame, output.valid)) return false;
output.vector.resize(cells * 2);
const auto* first = findPlane(
frame, binding.mode == ParticleBindingMode::VectorComponents ? binding.uPlane : binding.directionPlane);
const auto* second = binding.mode == ParticleBindingMode::VectorComponents ? findPlane(frame, binding.vPlane)
: nullptr;
if (!first || (binding.mode == ParticleBindingMode::VectorComponents && !second) || first->count != cells ||
(second && second->count != cells))
return false;
for (size_t cell = 0; cell < cells; ++cell) {
float u = 0, v = 0;
if (binding.mode == ParticleBindingMode::VectorComponents) {
if (!decodeRaw(*first, cell, u) || !decodeRaw(*second, cell, v)) return false;
output.vectorSemantics = VectorSemantics::Velocity;
} else {
// semantic 10 is the frozen QBIN V2 DIRECTION_TO degree encoding.
if (first->key.semanticID != 10 || first->dtype != FieldDType::UInt16 || !decodeRaw(*first, cell, u))
return false;
constexpr float Pi = 3.14159265358979323846f;
const auto radians = u * (Pi / 180.0f);
u = std::sin(radians);
v = std::cos(radians);
if (!std::isfinite(u) || !std::isfinite(v)) return false;
output.vectorSemantics = VectorSemantics::Direction;
}
output.vector[cell * 2] = u;
output.vector[cell * 2 + 1] = v;
}
return output.isValid();
}
} // namespace
std::optional<MaterializedColorProjection> materializeColorProjection(const ColorProjection& projection) {
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {};
const auto& binding = projection.contentIdentity.binding;
if (binding.expectedValueClass == FieldValueClass::VelocityComponent) {
const auto findComponent = [&](const DecodedFieldFrame& frame, const FieldComponent component) {
const DecodedFieldPlane* result = nullptr;
for (const auto& plane : frame.planes) {
if (plane.valueClass != FieldValueClass::VelocityComponent || plane.unitID != binding.expectedUnitID ||
plane.key.groupID != binding.plane.groupID || plane.key.component != component)
continue;
if (result) return static_cast<const DecodedFieldPlane*>(nullptr);
result = &plane;
}
return result;
};
const auto* u0 = findComponent(*projection.t0Frame, FieldComponent::UEast);
const auto* v0 = findComponent(*projection.t0Frame, FieldComponent::VNorth);
const auto* u1 = findComponent(*projection.t1Frame, FieldComponent::UEast);
const auto* v1 = findComponent(*projection.t1Frame, FieldComponent::VNorth);
if (!u0 || !v0 || !u1 || !v1 || !u0->isInterpolationCompatibleWith(*u1) ||
!v0->isInterpolationCompatibleWith(*v1))
return {};
ParticleBinding vectorBinding{
ParticleBindingMode::VectorComponents,
binding.fieldKey,
u0->key,
v0->key,
{},
binding.expectedUnitID,
};
auto t0 = std::make_shared<ParticleFieldSnapshot>(
baseSnapshot(*projection.t0Frame, u0->contentIdentity + "|" + v0->contentIdentity));
if (!vectorSnapshot(*projection.t0Frame, vectorBinding, *t0)) return {};
if (projection.t0Frame == projection.t1Frame)
return MaterializedColorProjection{t0, t0, projection.contentIdentity, projection.sampleState,
MaterializedColorValueSource::VectorMagnitude};
auto t1 = std::make_shared<ParticleFieldSnapshot>(
baseSnapshot(*projection.t1Frame, u1->contentIdentity + "|" + v1->contentIdentity));
if (!vectorSnapshot(*projection.t1Frame, vectorBinding, *t1)) return {};
return MaterializedColorProjection{std::move(t0),
std::move(t1),
projection.contentIdentity,
projection.sampleState,
MaterializedColorValueSource::VectorMagnitude};
}
const auto* p0 = findPlane(*projection.t0Frame, binding.plane);
const auto* p1 = findPlane(*projection.t1Frame, binding.plane);
if (!p0 || !p1) return {};
auto t0 = std::make_shared<ParticleFieldSnapshot>(
baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
if (!scalarSnapshot(*projection.t0Frame, *p0, *t0)) return {};
if (projection.t0Frame == projection.t1Frame)
return MaterializedColorProjection{t0, t0, projection.contentIdentity, projection.sampleState,
MaterializedColorValueSource::Scalar};
auto t1 = std::make_shared<ParticleFieldSnapshot>(
baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane));
if (!scalarSnapshot(*projection.t1Frame, *p1, *t1)) return {};
return MaterializedColorProjection{std::move(t0),
std::move(t1),
projection.contentIdentity,
projection.sampleState,
MaterializedColorValueSource::Scalar};
}
std::optional<MaterializedParticleProjection> materializeParticleProjection(const ParticleProjection& projection) {
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {};
auto t0 = std::make_shared<ParticleFieldSnapshot>(
baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane));
if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, *t0)) return {};
if (projection.t0Frame == projection.t1Frame)
return MaterializedParticleProjection{t0, t0, projection.contentIdentity, projection.sampleState};
auto t1 = std::make_shared<ParticleFieldSnapshot>(
baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane));
if (!vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, *t1)) return {};
return MaterializedParticleProjection{
std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState};
}
} // namespace mln::particle

View File

@@ -0,0 +1,308 @@
#include <mln/particle/field/field_runtime.hpp>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <set>
namespace mln::particle {
namespace {
bool validProfileID(const std::string& value) {
if (value.empty() || value.size() > 64) return false;
return std::all_of(value.begin(), value.end(), [](const unsigned char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
});
}
bool newerRevision(const FrameContentIdentity& next, const FrameContentIdentity& current) {
if (next.generation != current.generation) return next.generation > current.generation;
if (next.dataVersion != current.dataVersion) return next.dataVersion > current.dataVersion;
return false;
}
bool samePlaneSchema(const DecodedFieldPlane& a, const DecodedFieldPlane& b) {
return a.key == b.key && a.dtype == b.dtype && a.valueClass == b.valueClass && a.unitID == b.unitID &&
a.key.component == b.key.component && a.count == b.count && a.scale == b.scale && a.offset == b.offset;
}
bool validValueClass(const FieldValueClass value) {
return value <= FieldValueClass::Validity;
}
bool validComponent(const FieldComponent value) {
return value <= FieldComponent::WUp;
}
bool validDType(const FieldDType value) {
return value >= FieldDType::Int16 && value <= FieldDType::Bitset;
}
} // namespace
bool ResolvedFieldProfileContract::isStructurallyValid() const {
if (contractSchemaVersion != 1 || !validProfileID(profileID) || profileVersion == 0 || requiredPlanes.empty() ||
timeRelation > FieldTimeRelation::ValidEqReference)
return false;
std::set<std::pair<uint16_t, uint16_t>> keys;
for (const auto& plane : requiredPlanes) {
if (!plane.requiredFlag || plane.semanticID == 0 || !validValueClass(plane.valueClass) ||
!validComponent(plane.component)) return false;
const auto key = std::make_pair(plane.semanticID, plane.groupID);
if (!keys.insert(key).second) return false;
if (plane.valueClass == FieldValueClass::VelocityComponent &&
(plane.groupID == 0 || plane.component == FieldComponent::None)) return false;
if (plane.valueClass == FieldValueClass::Validity &&
(plane.groupID != 0 || plane.component != FieldComponent::None)) return false;
}
return true;
}
bool FrameContentIdentity::isValid() const {
return !fieldID.empty() && generation > 0 && validTimeMs > 0;
}
bool FrameContentIdentity::operator==(const FrameContentIdentity& other) const {
return fieldID == other.fieldID && generation == other.generation && dataVersion == other.dataVersion &&
validTimeMs == other.validTimeMs;
}
bool FieldPlaneKey::operator==(const FieldPlaneKey& other) const {
return semanticID == other.semanticID && groupID == other.groupID;
}
bool FieldPlaneKey::operator<(const FieldPlaneKey& other) const {
if (semanticID != other.semanticID) return semanticID < other.semanticID;
return groupID < other.groupID;
}
bool DecodedFieldPlane::isStructurallyValid(const uint64_t cellCount) const {
return key.semanticID > 0 && validDType(dtype) && validValueClass(valueClass) && validComponent(key.component) &&
count == cellCount && std::isfinite(scale) && scale > 0.0f && std::isfinite(offset) &&
!contentIdentity.empty() && storage && !storage->empty();
}
bool DecodedFieldPlane::isInterpolationCompatibleWith(const DecodedFieldPlane& other) const {
return samePlaneSchema(*this, other);
}
bool DecodedFieldGrid::isValid() const {
return width > 0 && height > 0 && std::isfinite(west) && std::isfinite(south) && std::isfinite(east) &&
std::isfinite(north) && west < east && south < north;
}
bool DecodedFieldGrid::operator==(const DecodedFieldGrid& other) const {
return layout == other.layout && projection == other.projection && sampleOrder == other.sampleOrder &&
gridRegistration == other.gridRegistration && width == other.width && height == other.height &&
west == other.west && south == other.south && east == other.east && north == other.north;
}
bool DecodedFieldFrame::isStructurallyValid() const {
if (!frameContentIdentity.isValid() || referenceTimeMs == 0 || !grid.isValid() || !validityBits ||
validityBits->empty() || validityContentIdentity.empty()) return false;
const uint64_t cells = static_cast<uint64_t>(grid.width) * grid.height;
const uint64_t expectedValidityBytes = (cells + 7u) / 8u;
if (validityBits->size() != expectedValidityBytes) return false;
if (!validityPlane.isStructurallyValid(cells) || validityPlane.valueClass != FieldValueClass::Validity ||
validityPlane.dtype != FieldDType::Bitset || validityPlane.unitID != FieldUnitNone ||
validityPlane.key.groupID != 0 || validityPlane.key.component != FieldComponent::None ||
!validityPlane.requiredFlag || validityPlane.contentIdentity != validityContentIdentity ||
validityPlane.storage != validityBits)
return false;
std::set<FieldPlaneKey> keys;
if (!keys.insert(validityPlane.key).second) return false;
for (const auto& plane : planes) {
if (!plane.isStructurallyValid(cells) || plane.valueClass == FieldValueClass::Validity ||
!keys.insert(plane.key).second)
return false;
}
return true;
}
bool DecodedFieldFrame::isSeriesCompatibleWith(const DecodedFieldFrame& other) const {
if (!(grid == other.grid) || !validityPlane.isInterpolationCompatibleWith(other.validityPlane) ||
planes.size() != other.planes.size()) return false;
for (size_t i = 0; i < planes.size(); ++i) {
if (!planes[i].isInterpolationCompatibleWith(other.planes[i])) return false;
}
return true;
}
bool DecodedFieldFrame::isInterpolationCompatibleWith(const DecodedFieldFrame& other) const {
return frameContentIdentity.fieldID == other.frameContentIdentity.fieldID &&
referenceTimeMs == other.referenceTimeMs && resolvedProfileID == other.resolvedProfileID &&
resolvedProfileVersion == other.resolvedProfileVersion && isSeriesCompatibleWith(other);
}
bool DecodedFieldFrame::hasSameContentAs(const DecodedFieldFrame& other) const {
if (referenceTimeMs != other.referenceTimeMs || resolvedProfileID != other.resolvedProfileID ||
resolvedProfileVersion != other.resolvedProfileVersion || !(grid == other.grid) ||
validityContentIdentity != other.validityContentIdentity ||
!samePlaneSchema(validityPlane, other.validityPlane) || planes.size() != other.planes.size()) return false;
for (size_t i = 0; i < planes.size(); ++i) {
if (!samePlaneSchema(planes[i], other.planes[i]) || planes[i].contentIdentity != other.planes[i].contentIdentity)
return false;
}
return true;
}
uint64_t DecodedFieldFrame::decodedByteSize() const {
uint64_t total = validityBits ? validityBits->size() : 0;
for (const auto& plane : planes) {
if (!plane.storage) continue;
const auto bytes = static_cast<uint64_t>(plane.storage->size());
if (bytes > std::numeric_limits<uint64_t>::max() - total) return std::numeric_limits<uint64_t>::max();
total += bytes;
}
return total;
}
FieldSeries::FieldSeries(const size_t capacity) : capacity_(capacity) {}
uint64_t FieldSeries::decodedByteSize() const {
uint64_t total = 0;
for (const auto& frame : frames) {
const auto bytes = frame->decodedByteSize();
if (bytes > std::numeric_limits<uint64_t>::max() - total) return std::numeric_limits<uint64_t>::max();
total += bytes;
}
return total;
}
FieldSeriesSubmitResult FieldSeries::submit(std::shared_ptr<const DecodedFieldFrame> frame) {
if (!frame || !frame->isStructurallyValid()) return FieldSeriesSubmitResult::RejectedInvalidFrame;
const auto timestamp = frame->frameContentIdentity.validTimeMs;
auto it = std::lower_bound(frames.begin(), frames.end(), timestamp,
[](const auto& item, const uint64_t value) {
return item->frameContentIdentity.validTimeMs < value;
});
if (it != frames.end() && (*it)->frameContentIdentity.validTimeMs == timestamp) {
const auto& current = **it;
if (frame->frameContentIdentity == current.frameContentIdentity) {
return frame->hasSameContentAs(current) ? FieldSeriesSubmitResult::Duplicate
: FieldSeriesSubmitResult::RejectedIdentityConflict;
}
if (!newerRevision(frame->frameContentIdentity, current.frameContentIdentity))
return FieldSeriesSubmitResult::RejectedStaleRevision;
if (!frame->isSeriesCompatibleWith(current))
return FieldSeriesSubmitResult::RejectedIncompatibleFrame;
if (it != frames.begin() && !frame->isSeriesCompatibleWith(**std::prev(it)))
return FieldSeriesSubmitResult::RejectedIncompatibleFrame;
if (std::next(it) != frames.end() && !frame->isSeriesCompatibleWith(**std::next(it)))
return FieldSeriesSubmitResult::RejectedIncompatibleFrame;
*it = std::move(frame);
return FieldSeriesSubmitResult::ReplacedRevision;
}
if (capacity_ == 0) return FieldSeriesSubmitResult::RejectedCapacity;
// Build and validate the complete replacement before mutating the live
// window. A failed frame must never evict a previously accepted endpoint.
auto candidate = frames;
size_t insertionIndex = static_cast<size_t>(std::distance(frames.begin(), it));
if (candidate.size() >= capacity_) {
const uint64_t earliest = frames.front()->frameContentIdentity.validTimeMs;
const uint64_t latest = frames.back()->frameContentIdentity.validTimeMs;
const uint64_t distanceToEarliest = timestamp >= earliest ? timestamp - earliest : earliest - timestamp;
const uint64_t distanceToLatest = timestamp >= latest ? timestamp - latest : latest - timestamp;
// C-002: equidistant candidates evict the original earliest endpoint.
const size_t victimIndex = distanceToEarliest >= distanceToLatest ? 0 : frames.size() - 1;
if (victimIndex < insertionIndex) --insertionIndex;
candidate.erase(candidate.begin() + static_cast<std::ptrdiff_t>(victimIndex));
}
candidate.insert(candidate.begin() + static_cast<std::ptrdiff_t>(insertionIndex), std::move(frame));
const auto inserted = candidate.begin() + static_cast<std::ptrdiff_t>(insertionIndex);
if (inserted != candidate.begin() && !(*inserted)->isSeriesCompatibleWith(**std::prev(inserted)))
return FieldSeriesSubmitResult::RejectedIncompatibleFrame;
if (std::next(inserted) != candidate.end() && !(*inserted)->isSeriesCompatibleWith(**std::next(inserted)))
return FieldSeriesSubmitResult::RejectedIncompatibleFrame;
frames.swap(candidate);
return FieldSeriesSubmitResult::Inserted;
}
std::optional<FieldSeriesWindow> FieldSeries::resolve(const uint64_t requestedForecastTimeMs) const {
if (frames.empty()) return std::nullopt;
const auto exact = std::lower_bound(frames.begin(), frames.end(), requestedForecastTimeMs,
[](const auto& item, const uint64_t value) {
return item->frameContentIdentity.validTimeMs < value;
});
if (exact != frames.end() && (*exact)->frameContentIdentity.validTimeMs == requestedForecastTimeMs)
return FieldSeriesWindow{*exact, *exact, 0.0f, requestedForecastTimeMs};
if (frames.size() < 2 || requestedForecastTimeMs < frames.front()->frameContentIdentity.validTimeMs ||
requestedForecastTimeMs > frames.back()->frameContentIdentity.validTimeMs) return std::nullopt;
const auto upper = std::upper_bound(frames.begin(), frames.end(), requestedForecastTimeMs,
[](const uint64_t value, const auto& item) {
return value < item->frameContentIdentity.validTimeMs;
});
if (upper == frames.begin() || upper == frames.end()) return std::nullopt;
const auto lower = std::prev(upper);
const uint64_t t0 = (*lower)->frameContentIdentity.validTimeMs;
const uint64_t t1 = (*upper)->frameContentIdentity.validTimeMs;
if (t1 <= t0 || !(*lower)->isInterpolationCompatibleWith(**upper)) return std::nullopt;
const double ratio = static_cast<double>(requestedForecastTimeMs - t0) / static_cast<double>(t1 - t0);
return FieldSeriesWindow{*lower, *upper, static_cast<float>(std::clamp(ratio, 0.0, 1.0)), requestedForecastTimeMs};
}
bool ForecastClock::setRequestedForecastTime(const uint64_t timestampMs) {
if (timestampMs == 0) return false;
requestedForecastTimeMs_ = timestampMs;
fractionalHalfMillisecond_ = 0;
return true;
}
bool ForecastClock::setPlaybackSpeed(const float value) {
if (value != 0.5f && value != 1.0f && value != 2.0f) return false;
playbackSpeed_ = value;
return true;
}
bool ForecastClock::advance(const uint64_t elapsedMs) {
if (state_ != ForecastPlaybackState::Playing || requestedForecastTimeMs_ == 0) return false;
const uint64_t halfMillisecondsPerElapsedMs = playbackSpeed_ == 0.5f ? 1 : playbackSpeed_ == 1.0f ? 2 : 4;
if (elapsedMs > std::numeric_limits<uint64_t>::max() / halfMillisecondsPerElapsedMs) return false;
const uint64_t elapsedHalfMilliseconds = elapsedMs * halfMillisecondsPerElapsedMs;
if (elapsedHalfMilliseconds > std::numeric_limits<uint64_t>::max() - fractionalHalfMillisecond_) return false;
const uint64_t accumulatedHalfMilliseconds = elapsedHalfMilliseconds + fractionalHalfMillisecond_;
const uint64_t wholeMilliseconds = accumulatedHalfMilliseconds / 2;
if (wholeMilliseconds > std::numeric_limits<uint64_t>::max() - requestedForecastTimeMs_) return false;
requestedForecastTimeMs_ += wholeMilliseconds;
fractionalHalfMillisecond_ = static_cast<uint8_t>(accumulatedHalfMilliseconds % 2);
return wholeMilliseconds != 0;
}
FieldSeriesSubmitResult EncodedFieldStore::submit(const std::string& fieldKey,
std::shared_ptr<const DecodedFieldFrame> frame) {
if (fieldKey.empty()) return FieldSeriesSubmitResult::RejectedInvalidFieldKey;
auto it = series.find(fieldKey);
if (it == series.end()) it = series.emplace(fieldKey, FieldSeries{perSeriesCapacity_}).first;
const auto result = it->second.submit(std::move(frame));
if (result == FieldSeriesSubmitResult::RejectedInvalidFrame && it->second.size() == 0) series.erase(it);
return result;
}
std::optional<FieldSeriesWindow> EncodedFieldStore::resolve(const std::string& fieldKey,
const uint64_t requestedForecastTimeMs) const {
const auto* found = find(fieldKey);
return found ? found->resolve(requestedForecastTimeMs) : std::nullopt;
}
const FieldSeries* EncodedFieldStore::find(const std::string& fieldKey) const {
const auto it = series.find(fieldKey);
return it == series.end() ? nullptr : &it->second;
}
uint64_t EncodedFieldStore::decodedByteSize() const {
uint64_t total = 0;
for (const auto& [_, fieldSeries] : series) {
const auto bytes = fieldSeries.decodedByteSize();
if (bytes > std::numeric_limits<uint64_t>::max() - total) return std::numeric_limits<uint64_t>::max();
total += bytes;
}
return total;
}
bool EncodedFieldStore::clear(const std::string& fieldKey) {
return series.erase(fieldKey) != 0;
}
} // namespace mln::particle

View File

@@ -0,0 +1,71 @@
#include <mln/particle/field/local_particle_field_feed.hpp>
#include <rapidjson/document.h>
namespace mln::particle {
namespace {
ParticleFieldAdapterResult reject(ParticleFieldAdapterError error, std::string message) {
return {.snapshot = std::nullopt, .error = error, .message = std::move(message)};
}
bool stringMember(const rapidjson::Value& v, const char* n) { return v.HasMember(n) && v[n].IsString(); }
bool numberMember(const rapidjson::Value& v, const char* n) { return v.HasMember(n) && v[n].IsNumber(); }
bool common(const rapidjson::Value& m) {
return m.IsObject() && stringMember(m, "fieldId") && m.HasMember("generation") && m["generation"].IsUint64() &&
m.HasMember("validTime") && m["validTime"].IsUint64() && m.HasMember("grid") && m["grid"].IsObject() &&
m["grid"].HasMember("width") && m["grid"]["width"].IsUint() && m["grid"].HasMember("height") && m["grid"]["height"].IsUint() &&
m.HasMember("bounds") && m["bounds"].IsObject() && numberMember(m["bounds"], "west") && numberMember(m["bounds"], "south") &&
numberMember(m["bounds"], "east") && numberMember(m["bounds"], "north");
}
void fillCommon(ParticleFieldDescriptor& d, const rapidjson::Value& m) {
d.fieldID = m["fieldId"].GetString(); d.generation = m["generation"].GetUint64(); d.validTime = m["validTime"].GetUint64();
d.width = m["grid"]["width"].GetUint(); d.height = m["grid"]["height"].GetUint();
d.west = m["bounds"]["west"].GetDouble(); d.south = m["bounds"]["south"].GetDouble();
d.east = m["bounds"]["east"].GetDouble(); d.north = m["bounds"]["north"].GetDouble();
if (m.HasMember("dataVersion") && m["dataVersion"].IsUint64()) d.dataVersion = m["dataVersion"].GetUint64();
if (stringMember(m, "fieldType")) d.fieldType = m["fieldType"].GetString();
if (stringMember(m, "displayDomain")) d.displayDomain = m["displayDomain"].GetString();
}
std::optional<ParticleFieldPlaneDescriptor> plane(const rapidjson::Value& m, const char* name, bool vector) {
if (!m.HasMember(name)) return std::nullopt;
const auto& p = m[name];
if (!p.IsObject() || !stringMember(p, "encoding") || !p.HasMember("byteLength") || !p["byteLength"].IsUint64() ||
(vector && !stringMember(p, "layout"))) return std::nullopt;
ParticleFieldPlaneDescriptor result{.byteLength = p["byteLength"].GetUint64(), .encoding = p["encoding"].GetString(),
.layout = {}, .noData = std::nullopt};
if (vector) result.layout = p["layout"].GetString();
if (p.HasMember("noData") && p["noData"].IsNumber()) result.noData = static_cast<float>(p["noData"].GetDouble());
return result;
}
} // namespace
ParticleFieldAdapterResult LocalParticleFieldFeed::load(const std::string& metadataText, const std::vector<char>& payload) {
rapidjson::Document m; m.Parse(metadataText.c_str());
if (m.HasParseError() || !m.IsObject() || !stringMember(m, "schema") || std::string(m["schema"].GetString()) != "maplibre-particle-field/v1" ||
!common(m) || !stringMember(m, "fieldType") || !stringMember(m, "displayDomain") || !m.HasMember("vector") || !m["vector"].IsObject() ||
!stringMember(m["vector"], "layout") || !stringMember(m["vector"], "encoding") || !numberMember(m["vector"], "noData") ||
!m.HasMember("payload") || !m["payload"].IsObject() || !m["payload"].HasMember("byteLength") || !m["payload"]["byteLength"].IsUint64())
return reject(ParticleFieldAdapterError::InvalidDescriptor, "invalid v1 local field metadata");
ParticleFieldDescriptor d; fillCommon(d, m);
d.layout = m["vector"]["layout"].GetString(); d.encoding = m["vector"]["encoding"].GetString();
d.noData = static_cast<float>(m["vector"]["noData"].GetDouble()); d.byteLength = m["payload"]["byteLength"].GetUint64();
if (m.HasMember("stride") && m["stride"].IsUint()) d.stride = m["stride"].GetUint();
return ParticleFieldDataAdapter::decode(d, payload, d.generation);
}
ParticleFieldAdapterResult LocalParticleFieldFeed::load(const std::string& metadataText, const ParticleFieldPayloadBundle& payloads) {
rapidjson::Document m; m.Parse(metadataText.c_str());
if (m.HasParseError() || !m.IsObject() || !stringMember(m, "schema") || std::string(m["schema"].GetString()) != "maplibre-particle-field/v2" || !common(m))
return reject(ParticleFieldAdapterError::InvalidDescriptor, "invalid v2 local field metadata");
ParticleFieldDescriptor d; fillCommon(d, m);
d.scalar = plane(m, "scalar", false); d.vector = plane(m, "vector", true); d.validity = plane(m, "validity", false);
if (!d.validity || d.validity->encoding != "uint8") return reject(ParticleFieldAdapterError::InvalidDescriptor, "invalid v2 validity metadata");
if (m.HasMember("vector")) {
if (!stringMember(m["vector"], "semantics")) return reject(ParticleFieldAdapterError::InvalidDescriptor, "v2 vector semantics missing");
const std::string semantics = m["vector"]["semantics"].GetString();
if (semantics == "VELOCITY") d.vectorSemantics = VectorSemantics::Velocity;
else if (semantics == "DIRECTION") d.vectorSemantics = VectorSemantics::Direction;
else return reject(ParticleFieldAdapterError::InvalidDescriptor, "unsupported vector semantics");
}
return ParticleFieldDataAdapter::decode(d, payloads);
}
} // namespace mln::particle

View File

@@ -0,0 +1,199 @@
#include <mln/particle/field/offline_fixture_particle_adapter.hpp>
#include <rapidjson/document.h>
#include <cmath>
#include <array>
#include <cstring>
#include <fstream>
#include <stdexcept>
namespace mln::particle {
namespace {
bool knownFixture(const std::string& fixtureID) {
return fixtureID == "uniform-east" || fixtureID == "uniform-north" || fixtureID == "vortex" ||
fixtureID == "nodata";
}
std::string readText(const std::string& path) {
std::ifstream input(path.c_str());
if (!input) throw std::invalid_argument("fixture metadata unavailable: " + path);
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
std::vector<char> readBinary(const std::string& path) {
std::ifstream input(path.c_str(), std::ios::binary);
if (!input) throw std::invalid_argument("fixture field unavailable: " + path);
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
const rapidjson::Value& member(const rapidjson::Value& value, const char* name) {
if (!value.HasMember(name)) throw std::invalid_argument(std::string("fixture metadata missing: ") + name);
return value[name];
}
std::string sha256(const std::vector<char>& input) {
constexpr std::array<uint32_t, 64> constants = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
const auto rotateRight = [](uint32_t value, uint32_t bits) {
return (value >> bits) | (value << (32 - bits));
};
std::vector<uint8_t> bytes(input.begin(), input.end());
const uint64_t bitLength = static_cast<uint64_t>(bytes.size()) * 8;
bytes.push_back(0x80);
while ((bytes.size() % 64) != 56) bytes.push_back(0);
for (int shift = 56; shift >= 0; shift -= 8) bytes.push_back(static_cast<uint8_t>(bitLength >> shift));
std::array<uint32_t, 8> hash = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
for (size_t offset = 0; offset < bytes.size(); offset += 64) {
std::array<uint32_t, 64> words{};
for (size_t i = 0; i < 16; ++i) {
words[i] = (static_cast<uint32_t>(bytes[offset + i * 4]) << 24) |
(static_cast<uint32_t>(bytes[offset + i * 4 + 1]) << 16) |
(static_cast<uint32_t>(bytes[offset + i * 4 + 2]) << 8) |
static_cast<uint32_t>(bytes[offset + i * 4 + 3]);
}
for (size_t i = 16; i < words.size(); ++i) {
const uint32_t a = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ (words[i - 15] >> 3);
const uint32_t b = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ (words[i - 2] >> 10);
words[i] = words[i - 16] + a + words[i - 7] + b;
}
uint32_t a = hash[0], b = hash[1], c = hash[2], d = hash[3];
uint32_t e = hash[4], f = hash[5], g = hash[6], h = hash[7];
for (size_t i = 0; i < words.size(); ++i) {
const uint32_t sigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
const uint32_t choice = (e & f) ^ (~e & g);
const uint32_t temp1 = h + sigma1 + choice + constants[i] + words[i];
const uint32_t sigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
const uint32_t majority = (a & b) ^ (a & c) ^ (b & c);
const uint32_t temp2 = sigma0 + majority;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
}
hash[0] += a;
hash[1] += b;
hash[2] += c;
hash[3] += d;
hash[4] += e;
hash[5] += f;
hash[6] += g;
hash[7] += h;
}
static constexpr char digits[] = "0123456789abcdef";
std::string result;
result.reserve(64);
for (const uint32_t word : hash) {
for (int shift = 28; shift >= 0; shift -= 4) result.push_back(digits[(word >> shift) & 0x0f]);
}
return result;
}
} // namespace
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID,
const std::string& metadataText,
const std::vector<char>& bytes,
const uint64_t generation) {
if (!knownFixture(fixtureID)) {
throw std::invalid_argument("unknown offline particle fixture: " + fixtureID);
}
rapidjson::Document metadata;
metadata.Parse(metadataText.c_str());
if (metadata.HasParseError() || !metadata.IsObject() || !member(metadata, "schema").IsString() ||
std::string(member(metadata, "schema").GetString()) != "maplibre-particle-fixture/v1" ||
!member(metadata, "fixtureId").IsString() ||
std::string(member(metadata, "fixtureId").GetString()) != fixtureID ||
!member(metadata, "fieldType").IsString() || !member(metadata, "displayDomain").IsString()) {
throw std::invalid_argument("invalid fixture metadata: " + fixtureID);
}
const auto& grid = member(metadata, "grid");
const auto& bounds = member(metadata, "bounds");
const auto& vector = member(metadata, "vector");
const auto& integrity = member(metadata, "integrity");
if (!grid.IsObject() || !bounds.IsObject() || !vector.IsObject() || !integrity.IsObject() ||
!member(metadata, "timeIndex").IsUint64() || !member(metadata, "crs").IsString() ||
std::string(member(metadata, "crs").GetString()) != "EPSG:4326" || !member(grid, "width").IsUint() ||
!member(grid, "height").IsUint() || !member(bounds, "west").IsNumber() || !member(bounds, "south").IsNumber() ||
!member(bounds, "east").IsNumber() || !member(bounds, "north").IsNumber() ||
!member(grid, "registration").IsString() || !member(grid, "origin").IsString() ||
!member(grid, "rowOrder").IsString() || !member(grid, "columnOrder").IsString() ||
!member(vector, "uPositive").IsString() || !member(vector, "vPositive").IsString() ||
!member(vector, "unit").IsString() || !member(vector, "layout").IsString() ||
!member(vector, "encoding").IsString() || !member(vector, "noData").IsNumber() ||
!member(vector, "file").IsString() || !member(integrity, "fieldByteLength").IsUint64() ||
!member(integrity, "fieldSha256").IsString()) {
throw std::invalid_argument("invalid fixture grid/bounds/encoding: " + fixtureID);
}
if (std::string(grid["registration"].GetString()) != "node" ||
std::string(grid["origin"].GetString()) != "north-west" ||
std::string(grid["rowOrder"].GetString()) != "north-to-south" ||
std::string(grid["columnOrder"].GetString()) != "west-to-east" ||
std::string(vector["uPositive"].GetString()) != "east" ||
std::string(vector["vPositive"].GetString()) != "north" ||
std::string(vector["layout"].GetString()) != "interleaved-uv" ||
std::string(vector["encoding"].GetString()) != "float32-le" ||
std::string(vector["file"].GetString()) != "field.f32") {
throw std::invalid_argument("unsupported fixture v1 layout: " + fixtureID);
}
ParticleFieldDescriptor descriptor;
descriptor.fieldID = fixtureID;
descriptor.fieldType = member(metadata, "fieldType").GetString();
descriptor.displayDomain = member(metadata, "displayDomain").GetString();
descriptor.generation = generation;
descriptor.validTime = member(metadata, "timeIndex").GetUint64() + 1;
descriptor.width = grid["width"].GetUint();
descriptor.height = grid["height"].GetUint();
descriptor.west = bounds["west"].GetDouble();
descriptor.south = bounds["south"].GetDouble();
descriptor.east = bounds["east"].GetDouble();
descriptor.north = bounds["north"].GetDouble();
descriptor.noData = static_cast<float>(vector["noData"].GetDouble());
if (!std::isfinite(descriptor.noData)) {
throw std::invalid_argument("non-finite fixture noData: " + fixtureID);
}
descriptor.dataVersion = member(metadata, "dataVersion").GetUint64();
descriptor.byteLength = static_cast<uint64_t>(descriptor.width) * descriptor.height * 2 * sizeof(float);
descriptor.fieldSha256 = member(integrity, "fieldSha256").GetString();
const size_t expected = static_cast<size_t>(descriptor.byteLength);
if (bytes.size() != expected || member(integrity, "fieldByteLength").GetUint64() != expected) {
throw std::invalid_argument("fixture field length mismatch: " + fixtureID);
}
if (sha256(bytes) != descriptor.fieldSha256) {
throw std::invalid_argument("fixture field checksum mismatch: " + fixtureID);
}
auto result = ParticleFieldDataAdapter::decode(descriptor, bytes, generation);
if (!result) {
throw std::invalid_argument("field adapter rejected fixture errorCode=" +
std::string(particleFieldAdapterErrorName(result.error)) +
" fieldId=" + descriptor.fieldID + " generation=" + std::to_string(generation) +
" validTime=" + std::to_string(descriptor.validTime) + " " + result.message);
}
return std::move(*result.snapshot);
}
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID, const uint64_t generation) {
if (!knownFixture(fixtureID)) {
throw std::invalid_argument("unknown offline particle fixture: " + fixtureID);
}
const std::string directory = "particle-fixtures/" + fixtureID;
return load(fixtureID, readText(directory + "/metadata.json"), readBinary(directory + "/field.f32"), generation);
}
} // namespace mln::particle

View File

@@ -0,0 +1,22 @@
#pragma once
#include <mln/particle/field/field_data_adapter.hpp>
#include <cstdint>
#include <string>
#include <vector>
namespace mln::particle {
// Validation-only wrapper for the frozen offline fixture-v1 metadata schema.
// The resulting snapshot is always produced by ParticleFieldDataAdapter.
class OfflineFixtureParticleAdapter final {
public:
static ParticleFieldSnapshot load(const std::string& fixtureID,
const std::string& metadata,
const std::vector<char>& fieldBytes,
uint64_t generation);
static ParticleFieldSnapshot load(const std::string& fixtureID, uint64_t generation);
};
} // namespace mln::particle

View File

@@ -0,0 +1,127 @@
#include <mln/particle/ingress/qbin_v2_decoder.hpp>
#include <mln/particle/ingress/qbin_v3_decoder.hpp>
#include <cmath>
#include <cstring>
#include <limits>
#include <set>
#include <sstream>
#include <tuple>
#include <vector>
namespace mln::particle::ingress {
namespace {
constexpr size_t HeaderSize = 112, DescriptorSize = 32, MaxBytes = 64u * 1024u * 1024u;
constexpr uint32_t MaxPlanes = 32, MaxCells = 2'097'152, MaxDecoded = 32u * 1024u * 1024u;
constexpr uint16_t ValiditySemantic = 3;
template <class T> T le(std::span<const uint8_t> b, size_t o) {
uint64_t v = 0; for (size_t i = 0; i < sizeof(T); ++i) v |= uint64_t(b[o + i]) << (8 * i);
T r{}; std::memcpy(&r, &v, sizeof(T)); return r;
}
template <> float le<float>(std::span<const uint8_t> b, size_t o) { uint32_t v = le<uint32_t>(b,o); float r; std::memcpy(&r,&v,4); return r; }
template <> double le<double>(std::span<const uint8_t> b, size_t o) { uint64_t v = le<uint64_t>(b,o); double r; std::memcpy(&r,&v,8); return r; }
uint32_t crc(std::span<const uint8_t> bytes) { uint32_t c=0xffffffffu; for(size_t i=0;i<bytes.size();++i) { uint8_t x=(i>=76&&i<80)?0:bytes[i]; c^=x; for(int j=0;j<8;++j)c=(c>>1)^((c&1)?0xedb88320u:0); } return ~c; }
size_t align16(size_t x) { return (x + 15u) & ~size_t(15u); }
QbinV2DecodeResult fail(QbinV2DecodeErrorCode c, std::string d) { return {c,std::move(d),{}}; }
bool zero(std::span<const uint8_t> b) { for(auto v:b) if(v) return false; return true; }
std::string identity(uint16_t semantic, uint16_t group, uint8_t component, uint8_t dtype, uint8_t flags, uint8_t vc, uint16_t unit, uint32_t count, float scale, float offset, std::span<const uint8_t> raw) {
uint64_t h=1469598103934665603ull; auto add=[&](uint8_t v){h^=v;h*=1099511628211ull;};
auto addLE=[&](uint64_t value, size_t count) { for (size_t i = 0; i < count; ++i) add(uint8_t(value >> (8 * i))); };
uint32_t scaleBits = 0, offsetBits = 0;
std::memcpy(&scaleBits, &scale, sizeof(scaleBits));
std::memcpy(&offsetBits, &offset, sizeof(offsetBits));
for(auto v:raw)add(v);
addLE(semantic, 2); addLE(group, 2); add(component); add(dtype); add(flags); add(vc);
addLE(unit, 2); addLE(count, 4); addLE(scaleBits, 4); addLE(offsetBits, 4);
std::ostringstream s; s<<std::hex<<h; return s.str();
}
struct D { uint16_t semantic, unit, group; uint8_t dtype, flags, vc, component; float scale, offset; uint32_t count, off, len; };
}
QbinV2DecodeResult QbinV2Decoder::decode(const EncodedFieldRuntimeMetadata& meta, std::span<const uint8_t> b) {
if(meta.fieldID.empty() || meta.generation==0) return fail(QbinV2DecodeErrorCode::InvalidRuntimeMetadata,"fieldID/generation");
if(b.size()<HeaderSize) return fail(QbinV2DecodeErrorCode::InputTooSmall,"header"); if(b.size()>MaxBytes) return fail(QbinV2DecodeErrorCode::LimitExceeded,"file bytes");
if(std::memcmp(b.data(),"QBV2",4)!=0) return fail(QbinV2DecodeErrorCode::BadMagic,"magic"); if(b[4]!=2) return fail(QbinV2DecodeErrorCode::UnsupportedVersion,"version");
if(b[5]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedLayout,"layout"); if(b[8]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedProjection,"projection");
if(b[9]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedSampleOrder,"sampleOrder"); if(b[10]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedGridRegistration,"gridRegistration");
if(b[11]||le<uint16_t>(b,14)||le<uint16_t>(b,82)||le<uint32_t>(b,92)||le<uint32_t>(b,96)||le<uint32_t>(b,100)||le<uint32_t>(b,104)||le<uint32_t>(b,108)) return fail(QbinV2DecodeErrorCode::ReservedNonZero,"header reserved");
const uint16_t n=le<uint16_t>(b,12); const uint32_t w=le<uint32_t>(b,16), h=le<uint32_t>(b,20), payload=le<uint32_t>(b,72);
if(n==0 || n>MaxPlanes || w<2 || h<2) return fail(QbinV2DecodeErrorCode::InvalidHeader,"planes/grid"); const uint64_t cells=uint64_t(w)*h;
if(cells>MaxCells) return fail(QbinV2DecodeErrorCode::LimitExceeded,"cell count"); if(le<uint16_t>(b,80)!=0xffff || le<uint32_t>(b,84)||le<uint32_t>(b,88)) return fail(QbinV2DecodeErrorCode::InvalidHeader,"FRAME tile");
const int64_t ref=le<int64_t>(b,24), valid=le<int64_t>(b,32); if(ref<=0||valid<=0||uint64_t(ref)>uint64_t(std::numeric_limits<uint64_t>::max()/1000)||uint64_t(valid)>uint64_t(std::numeric_limits<uint64_t>::max()/1000)) return fail(QbinV2DecodeErrorCode::InvalidTime,"UTC seconds");
const double west=le<double>(b,40), south=le<double>(b,48), east=le<double>(b,56), north=le<double>(b,64);
if(!std::isfinite(west)||!std::isfinite(south)||!std::isfinite(east)||!std::isfinite(north)||west < -180||east > 180||west>=east||south < -85.0511287798066||north >85.0511287798066||south>=north) return fail(QbinV2DecodeErrorCode::InvalidBounds,"Web Mercator bounds");
const size_t po=HeaderSize+size_t(n)*DescriptorSize;
if(po>std::numeric_limits<size_t>::max()-payload || b.size()!=po+payload) return fail(QbinV2DecodeErrorCode::LengthMismatch,"total length");
std::vector<D> ds; ds.reserve(n); std::set<std::pair<uint16_t,uint16_t>> keys; uint16_t ps=0,pg=0; uint8_t pc=0; size_t expected=0; bool first=true; int validity=-1; uint64_t totalDecoded=0;
for(uint16_t i=0;i<n;++i){ size_t o=HeaderSize+size_t(i)*DescriptorSize; D d{le<uint16_t>(b,o),le<uint16_t>(b,o+24),le<uint16_t>(b,o+26),b[o+2],b[o+3],b[o+28],b[o+29],le<float>(b,o+4),le<float>(b,o+8),le<uint32_t>(b,o+12),le<uint32_t>(b,o+16),le<uint32_t>(b,o+20)};
if(le<uint16_t>(b,o+30)||d.flags&~1u) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"descriptor "+std::to_string(i)); if(!first && std::tuple(d.semantic,d.group,d.component)<=std::tuple(ps,pg,pc)) return fail(QbinV2DecodeErrorCode::NonCanonicalDescriptorOrder,"descriptor "+std::to_string(i)); first=false;ps=d.semantic;pg=d.group;pc=d.component;
if(!keys.insert({d.semantic,d.group}).second) return fail(QbinV2DecodeErrorCode::DuplicatePlaneKey,"descriptor "+std::to_string(i)); if(d.semantic==0||d.dtype<1||d.dtype>6||d.vc>3||d.component>3||d.count!=cells) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"descriptor "+std::to_string(i));
const bool isBitset = d.dtype == static_cast<uint8_t>(FieldDType::Bitset);
const bool isValidity = d.vc == static_cast<uint8_t>(FieldValueClass::Validity);
if (isBitset != isValidity) return fail(QbinV2DecodeErrorCode::InvalidValidity, "BITSET/valueClass");
uint64_t len=d.dtype==1||d.dtype==2?cells*2:d.dtype==5?cells*4:d.dtype==6?(cells+7)/8:cells; if(d.len!=len||d.off!=expected||len>payload||d.off>payload-len) return fail(d.off!=expected?QbinV2DecodeErrorCode::NonCanonicalPacking:QbinV2DecodeErrorCode::InvalidDescriptor,"descriptor "+std::to_string(i));
if(d.dtype!=6 && (!std::isfinite(d.scale)||d.scale<=0||!std::isfinite(d.offset))) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"numeric descriptor "+std::to_string(i)); if(d.dtype==6 && (d.scale!=1||d.offset!=0)) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"bitset descriptor");
expected=align16(expected+size_t(len)); totalDecoded+=len; if(totalDecoded>MaxDecoded) return fail(QbinV2DecodeErrorCode::LimitExceeded,"decoded bytes"); if(d.vc==3) { if(validity!=-1||d.semantic!=ValiditySemantic||d.unit||d.group||d.component||d.dtype!=6||!(d.flags&1)) return fail(QbinV2DecodeErrorCode::InvalidValidity,"validity descriptor"); validity=i; } ds.push_back(d); }
if(validity<0) return fail(QbinV2DecodeErrorCode::InvalidValidity,"missing validity"); if(expected!=payload) return fail(QbinV2DecodeErrorCode::NonCanonicalPacking,"payload length"); if(!zero(b.subspan(po+ds.back().off+ds.back().len, payload-(ds.back().off+ds.back().len)))) return fail(QbinV2DecodeErrorCode::NonZeroPadding,"tail padding");
for(size_t i=0;i<ds.size();++i) { const auto& d=ds[i]; size_t end=d.off+d.len, next=i+1<ds.size()?ds[i+1].off:payload; if(next>end&&!zero(b.subspan(po+end,next-end))) return fail(QbinV2DecodeErrorCode::NonZeroPadding,"plane padding"); }
if(crc(b)!=le<uint32_t>(b,76)) return fail(QbinV2DecodeErrorCode::CrcMismatch,"file CRC32"); const auto& vd=ds[validity]; auto vb=b.subspan(po+vd.off,vd.len); if(cells%8 && (vb.back() & uint8_t(0xffu << (cells%8)))) return fail(QbinV2DecodeErrorCode::InvalidValidity,"unused validity bits");
for(const auto& d:ds) if(d.dtype!=6) for(uint64_t c=0;c<cells;++c) if(!(vb[c/8]&(1u<<(c%8)))) { auto raw=b.subspan(po+d.off,d.len); size_t start=d.dtype==1||d.dtype==2?c*2:d.dtype==5?c*4:c; size_t z=d.dtype==1||d.dtype==2?2:d.dtype==5?4:1; if(!zero(raw.subspan(start,z))) return fail(QbinV2DecodeErrorCode::InvalidCellPayload,"invalid cell payload"); }
auto out=std::make_shared<DecodedFieldFrame>(); out->frameContentIdentity={meta.fieldID,meta.generation,meta.dataVersion,uint64_t(valid)*1000}; out->referenceTimeMs=uint64_t(ref)*1000; out->grid={1,1,1,1,w,h,west,south,east,north};
for(size_t i=0;i<ds.size();++i){const auto& d=ds[i]; auto raw=std::make_shared<const std::vector<uint8_t>>(b.begin()+po+d.off,b.begin()+po+d.off+d.len); DecodedFieldPlane p{{d.semantic,d.group,static_cast<FieldComponent>(d.component)},static_cast<FieldDType>(d.dtype),static_cast<FieldValueClass>(d.vc),d.unit,bool(d.flags&1),d.count,d.scale,d.offset,identity(d.semantic,d.group,d.component,d.dtype,d.flags,d.vc,d.unit,d.count,d.scale,d.offset,*raw),raw}; if(int(i)==validity){out->validityContentIdentity=p.contentIdentity;out->validityBits=raw;out->validityPlane=std::move(p);}else out->planes.push_back(std::move(p)); }
if (!out->isStructurallyValid()) return fail(QbinV2DecodeErrorCode::InvalidFrame, "decoded frame structural validity");
return {QbinV2DecodeErrorCode::Accepted,"accepted",out};
}
EncodedFieldIngressResult EncodedFieldIngress::decodeAndValidate(const EncodedFieldRuntimeMetadata& m,
const ResolvedFieldProfileContract* c,
std::span<const uint8_t> b) {
QbinDecodeErrorCode decodeCode = QbinDecodeErrorCode::BadMagic;
std::string detail;
std::shared_ptr<const DecodedFieldFrame> frame;
if (b.size() >= 4 && std::memcmp(b.data(), "QBV2", 4) == 0) {
auto decoded = QbinV2Decoder::decode(m, b);
decodeCode = decoded.code;
detail = std::move(decoded.detail);
frame = std::move(decoded.frame);
} else if (b.size() >= 4 && std::memcmp(b.data(), "QBV3", 4) == 0) {
auto decoded = QbinV3Decoder::decode(m, b);
decodeCode = decoded.code;
detail = std::move(decoded.detail);
frame = std::move(decoded.frame);
} else {
return {EncodedFieldIngressCode::DecodeRejected,
"QBIN magic",
QbinDecodeErrorCode::BadMagic,
FieldProfileValidationCode::InvalidContract,
{}};
}
if (decodeCode != QbinDecodeErrorCode::Accepted || !frame) {
return {EncodedFieldIngressCode::DecodeRejected,
std::move(detail),
decodeCode,
FieldProfileValidationCode::InvalidContract,
{}};
}
if (!c) {
return {EncodedFieldIngressCode::ContractMissing,
"contract",
decodeCode,
FieldProfileValidationCode::InvalidContract,
{}};
}
const auto validation = FieldProfileValidator::validate(*c, *frame);
if (!validation.accepted()) {
return {EncodedFieldIngressCode::ContractRejected,
validation.detail,
decodeCode,
validation.code,
{}};
}
auto resolvedFrame = std::make_shared<DecodedFieldFrame>(*frame);
resolvedFrame->resolvedProfileID = c->profileID;
resolvedFrame->resolvedProfileVersion = c->profileVersion;
return {EncodedFieldIngressCode::Accepted, "accepted", decodeCode, validation.code, std::move(resolvedFrame)};
}
} // namespace mln::particle::ingress

View File

@@ -0,0 +1,674 @@
#include <mln/particle/ingress/qbin_v3_decoder.hpp>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include <optional>
#include <set>
#include <sstream>
#include <tuple>
#include <utility>
#include <vector>
namespace mln::particle::ingress {
namespace {
constexpr size_t HeaderSize = 112;
constexpr size_t DescriptorSize = 32;
constexpr size_t MaxBytes = 64u * 1024u * 1024u;
constexpr uint32_t MaxPlanes = 32;
constexpr uint32_t MaxCells = 2'097'152;
constexpr uint32_t MaxDecoded = 32u * 1024u * 1024u;
constexpr uint16_t ValiditySemantic = 3;
uint16_t readU16(std::span<const uint8_t> b, size_t o) {
return uint16_t(b[o]) | (uint16_t(b[o + 1]) << 8);
}
uint32_t readU32(std::span<const uint8_t> b, size_t o) {
uint32_t v = 0;
for (size_t i = 0; i < 4; ++i) v |= uint32_t(b[o + i]) << (8 * i);
return v;
}
uint64_t readU64(std::span<const uint8_t> b, size_t o) {
uint64_t v = 0;
for (size_t i = 0; i < 8; ++i) v |= uint64_t(b[o + i]) << (8 * i);
return v;
}
int64_t readI64(std::span<const uint8_t> b, size_t o) {
const uint64_t bits = readU64(b, o);
int64_t value = 0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
float readF32(std::span<const uint8_t> b, size_t o) {
const uint32_t bits = readU32(b, o);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
double readF64(std::span<const uint8_t> b, size_t o) {
const uint64_t bits = readU64(b, o);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
uint32_t floatBits(float value) {
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
uint32_t crc32(std::span<const uint8_t> bytes) {
uint32_t c = 0xffffffffu;
for (size_t i = 0; i < bytes.size(); ++i) {
const uint8_t x = (i >= 76 && i < 80) ? 0 : bytes[i];
c ^= x;
for (int j = 0; j < 8; ++j) {
c = (c >> 1) ^ ((c & 1u) ? 0xedb88320u : 0u);
}
}
return ~c;
}
size_t align16(size_t x) {
return (x + 15u) & ~size_t(15u);
}
bool allZero(std::span<const uint8_t> bytes) {
for (const auto byte : bytes) {
if (byte != 0) return false;
}
return true;
}
QbinV3DecodeResult fail(QbinDecodeErrorCode code, std::string detail) {
return {code, std::move(detail), {}};
}
struct Descriptor {
uint16_t semantic = 0;
uint16_t unit = 0;
uint16_t group = 0;
uint8_t dtype = 0;
uint8_t flags = 0;
uint8_t valueClass = 0;
uint8_t component = 0;
float scale = 0.0f;
float offset = 0.0f;
uint32_t scaleBits = 0;
uint32_t offsetBits = 0;
uint32_t count = 0;
uint32_t byteOffset = 0;
uint32_t byteLength = 0;
uint32_t directionN = 0;
};
std::string contentIdentity(const Descriptor& d, std::span<const uint8_t> raw) {
uint64_t hash = 1469598103934665603ull;
auto addByte = [&](uint8_t v) {
hash ^= v;
hash *= 1099511628211ull;
};
auto addLE = [&](uint64_t value, size_t count) {
for (size_t i = 0; i < count; ++i) addByte(uint8_t(value >> (8 * i)));
};
for (const auto v : raw) addByte(v);
addLE(d.semantic, 2);
addLE(d.group, 2);
addByte(d.component);
addByte(d.dtype);
addByte(d.flags);
addByte(d.valueClass);
addLE(d.unit, 2);
addLE(d.count, 4);
addLE(d.scaleBits, 4);
addLE(d.offsetBits, 4);
std::ostringstream out;
out << std::hex << hash;
return out.str();
}
std::optional<uint32_t> directionCount(float scale) {
if (!std::isfinite(scale) || scale <= 0.0f) return std::nullopt;
const double ratioToN = 360.0 / double(scale);
if (!std::isfinite(ratioToN) || ratioToN < 1.0 || ratioToN > 65536.0) return std::nullopt;
const uint32_t n = static_cast<uint32_t>(std::floor(ratioToN + 0.5));
if (n < 1 || n > 65536) return std::nullopt;
const double ratio64 = 360.0 / double(n);
if (!std::isfinite(ratio64)) return std::nullopt;
const float canonicalScale = static_cast<float>(ratio64);
if (floatBits(canonicalScale) != floatBits(scale)) return std::nullopt;
return n;
}
uint64_t logicalDecodedBytes(const Descriptor& d, uint64_t cells) {
switch (d.dtype) {
case 1:
case 2:
return cells * 2u;
case 3:
case 4:
return cells;
case 6:
return (cells + 7u) / 8u;
default:
return std::numeric_limits<uint64_t>::max();
}
}
bool validAt(std::span<const uint8_t> validity, uint64_t index) {
return ((validity[index >> 3] >> (index & 7u)) & 1u) != 0;
}
std::pair<int64_t, int64_t> dtypeRange(uint8_t dtype) {
switch (dtype) {
case 1:
return {-32768, 32767};
case 2:
return {0, 65535};
case 3:
return {0, 255};
case 4:
return {-128, 127};
default:
return {1, 0};
}
}
int64_t readRaw(const std::vector<uint8_t>& raw, uint64_t index, uint8_t dtype) {
if (dtype == 1 || dtype == 2) {
const size_t o = size_t(index) * 2u;
const uint16_t bits = uint16_t(raw[o]) | (uint16_t(raw[o + 1]) << 8);
if (dtype == 2) return bits;
int16_t value = 0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
const uint8_t bits = raw[size_t(index)];
if (dtype == 3) return bits;
int8_t value = 0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
void writeRaw(std::vector<uint8_t>& raw, uint64_t index, uint8_t dtype, int64_t value) {
if (dtype == 1 || dtype == 2) {
uint16_t bits = 0;
if (dtype == 1) {
const int16_t signedValue = static_cast<int16_t>(value);
std::memcpy(&bits, &signedValue, sizeof(bits));
} else {
bits = static_cast<uint16_t>(value);
}
const size_t o = size_t(index) * 2u;
raw[o] = uint8_t(bits);
raw[o + 1] = uint8_t(bits >> 8);
return;
}
if (dtype == 3) {
raw[size_t(index)] = static_cast<uint8_t>(value);
} else {
const int8_t signedValue = static_cast<int8_t>(value);
uint8_t bits = 0;
std::memcpy(&bits, &signedValue, sizeof(bits));
raw[size_t(index)] = bits;
}
}
int64_t normalizeDirection(int64_t value, uint32_t n) {
if (value < 0) value += n;
else if (value >= int64_t(n)) value -= n;
return value;
}
class BitReader {
public:
explicit BitReader(std::span<const uint8_t> input_) : input(input_) {}
bool readBit(uint8_t& bit) {
if (bitPosition >= input.size() * 8u) return false;
bit = (input[bitPosition >> 3] >> (bitPosition & 7u)) & 1u;
++bitPosition;
return true;
}
bool readBits(uint8_t count, uint32_t& value) {
value = 0;
for (uint8_t bitIndex = 0; bitIndex < count; ++bitIndex) {
uint8_t bit = 0;
if (!readBit(bit)) return false;
value |= uint32_t(bit) << bitIndex;
}
return true;
}
size_t position() const { return bitPosition; }
private:
std::span<const uint8_t> input;
size_t bitPosition = 0;
};
struct PlaneDecode {
QbinDecodeErrorCode code = QbinDecodeErrorCode::Accepted;
std::string detail;
std::shared_ptr<const std::vector<uint8_t>> raw;
bool accepted() const {
return code == QbinDecodeErrorCode::Accepted && static_cast<bool>(raw);
}
};
PlaneDecode decodeNumericPlane(const Descriptor& d,
std::span<const uint8_t> encoded,
std::span<const uint8_t> validity,
uint32_t width,
uint32_t height) {
const uint64_t cells = uint64_t(width) * height;
const size_t bytesPerCell = (d.dtype == 1 || d.dtype == 2) ? 2u : 1u;
auto output = std::make_shared<std::vector<uint8_t>>(size_t(cells) * bytesPerCell, 0);
const bool direction = d.valueClass == 2;
const auto [minRaw, maxRaw] = dtypeRange(d.dtype);
const uint64_t maxAbsResidual = direction ? uint64_t(d.directionN / 2u)
: uint64_t(2) * uint64_t(maxRaw - minRaw);
const uint64_t maxZigZag = 2u * maxAbsResidual;
size_t cursor = 0;
for (uint32_t rowBegin = 0; rowBegin < height; rowBegin += QbinV3Decoder::BlockHeight) {
const uint32_t rowEnd = std::min(rowBegin + QbinV3Decoder::BlockHeight, height);
for (uint32_t colBegin = 0; colBegin < width; colBegin += QbinV3Decoder::BlockWidth) {
const uint32_t colEnd = std::min(colBegin + QbinV3Decoder::BlockWidth, width);
uint32_t symbolCount = 0;
for (uint32_t row = rowBegin; row < rowEnd; ++row) {
for (uint32_t col = colBegin; col < colEnd; ++col) {
if (validAt(validity, uint64_t(row) * width + col)) ++symbolCount;
}
}
if (symbolCount == 0) continue;
if (cursor >= encoded.size()) {
return {QbinDecodeErrorCode::MalformedRice, "missing riceK", {}};
}
const uint8_t riceK = encoded[cursor++];
if (riceK > 31) {
return {QbinDecodeErrorCode::MalformedRice, "riceK > 31", {}};
}
const uint64_t maxQ = maxZigZag >> riceK;
BitReader reader(encoded.subspan(cursor));
uint32_t decodedSymbols = 0;
for (uint32_t row = rowBegin; row < rowEnd; ++row) {
for (uint32_t col = colBegin; col < colEnd; ++col) {
const uint64_t index = uint64_t(row) * width + col;
if (!validAt(validity, index)) continue;
uint64_t q = 0;
while (true) {
uint8_t bit = 0;
if (!reader.readBit(bit)) {
return {QbinDecodeErrorCode::MalformedRice, "unterminated unary", {}};
}
if (bit != 0) break;
if (q >= maxQ) {
return {QbinDecodeErrorCode::ResidualOutOfRange, "rice quotient exceeds logical maximum", {}};
}
++q;
}
uint32_t remainder = 0;
if (!reader.readBits(riceK, remainder)) {
return {QbinDecodeErrorCode::MalformedRice, "truncated rice remainder", {}};
}
if (q > (std::numeric_limits<uint32_t>::max() >> riceK)) {
return {QbinDecodeErrorCode::ResidualOutOfRange, "rice symbol overflow", {}};
}
const uint64_t zigzag = (q << riceK) | remainder;
if (zigzag > maxZigZag || zigzag > std::numeric_limits<uint32_t>::max()) {
return {QbinDecodeErrorCode::ResidualOutOfRange, "zigzag exceeds logical maximum", {}};
}
const int64_t residual = (zigzag & 1u) ? -int64_t(zigzag / 2u) - 1
: int64_t(zigzag / 2u);
if (residual < -int64_t(maxAbsResidual) || residual > int64_t(maxAbsResidual)) {
return {QbinDecodeErrorCode::ResidualOutOfRange, "residual exceeds logical maximum", {}};
}
if (direction && (d.directionN % 2u == 0u) &&
residual == int64_t(d.directionN / 2u)) {
return {QbinDecodeErrorCode::ResidualOutOfRange, "non-canonical positive half-circle residual", {}};
}
const bool hasLeft = col > 0 && validAt(validity, index - 1);
const bool hasUp = row > 0 && validAt(validity, index - width);
const bool hasUpperLeft = row > 0 && col > 0 && validAt(validity, index - width - 1);
int64_t predicted = 0;
if (hasLeft && hasUp && hasUpperLeft) {
predicted = readRaw(*output, index - 1, d.dtype) +
readRaw(*output, index - width, d.dtype) -
readRaw(*output, index - width - 1, d.dtype);
if (direction) predicted = normalizeDirection(predicted, d.directionN);
} else if (hasLeft) {
predicted = readRaw(*output, index - 1, d.dtype);
} else if (hasUp) {
predicted = readRaw(*output, index - width, d.dtype);
}
int64_t reconstructed = predicted + residual;
if (direction) {
reconstructed = normalizeDirection(reconstructed, d.directionN);
if (reconstructed < 0 || reconstructed >= int64_t(d.directionN)) {
return {QbinDecodeErrorCode::InvalidCellPayload, "direction raw outside circular domain", {}};
}
} else if (reconstructed < minRaw || reconstructed > maxRaw) {
return {QbinDecodeErrorCode::InvalidCellPayload, "numeric raw outside dtype range", {}};
}
writeRaw(*output, index, d.dtype, reconstructed);
++decodedSymbols;
}
}
if (decodedSymbols != symbolCount) {
return {QbinDecodeErrorCode::MalformedRice, "block symbol count mismatch", {}};
}
const size_t remainderBits = reader.position() & 7u;
if (remainderBits != 0) {
const size_t paddingBits = 8u - remainderBits;
for (size_t i = 0; i < paddingBits; ++i) {
uint8_t bit = 0;
if (!reader.readBit(bit)) {
return {QbinDecodeErrorCode::MalformedRice, "truncated block padding", {}};
}
if (bit != 0) {
return {QbinDecodeErrorCode::MalformedRice, "nonzero block padding", {}};
}
}
}
const size_t blockBytes = reader.position() / 8u;
if (blockBytes > encoded.size() - cursor) {
return {QbinDecodeErrorCode::MalformedRice, "block exceeds plane", {}};
}
cursor += blockBytes;
}
}
if (cursor != encoded.size()) {
return {QbinDecodeErrorCode::MalformedRice, "extra encoded bytes after final block", {}};
}
return {QbinDecodeErrorCode::Accepted, "accepted", output};
}
} // namespace
QbinV3DecodeResult QbinV3Decoder::decode(const EncodedFieldRuntimeMetadata& meta,
std::span<const uint8_t> bytes) {
if (meta.fieldID.empty() || meta.generation == 0) {
return fail(QbinDecodeErrorCode::InvalidRuntimeMetadata, "fieldID/generation");
}
if (bytes.size() < HeaderSize) return fail(QbinDecodeErrorCode::InputTooSmall, "header");
if (bytes.size() > MaxBytes) return fail(QbinDecodeErrorCode::LimitExceeded, "file bytes");
if (std::memcmp(bytes.data(), "QBV3", 4) != 0) return fail(QbinDecodeErrorCode::BadMagic, "magic");
if (bytes[4] != 3) return fail(QbinDecodeErrorCode::UnsupportedVersion, "version");
if (bytes[5] != 1) return fail(QbinDecodeErrorCode::UnsupportedLayout, "layout");
if (bytes[8] != 1) return fail(QbinDecodeErrorCode::UnsupportedProjection, "projection");
if (bytes[9] != 1) return fail(QbinDecodeErrorCode::UnsupportedSampleOrder, "sampleOrder");
if (bytes[10] != 1) return fail(QbinDecodeErrorCode::UnsupportedGridRegistration, "gridRegistration");
if (bytes[11] != 0 || readU16(bytes, 14) != 0 || readU16(bytes, 82) != 0 ||
readU32(bytes, 92) != 0 || readU32(bytes, 96) != 0 || readU32(bytes, 100) != 0 ||
readU32(bytes, 104) != 0 || readU32(bytes, 108) != 0) {
return fail(QbinDecodeErrorCode::ReservedNonZero, "header reserved");
}
const uint16_t planeCount = readU16(bytes, 12);
const uint32_t width = readU32(bytes, 16);
const uint32_t height = readU32(bytes, 20);
const uint32_t payloadLength = readU32(bytes, 72);
if (planeCount == 0 || planeCount > MaxPlanes || width < 2 || height < 2) {
return fail(QbinDecodeErrorCode::InvalidHeader, "planes/grid");
}
const uint64_t cells = uint64_t(width) * uint64_t(height);
if (cells > MaxCells) return fail(QbinDecodeErrorCode::LimitExceeded, "cell count");
if (readU16(bytes, 80) != 0xffff || readU32(bytes, 84) != 0 || readU32(bytes, 88) != 0) {
return fail(QbinDecodeErrorCode::InvalidHeader, "FRAME tile");
}
const int64_t referenceTime = readI64(bytes, 24);
const int64_t validTime = readI64(bytes, 32);
constexpr uint64_t MaxSecondsForMs = std::numeric_limits<uint64_t>::max() / 1000u;
if (referenceTime <= 0 || validTime <= 0 ||
uint64_t(referenceTime) > MaxSecondsForMs || uint64_t(validTime) > MaxSecondsForMs) {
return fail(QbinDecodeErrorCode::InvalidTime, "UTC seconds");
}
const double west = readF64(bytes, 40);
const double south = readF64(bytes, 48);
const double east = readF64(bytes, 56);
const double north = readF64(bytes, 64);
for (const size_t offset : {size_t(40), size_t(48), size_t(56), size_t(64)}) {
if (readF64(bytes, offset) == 0.0 && readU64(bytes, offset) != 0) {
return fail(QbinDecodeErrorCode::InvalidBounds, "negative-zero bound");
}
}
if (!std::isfinite(west) || !std::isfinite(south) || !std::isfinite(east) || !std::isfinite(north) ||
west < -180.0 || east > 180.0 || west >= east ||
south < -85.0511287798066 || north > 85.0511287798066 || south >= north) {
return fail(QbinDecodeErrorCode::InvalidBounds, "Web Mercator bounds");
}
const size_t payloadOffset = HeaderSize + size_t(planeCount) * DescriptorSize;
if (payloadOffset > std::numeric_limits<size_t>::max() - payloadLength ||
bytes.size() != payloadOffset + payloadLength) {
return fail(QbinDecodeErrorCode::LengthMismatch, "total length");
}
std::vector<Descriptor> descriptors;
descriptors.reserve(planeCount);
std::set<std::pair<uint16_t, uint16_t>> keys;
std::tuple<uint16_t, uint16_t, uint8_t> previous{};
bool first = true;
int validityIndex = -1;
size_t expectedOffset = 0;
uint64_t decodedBytes = 0;
for (uint16_t i = 0; i < planeCount; ++i) {
const size_t o = HeaderSize + size_t(i) * DescriptorSize;
Descriptor d;
d.semantic = readU16(bytes, o);
d.dtype = bytes[o + 2];
d.flags = bytes[o + 3];
d.scaleBits = readU32(bytes, o + 4);
d.offsetBits = readU32(bytes, o + 8);
d.scale = readF32(bytes, o + 4);
d.offset = readF32(bytes, o + 8);
d.count = readU32(bytes, o + 12);
d.byteOffset = readU32(bytes, o + 16);
d.byteLength = readU32(bytes, o + 20);
d.unit = readU16(bytes, o + 24);
d.group = readU16(bytes, o + 26);
d.valueClass = bytes[o + 28];
d.component = bytes[o + 29];
if (readU16(bytes, o + 30) != 0 || (d.flags & ~1u) != 0) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "descriptor reserved/flags " + std::to_string(i));
}
const auto order = std::tuple(d.semantic, d.group, d.component);
if (!first && order <= previous) {
return fail(QbinDecodeErrorCode::NonCanonicalDescriptorOrder, "descriptor " + std::to_string(i));
}
first = false;
previous = order;
if (!keys.insert({d.semantic, d.group}).second) {
return fail(QbinDecodeErrorCode::DuplicatePlaneKey, "descriptor " + std::to_string(i));
}
if (d.semantic == 0 || d.valueClass > 3 || d.component > 3 || d.count != cells) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "descriptor values " + std::to_string(i));
}
const bool validitySemantic = d.semantic == ValiditySemantic;
const bool validityClass = d.valueClass == 3;
const bool bitset = d.dtype == 6;
if (validitySemantic != validityClass || validityClass != bitset) {
return fail(QbinDecodeErrorCode::InvalidValidity, "VALIDITY semantic/valueClass/dtype");
}
if (validityClass) {
if (validityIndex != -1 || d.unit != 0 || d.group != 0 || d.component != 0 ||
(d.flags & 1u) == 0 || d.scaleBits != 0x3f800000u || d.offsetBits != 0u) {
return fail(QbinDecodeErrorCode::InvalidValidity, "validity descriptor");
}
validityIndex = i;
} else {
if (d.dtype < 1 || d.dtype > 4 || !std::isfinite(d.scale) || d.scale <= 0.0f ||
!std::isfinite(d.offset) || (d.offset == 0.0f && d.offsetBits != 0u)) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "numeric descriptor " + std::to_string(i));
}
if (d.valueClass == 0 && d.component != 0) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "scalar component");
}
if (d.valueClass == 1 &&
(d.unit != 1 || d.component < 1 || d.component > 3 || d.group == 0)) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "velocity descriptor");
}
if (d.valueClass == 2) {
if (d.dtype != 2 || d.unit != 4 || d.component != 0 || d.group != 0 || d.offsetBits != 0u) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "direction descriptor");
}
const auto n = directionCount(d.scale);
if (!n) return fail(QbinDecodeErrorCode::InvalidDescriptor, "direction canonical scale");
d.directionN = *n;
}
}
if (d.byteOffset != expectedOffset || d.byteOffset > payloadLength ||
d.byteLength > payloadLength - d.byteOffset) {
return fail(d.byteOffset != expectedOffset ? QbinDecodeErrorCode::NonCanonicalPacking
: QbinDecodeErrorCode::InvalidDescriptor,
"descriptor packing " + std::to_string(i));
}
expectedOffset = align16(size_t(d.byteOffset) + d.byteLength);
const uint64_t logicalBytes = logicalDecodedBytes(d, cells);
if (logicalBytes == std::numeric_limits<uint64_t>::max() ||
decodedBytes > MaxDecoded - logicalBytes) {
return fail(QbinDecodeErrorCode::LimitExceeded, "decoded bytes");
}
decodedBytes += logicalBytes;
descriptors.push_back(d);
}
if (validityIndex < 0) return fail(QbinDecodeErrorCode::InvalidValidity, "missing validity");
if (expectedOffset != payloadLength) {
return fail(QbinDecodeErrorCode::NonCanonicalPacking, "payload length");
}
if (crc32(bytes) != readU32(bytes, 76)) {
return fail(QbinDecodeErrorCode::CrcMismatch, "file CRC32");
}
for (size_t i = 0; i < descriptors.size(); ++i) {
const auto& d = descriptors[i];
const size_t end = size_t(d.byteOffset) + d.byteLength;
const size_t next = i + 1 < descriptors.size() ? descriptors[i + 1].byteOffset : payloadLength;
if (next > end && !allZero(bytes.subspan(payloadOffset + end, next - end))) {
return fail(QbinDecodeErrorCode::NonZeroPadding, "plane/tail padding");
}
}
const auto& validityDescriptor = descriptors[size_t(validityIndex)];
const size_t expectedValidityBytes = size_t((cells + 7u) / 8u);
if (validityDescriptor.byteLength != expectedValidityBytes) {
return fail(QbinDecodeErrorCode::InvalidValidity, "validity byteLength");
}
const auto validity = bytes.subspan(payloadOffset + validityDescriptor.byteOffset,
validityDescriptor.byteLength);
if ((cells & 7u) != 0) {
const uint8_t unusedMask = uint8_t(0xffu << (cells & 7u));
if ((validity.back() & unusedMask) != 0) {
return fail(QbinDecodeErrorCode::InvalidValidity, "unused validity bits");
}
}
uint64_t validCount = 0;
for (uint64_t i = 0; i < cells; ++i) {
if (validAt(validity, i)) ++validCount;
}
for (size_t i = 0; i < descriptors.size(); ++i) {
if (int(i) == validityIndex) continue;
if ((validCount == 0 && descriptors[i].byteLength != 0) ||
(validCount != 0 && descriptors[i].byteLength == 0)) {
return fail(QbinDecodeErrorCode::InvalidDescriptor, "numeric byteLength vs validity");
}
}
auto output = std::make_shared<DecodedFieldFrame>();
output->frameContentIdentity = {
meta.fieldID,
meta.generation,
meta.dataVersion,
uint64_t(validTime) * 1000u,
};
output->referenceTimeMs = uint64_t(referenceTime) * 1000u;
output->grid = {1, 1, 1, 1, width, height, west, south, east, north};
for (size_t i = 0; i < descriptors.size(); ++i) {
const auto& d = descriptors[i];
std::shared_ptr<const std::vector<uint8_t>> raw;
if (int(i) == validityIndex) {
raw = std::make_shared<const std::vector<uint8_t>>(validity.begin(), validity.end());
} else if (validCount == 0) {
const size_t logicalBytes = size_t(logicalDecodedBytes(d, cells));
raw = std::make_shared<const std::vector<uint8_t>>(logicalBytes, 0);
} else {
const auto encoded = bytes.subspan(payloadOffset + d.byteOffset, d.byteLength);
const auto decoded = decodeNumericPlane(d, encoded, validity, width, height);
if (!decoded.accepted()) return fail(decoded.code, decoded.detail);
raw = decoded.raw;
}
DecodedFieldPlane plane{
{d.semantic, d.group, static_cast<FieldComponent>(d.component)},
static_cast<FieldDType>(d.dtype),
static_cast<FieldValueClass>(d.valueClass),
d.unit,
bool(d.flags & 1u),
d.count,
d.scale,
d.offset,
contentIdentity(d, *raw),
raw,
};
if (int(i) == validityIndex) {
output->validityContentIdentity = plane.contentIdentity;
output->validityBits = raw;
output->validityPlane = std::move(plane);
} else {
output->planes.push_back(std::move(plane));
}
}
if (!output->isStructurallyValid()) {
return fail(QbinDecodeErrorCode::InvalidFrame, "decoded frame structural validity");
}
return {QbinDecodeErrorCode::Accepted, "accepted", output};
}
} // namespace mln::particle::ingress

View File

@@ -0,0 +1,80 @@
#pragma once
#include <mln/renderer/retained_underlay.hpp>
#include <cstdint>
#include <string>
#include <vector>
namespace mln {
struct ContinuousRenderRanges {
bool explicitConfigured = false;
bool explicitValid = false;
int32_t layerCount = 0;
int32_t continuousBegin = 0;
int32_t continuousEndExclusive = 0;
uint64_t configRevision = 0;
bool retainedMode() const noexcept {
return explicitValid || (!explicitConfigured && continuousBegin > 0);
}
bool bottomPresent() const noexcept { return retainedMode() && continuousBegin > 0; }
bool topPresent() const noexcept {
return explicitValid && continuousEndExclusive < layerCount;
}
};
inline ContinuousRenderRanges selectContinuousRenderRanges(
const ContinuousLayerRangeResolution& explicitRange,
std::optional<int32_t> legacyBoundary,
int32_t layerCount) {
ContinuousRenderRanges ranges;
ranges.explicitConfigured = explicitRange.configured;
ranges.explicitValid = explicitRange.valid;
ranges.layerCount = layerCount;
ranges.continuousEndExclusive = layerCount;
ranges.configRevision = explicitRange.configRevision;
if (explicitRange.valid) {
ranges.continuousBegin = explicitRange.begin();
ranges.continuousEndExclusive = explicitRange.endExclusive();
} else if (!explicitRange.configured && legacyBoundary) {
ranges.continuousBegin = *legacyBoundary;
}
return ranges;
}
inline ContinuousLayerRangeResolution resolveContinuousLayerRange(
const ContinuousLayerRangeConfig& config,
const std::vector<std::string>& orderedLayerIds) {
ContinuousLayerRangeResolution result;
result.configured = config.configured;
result.startLayerId = config.startLayerId;
result.endLayerId = config.endLayerId;
result.configRevision = config.revision;
if (!config.configured) {
result.reason = "not-configured";
return result;
}
if (orderedLayerIds.empty()) {
result.reason = "style-not-ready";
return result;
}
for (std::size_t index = 0; index < orderedLayerIds.size(); ++index) {
if (orderedLayerIds[index] == config.startLayerId) result.startIndex = static_cast<int32_t>(index);
if (orderedLayerIds[index] == config.endLayerId) result.endIndex = static_cast<int32_t>(index);
}
if (result.startIndex < 0) {
result.reason = "start-layer-missing";
} else if (result.endIndex < 0) {
result.reason = "end-layer-missing";
} else if (result.startIndex > result.endIndex) {
result.reason = "range-order-invalid";
} else {
result.valid = true;
result.reason = "none";
}
return result;
}
} // namespace mln

View File

@@ -45,6 +45,7 @@ private:
#ifndef MLN_DRAWABLE_LOCATION_INDICATOR
void render(PaintParameters &) override;
bool usesLegacyRendering() const override { return true; }
#endif
void populateDynamicRenderFeatureIndex(DynamicFeatureIndex &) const override;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
#pragma once
#include <mln/renderer/layers/render_raster_particle_layer.hpp>
namespace mln {
// Validation styles reuse the formal GPU renderer and do not own another
// particle simulation.
using RenderParticleValidationLayer = RenderRasterParticleLayer;
} // namespace mln

View File

@@ -0,0 +1,52 @@
#pragma once
#include <mln/particle/field/offline_fixture_particle_adapter.hpp>
#include <mln/renderer/render_layer.hpp>
#include <mln/style/sources/raster_array_source.hpp>
namespace mln {
class ParticleGpuResources;
class RenderRasterParticleLayer final : public RenderLayer {
public:
explicit RenderRasterParticleLayer(Immutable<style::Layer::Impl>);
~RenderRasterParticleLayer() override;
void update(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const PaintParameters&,
const RenderTree&,
UniqueChangeRequestVec&) override;
bool needsHiddenLayerSourceCleanup(const RenderSource&) const override;
private:
void transition(const TransitionParameters&) override {}
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override { return false; }
bool hasCrossfade() const override { return false; }
bool needsRepaint() const override;
bool isParticleAnimationLayer() const override { return true; }
bool isParticleAnimationActive() const override;
bool startsRetainedDynamicRange() const override { return true; }
void prepare(const LayerPrepareParameters&) override;
void markLayerRenderable(bool, UniqueChangeRequestVec&) override;
void layerRemoved(UniqueChangeRequestVec&) override;
void markContextDestroyed() override;
std::shared_ptr<ParticleGpuResources> gpuResources;
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceField;
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceNextField;
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
std::shared_ptr<style::RasterArrayRendererCadence> sourceV4Cadence;
float sourceProjectionMix = 0.0f;
bool sourceUsesV4Projection = false;
uint64_t sourceTimelineWindowRevision = 0;
uint64_t sourceTimelineCadenceRevision = 0;
bool formalSource = false;
uint64_t renderLayerInstanceId = 0;
};
} // namespace mln

View File

@@ -0,0 +1,393 @@
#include <mln/renderer/layers/render_weather_color_layer.hpp>
#include <mln/renderer/layers/weather_color_texture_lifecycle.hpp>
#include <mln/gfx/color_mode.hpp>
#include <mln/gfx/context.hpp>
#include <mln/gfx/cull_face_mode.hpp>
#include <mln/gfx/drawable_builder.hpp>
#include <mln/gfx/drawable_tweaker.hpp>
#include <mln/gfx/shader_registry.hpp>
#include <mln/gfx/texture2d.hpp>
#include <mln/gfx/vertex_attribute.hpp>
#include <mln/particle/renderer_debug_counters.hpp>
#include <mln/renderer/buckets/heatmap_bucket.hpp>
#include <mln/renderer/layer_group.hpp>
#include <mln/renderer/render_static_data.hpp>
#include <mln/renderer/sources/render_raster_array_source.hpp>
#include <mln/shaders/particle_gpu_validation_ubo.hpp>
#include <mln/style/layers/weather_color_layer_impl.hpp>
#include <mln/util/image.hpp>
#include <mln/util/logging.hpp>
#include <mln/util/mat4.hpp>
#include <algorithm>
#include <cmath>
namespace mln {
namespace {
constexpr auto ShaderName = "ParticleGpuValidationShader";
double mercatorY(const double latitude) {
const auto radians = latitude * M_PI / 180.0;
return (1.0 - std::log(std::tan(radians) + 1.0 / std::cos(radians)) / M_PI) * 0.5;
}
std::array<float, 4> bounds(const particle::ParticleFieldSnapshot& field) {
return {static_cast<float>((field.west + 180.0) / 360.0),
static_cast<float>(mercatorY(field.north)),
static_cast<float>((field.east + 180.0) / 360.0),
static_cast<float>(mercatorY(field.south))};
}
bool supportsValueSource(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) {
return valueSource == style::FieldValueSource::Scalar ? field.hasScalar() : field.hasVector();
}
float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) {
float result = 1.0f;
for (size_t index = 0; index < field.valid.size(); ++index)
if (field.valid[index]) {
if (valueSource == style::FieldValueSource::Scalar) {
result = std::max(result, std::abs(field.scalar[index]));
} else {
result = std::max(result, std::abs(field.vector[index * 2]));
result = std::max(result, std::abs(field.vector[index * 2 + 1]));
}
}
return result;
}
std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot& field,
const float fieldScale,
style::FieldValueSource valueSource) {
auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
for (uint32_t y = 0; y < field.height; ++y)
for (uint32_t x = 0; x < field.width; ++x) {
const auto cell = static_cast<size_t>(y) * field.width + x;
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell]
: field.vector[cell * 2];
const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1];
result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) *
255.0f);
result->data[pixel + 2] = 0;
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
}
return result;
}
struct Resources {
gfx::ShaderProgramBasePtr shader;
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
SegmentVector segments;
particle::ParticleFieldSnapshot t0;
particle::ParticleFieldSnapshot t1;
std::shared_ptr<PremultipliedImage> image0;
std::shared_ptr<PremultipliedImage> image1;
gfx::Texture2DPtr texture0;
gfx::Texture2DPtr texture1;
float scale0 = 1.0f;
float scale1 = 1.0f;
style::FieldValueSource valueSource = style::FieldValueSource::VectorMagnitude;
float mix = 0.0f;
float opacity = 1.0f;
std::vector<style::WeatherColorStop> stops;
int16_t firstWrap = 0;
int16_t lastWrap = -1;
uint64_t drawableRebuildCount = 0;
};
class Tweaker final : public gfx::DrawableTweaker {
public:
Tweaker(std::shared_ptr<Resources> resources_, int16_t wrap_)
: resources(std::move(resources_)),
wrap(wrap_) {}
void init(gfx::Drawable&) override {}
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
shaders::ParticleGpuValidationUBO ubo = {
.pass = {5.0f, 0.0f, 0.0f, 0.0f},
.marker = {resources->mix, 0.0f, 0.0f, 0.0f},
.mask = {0, 0, 0, 0},
.visual = {resources->opacity, 0, 0, 0},
.fieldBounds = bounds(resources->t0),
.respawnBounds = bounds(resources->t0),
.fieldScale = {resources->scale0, resources->scale1, 0, 0},
// WeatherColor never performs particle advection. Keep the shared
// UBO's advection controls explicitly neutral for this color-only pass.
.advection = {0.0f, 1.0f, 0.00001f, 0.0f},
.particleColor = {0.08f, 0.55f, 1.0f, 1.0f},
.trailColor = {1.0f, 1.0f, 1.0f, 1.0f},
.weatherStops = {},
.weatherColors = {},
.weatherRamp = {},
.matrix = util::cast<float>(parameters.matrixForTile({wrap, {0, 0, 0}})),
};
ubo.weatherRamp[0] = static_cast<float>(resources->stops.size());
ubo.weatherRamp[1] = resources->valueSource == style::FieldValueSource::VectorMagnitude ? 1.0f : 0.0f;
for (size_t i = 0; i < resources->stops.size(); ++i) {
ubo.weatherStops[i][0] = resources->stops[i].value;
ubo.weatherColors[i] = resources->stops[i].color;
}
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
}
private:
std::shared_ptr<Resources> resources;
int16_t wrap;
};
bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) {
return particle::sameFieldGpuContent(a, b);
}
bool uploadField(gfx::Context& context,
const particle::ParticleFieldSnapshot& field,
style::FieldValueSource valueSource,
std::shared_ptr<PremultipliedImage>& outputImage,
gfx::Texture2DPtr& outputTexture,
float& outputScale) {
outputScale = scale(field, valueSource);
outputImage = image(field, outputScale, valueSource);
outputTexture = context.createTexture2D();
if (!outputTexture) return false;
outputTexture->setImage(outputImage);
outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear,
.wrapU = gfx::TextureWrapType::Clamp,
.wrapV = gfx::TextureWrapType::Clamp});
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload);
return true;
}
} // namespace
RenderWeatherColorLayer::RenderWeatherColorLayer(Immutable<style::Layer::Impl> impl_)
: RenderLayer(makeMutable<style::WeatherColorLayerProperties>(
impl_, static_cast<const style::WeatherColorLayer::Impl&>(*impl_).opacity)) {}
RenderWeatherColorLayer::~RenderWeatherColorLayer() = default;
void RenderWeatherColorLayer::evaluate(const PropertyEvaluationParameters&) {
auto properties = makeMutable<style::WeatherColorLayerProperties>(
baseImpl, static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity);
passes = RenderPass::Translucent;
properties->renderPasses = mln::underlying_type(passes);
evaluatedProperties = std::move(properties);
}
bool RenderWeatherColorLayer::needsRepaint() const {
if (!isLayerRenderable() || !sourceField || !sourceNextField) return false;
const bool repaint = sourceTimeline && sourceTimeline->frame().state == style::RasterArrayPlaybackState::Playing &&
sourceTimeline->cadenceDue(std::chrono::steady_clock::now());
if (repaint) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::WeatherColorNeedsRepaint);
return repaint;
}
bool RenderWeatherColorLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const {
if (!resources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false;
const auto& source = static_cast<const RenderRasterArraySource&>(renderSource);
const auto& impl = source.impl();
return impl.hostFieldSubmission && !impl.field && !impl.nextField && !impl.timeline;
}
void RenderWeatherColorLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) {
RenderLayer::markLayerRenderable(willRender, changes);
}
void RenderWeatherColorLayer::layerRemoved(UniqueChangeRequestVec& changes) {
resources.reset();
RenderLayer::layerRemoved(changes);
}
void RenderWeatherColorLayer::markContextDestroyed() {
resources.reset();
}
void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters) {
if (!parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) return;
const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source);
sourceUsesV4Projection = false;
sourceProjectionUsesVectorMagnitude = false;
sourceProjectionMix = 0.0f;
if (source.impl().v4RendererState.color) {
const auto& projection = *source.impl().v4RendererState.color;
sourceField = projection.t0;
sourceNextField = projection.t1;
sourceProjectionMix = projection.sampleState.mix;
sourceTimeline.reset();
sourceUsesV4Projection = true;
sourceProjectionUsesVectorMagnitude = projection.valueSource ==
particle::MaterializedColorValueSource::VectorMagnitude;
return;
}
sourceTimeline = source.impl().timeline;
if (sourceTimeline) {
const auto frame = sourceTimeline->frame();
sourceField = frame.t0;
sourceNextField = frame.t1;
} else if (source.impl().field) {
sourceField = std::make_shared<const particle::ParticleFieldSnapshot>(*source.impl().field);
sourceNextField = source.impl().nextField
? std::make_shared<const particle::ParticleFieldSnapshot>(*source.impl().nextField)
: sourceField;
} else {
sourceField.reset();
sourceNextField.reset();
}
}
void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders,
gfx::Context& context,
const TransformState& transformState,
const std::shared_ptr<UpdateParameters>&,
const PaintParameters&,
const RenderTree&,
UniqueChangeRequestVec& changes) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::WeatherColorUpdate);
if (!layerGroup)
if (auto group = context.createLayerGroup(layerIndex, 1, getID())) setLayerGroup(std::move(group), changes);
if (!sourceField || !sourceNextField) {
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
if (resources)
Log::Info(
Event::Render,
"WeatherRuntime: event=WEATHER_COLOR_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID());
resources.reset();
return;
}
// Encoded ingress is decoder-only. After materialization, WeatherColor
// consumes the same canonical scalar/vector semantics as the legacy path.
const auto valueSource = sourceUsesV4Projection
? (sourceProjectionUsesVectorMagnitude ? style::FieldValueSource::VectorMagnitude
: style::FieldValueSource::Scalar)
: static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
resources.reset();
return;
}
if (!layerGroup || !isLayerRenderable()) return;
style::RasterArrayTimelineFrame timelineFrame;
if (sourceTimeline) {
timelineFrame = sourceTimeline->frame();
if (timelineFrame.state == style::RasterArrayPlaybackState::Playing) {
timelineFrame = sourceTimeline->advanceIfDue(std::chrono::steady_clock::now()).frame;
}
sourceField = timelineFrame.t0;
sourceNextField = timelineFrame.t1;
}
auto state = std::static_pointer_cast<Resources>(resources);
const auto previousTexture0 = state ? state->texture0.get() : nullptr;
const auto previousTexture1 = state ? state->texture1.get() : nullptr;
if (!state) {
state = std::make_shared<Resources>();
state->shader = context.getGenericShader(shaders, ShaderName);
state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
RenderStaticData::heatmapTextureVertices());
state->segments = RenderStaticData::heatmapTextureSegments();
state->valueSource = valueSource;
if (!state->shader || !uploadField(context, *sourceField, valueSource, state->image0, state->texture0,
state->scale0))
return;
if (particle::sameFieldGpuEndpoint(*sourceField, *sourceNextField)) {
state->image1 = state->image0;
state->texture1 = state->texture0;
state->scale1 = state->scale0;
} else if (!uploadField(context, *sourceNextField, valueSource, state->image1, state->texture1, state->scale1)) {
return;
}
state->t0 = *sourceField;
state->t1 = *sourceNextField;
resources = state;
Log::Info(
Event::Render,
"WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID +
" valueSource=" + (valueSource == style::FieldValueSource::Scalar ? "SCALAR" : "VECTOR_MAGNITUDE"));
} else if (state->valueSource != valueSource || !same(state->t0, *sourceField) ||
!same(state->t1, *sourceNextField)) {
const auto old0 = state->t0;
const auto old1 = state->t1;
auto install = [&](const particle::ParticleFieldSnapshot& wanted,
particle::ParticleFieldSnapshot& assigned,
std::shared_ptr<PremultipliedImage>& outputImage,
gfx::Texture2DPtr& outputTexture,
float& outputScale) {
// The decoded bundle may be identical while its selected GPU
// representation changes (scalar versus vector magnitude).
if (state->valueSource == valueSource && same(old0, wanted)) {
assigned = wanted;
outputImage = state->image0;
outputTexture = state->texture0;
outputScale = state->scale0;
return true;
}
if (state->valueSource == valueSource && same(old1, wanted)) {
assigned = wanted;
outputImage = state->image1;
outputTexture = state->texture1;
outputScale = state->scale1;
return true;
}
assigned = wanted;
return uploadField(context, assigned, valueSource, outputImage, outputTexture, outputScale);
};
particle::ParticleFieldSnapshot t0, t1;
std::shared_ptr<PremultipliedImage> i0, i1;
gfx::Texture2DPtr x0, x1;
float s0 = 1, s1 = 1;
if (!install(*sourceField, t0, i0, x0, s0) || !install(*sourceNextField, t1, i1, x1, s1)) return;
state->t0 = std::move(t0);
state->t1 = std::move(t1);
state->image0 = std::move(i0);
state->image1 = std::move(i1);
state->texture0 = std::move(x0);
state->texture1 = std::move(x1);
state->scale0 = s0;
state->scale1 = s1;
state->valueSource = valueSource;
Log::Info(
Event::Render,
"WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID);
}
const float mix = sourceTimeline ? timelineFrame.mix : (sourceUsesV4Projection ? sourceProjectionMix : 0.0f);
const float opacity = static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity;
state->mix = mix;
state->opacity = opacity;
state->stops = static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops;
auto& group = static_cast<LayerGroup&>(*layerGroup);
const auto center = transformState.getLatLng(LatLng::Unwrapped);
const auto centerWrap = static_cast<int16_t>(std::floor((center.longitude() + 180.0) / 360.0));
const int16_t firstWrap = centerWrap - 1;
const int16_t lastWrap = centerWrap + 1;
const bool wrapsChanged = state->firstWrap != firstWrap || state->lastWrap != lastWrap;
if (!weatherColorDrawableBindingsChanged(previousTexture0,
previousTexture1,
state->texture0.get(),
state->texture1.get(),
wrapsChanged))
return;
group.clearDrawables();
state->firstWrap = firstWrap;
state->lastWrap = lastWrap;
++state->drawableRebuildCount;
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::WeatherColorDrawableRebuild);
for (int16_t wrap = firstWrap; wrap <= lastWrap; ++wrap) {
auto attributes = context.createVertexAttributeArray();
if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute))
attribute->setSharedRawData(state->vertices,
offsetof(HeatmapTextureLayoutVertex, a1),
0,
sizeof(HeatmapTextureLayoutVertex),
gfx::AttributeDataType::Short2);
auto builder = context.createDrawableBuilder(getID() + "-weather-color-" + std::to_string(wrap));
builder->setShader(state->shader);
builder->setRenderPass(RenderPass::Translucent);
builder->setEnableDepth(false);
builder->setColorMode(gfx::ColorMode::alphaBlended());
builder->setCullFaceMode(gfx::CullFaceMode::disabled());
builder->setVertexAttributes(std::move(attributes));
builder->setRawVertices({}, state->vertices->elements(), gfx::AttributeDataType::Short2);
builder->setSegments(gfx::Triangles(),
RenderStaticData::quadTriangleIndices().vector(),
state->segments.data(),
state->segments.size());
builder->setTexture(state->texture0, shaders::idParticleGpuValidationPreviousTexture);
builder->setTexture(state->texture0, shaders::idParticleGpuValidationFieldTexture);
builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture);
builder->addTweaker(std::make_shared<Tweaker>(state, wrap));
builder->flush(context);
auto drawables = builder->clearDrawables();
if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front()));
}
}
} // namespace mln

View File

@@ -0,0 +1,42 @@
#pragma once
#include <mln/renderer/render_layer.hpp>
#include <mln/style/sources/raster_array_source.hpp>
namespace mln {
class RenderWeatherColorLayer final : public RenderLayer {
public:
explicit RenderWeatherColorLayer(Immutable<style::Layer::Impl>);
~RenderWeatherColorLayer() override;
void update(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const PaintParameters&,
const RenderTree&,
UniqueChangeRequestVec&) override;
bool needsHiddenLayerSourceCleanup(const RenderSource&) const override;
private:
void transition(const TransitionParameters&) override {}
void evaluate(const PropertyEvaluationParameters&) override;
bool hasTransition() const override { return false; }
bool hasCrossfade() const override { return false; }
bool needsRepaint() const override;
bool startsRetainedDynamicRange() const override { return true; }
void prepare(const LayerPrepareParameters&) override;
void markLayerRenderable(bool, UniqueChangeRequestVec&) override;
void layerRemoved(UniqueChangeRequestVec&) override;
void markContextDestroyed() override;
std::shared_ptr<void> resources;
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceField;
std::shared_ptr<const particle::ParticleFieldSnapshot> sourceNextField;
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
float sourceProjectionMix = 0.0f;
bool sourceUsesV4Projection = false;
bool sourceProjectionUsesVectorMagnitude = false;
};
} // namespace mln

View File

@@ -110,6 +110,14 @@ void PaintParameters::updateStencilBufferAvailability() {
}
#endif
void PaintParameters::beginRenderTarget() {
nextStencilID = 1;
tileClippingMaskIDs.clear();
#if MLN_RENDER_BACKEND_OPENGL
updateStencilBufferAvailability();
#endif
}
mat4 PaintParameters::matrixForTile(const UnwrappedTileID& tileID, bool aligned) const {
mat4 matrix;
state.matrixFor(matrix, tileID);

View File

@@ -114,6 +114,9 @@ public:
/// @return True when clipping mask state is ready or no update was needed.
bool renderTileClippingMasks(const RenderTiles&);
/// A newly bound render target has no tile masks from the previous target.
void beginRenderTarget();
/// Clear the stencil buffer, even if there are no tile masks (for 3D)
void clearStencil();

View File

@@ -0,0 +1,141 @@
#pragma once
#include <cstdint>
#include <memory>
namespace mln {
enum class ParticleFastPathMissReason : uint8_t {
None,
NoPreparedTree,
RetainedDisabled,
CacheInvalid,
RevisionMismatch,
ContentMutation,
CameraChanged,
ViewportChanged,
UpdateParametersChanged,
StyleNotLoaded,
BoundaryChanged,
TopologyChanged,
StaticUnstable,
PlacementOrFade,
PendingChanges,
NonParticleTransition,
NonParticleRepaint,
FadingTiles,
NoParticleRepaint,
StillImageRequest,
UnsupportedMode,
DebugOptions,
UnsupportedContinuousLayer,
UnsupportedScene,
AnnotationMutation,
};
inline const char* particleFastPathMissReasonText(ParticleFastPathMissReason reason) noexcept {
switch (reason) {
case ParticleFastPathMissReason::None: return "none";
case ParticleFastPathMissReason::NoPreparedTree: return "no-prepared-tree";
case ParticleFastPathMissReason::RetainedDisabled: return "retained-disabled";
case ParticleFastPathMissReason::CacheInvalid: return "cache-invalid";
case ParticleFastPathMissReason::RevisionMismatch: return "revision-mismatch";
case ParticleFastPathMissReason::ContentMutation: return "content-mutation";
case ParticleFastPathMissReason::CameraChanged: return "camera-changed";
case ParticleFastPathMissReason::ViewportChanged: return "viewport-changed";
case ParticleFastPathMissReason::UpdateParametersChanged: return "update-parameters-changed";
case ParticleFastPathMissReason::StyleNotLoaded: return "style-not-loaded";
case ParticleFastPathMissReason::BoundaryChanged: return "boundary-changed";
case ParticleFastPathMissReason::TopologyChanged: return "topology-changed";
case ParticleFastPathMissReason::StaticUnstable: return "static-unstable";
case ParticleFastPathMissReason::PlacementOrFade: return "placement-or-fade";
case ParticleFastPathMissReason::PendingChanges: return "pending-changes";
case ParticleFastPathMissReason::NonParticleTransition: return "non-particle-transition";
case ParticleFastPathMissReason::NonParticleRepaint: return "non-particle-repaint";
case ParticleFastPathMissReason::FadingTiles: return "fading-tiles";
case ParticleFastPathMissReason::NoParticleRepaint: return "no-particle-repaint";
case ParticleFastPathMissReason::StillImageRequest: return "still-image-request";
case ParticleFastPathMissReason::UnsupportedMode: return "unsupported-mode";
case ParticleFastPathMissReason::DebugOptions: return "debug-options";
case ParticleFastPathMissReason::UnsupportedContinuousLayer: return "unsupported-continuous-layer";
case ParticleFastPathMissReason::UnsupportedScene: return "unsupported-scene";
case ParticleFastPathMissReason::AnnotationMutation: return "annotation-mutation";
}
return "unknown";
}
struct ParticleFastPathGateInput {
bool hasPreparedTree = false;
bool retainedEnabled = false;
bool cacheValid = false;
bool revisionMatches = false;
bool contentRevisionMatches = false;
bool cameraMatches = false;
bool viewportMatches = false;
bool updateParametersMatch = false;
bool styleLoaded = false;
bool boundaryMatches = false;
bool topologyMatches = false;
bool staticStable = false;
bool hasPlacementTransition = false;
bool hasPendingChanges = false;
bool hasNonParticleTransition = false;
bool hasNonParticleRepaint = false;
bool hasFadingTiles = false;
bool particleNeedsRepaint = false;
bool stillImageRequest = false;
bool continuousMode = false;
bool debugDisabled = false;
bool unsupportedContinuousLayer = false;
bool supportedScene = false;
bool annotationMutation = false;
};
struct ParticleFastPathDecision {
bool candidate = false;
ParticleFastPathMissReason reason = ParticleFastPathMissReason::None;
};
inline ParticleFastPathDecision evaluateParticleFastPath(const ParticleFastPathGateInput& input) noexcept {
if (!input.hasPreparedTree) return {false, ParticleFastPathMissReason::NoPreparedTree};
if (!input.retainedEnabled) return {false, ParticleFastPathMissReason::RetainedDisabled};
if (!input.cacheValid) return {false, ParticleFastPathMissReason::CacheInvalid};
if (!input.revisionMatches) return {false, ParticleFastPathMissReason::RevisionMismatch};
if (!input.contentRevisionMatches) return {false, ParticleFastPathMissReason::ContentMutation};
if (!input.viewportMatches) return {false, ParticleFastPathMissReason::ViewportChanged};
if (!input.cameraMatches) return {false, ParticleFastPathMissReason::CameraChanged};
if (!input.updateParametersMatch) return {false, ParticleFastPathMissReason::UpdateParametersChanged};
if (!input.styleLoaded) return {false, ParticleFastPathMissReason::StyleNotLoaded};
if (!input.boundaryMatches) return {false, ParticleFastPathMissReason::BoundaryChanged};
if (!input.topologyMatches) return {false, ParticleFastPathMissReason::TopologyChanged};
if (!input.staticStable) return {false, ParticleFastPathMissReason::StaticUnstable};
if (input.hasPlacementTransition) return {false, ParticleFastPathMissReason::PlacementOrFade};
if (input.hasPendingChanges) return {false, ParticleFastPathMissReason::PendingChanges};
if (input.hasNonParticleTransition) return {false, ParticleFastPathMissReason::NonParticleTransition};
if (input.hasNonParticleRepaint) return {false, ParticleFastPathMissReason::NonParticleRepaint};
if (input.hasFadingTiles) return {false, ParticleFastPathMissReason::FadingTiles};
if (!input.particleNeedsRepaint) return {false, ParticleFastPathMissReason::NoParticleRepaint};
if (input.stillImageRequest) return {false, ParticleFastPathMissReason::StillImageRequest};
if (!input.continuousMode) return {false, ParticleFastPathMissReason::UnsupportedMode};
if (!input.debugDisabled) return {false, ParticleFastPathMissReason::DebugOptions};
if (input.unsupportedContinuousLayer) {
return {false, ParticleFastPathMissReason::UnsupportedContinuousLayer};
}
if (!input.supportedScene) return {false, ParticleFastPathMissReason::UnsupportedScene};
if (input.annotationMutation) return {false, ParticleFastPathMissReason::AnnotationMutation};
return {true, ParticleFastPathMissReason::None};
}
template <class T>
class ParticleFastPathPreparedTree {
public:
T* get() const noexcept { return tree.get(); }
bool empty() const noexcept { return !tree; }
void replace(std::unique_ptr<T> next) { tree = std::move(next); }
void reset() { tree.reset(); }
private:
std::unique_ptr<T> tree;
};
} // namespace mln

View File

@@ -0,0 +1,59 @@
#pragma once
#include <cstdint>
namespace mln {
// Ignore sub-step zoom noise while remaining far below a deliberate pinch or
// animated zoom level change.
constexpr double ParticleViewportReseedZoomEpsilon = 0.05;
enum class ParticleViewportReseedAction : uint8_t {
None,
Reseed,
SkipInvalidDomain,
};
struct ParticleViewportReseedDecision {
ParticleViewportReseedAction action = ParticleViewportReseedAction::None;
double startZoom = 0.0;
double endZoom = 0.0;
};
class ParticleViewportReseedGesture {
public:
void begin(double zoom) noexcept {
if (!active) {
active = true;
startZoom = zoom;
}
}
void cancel() noexcept { active = false; }
bool isActive() const noexcept { return active; }
double getStartZoom() const noexcept { return startZoom; }
ParticleViewportReseedDecision finish(double endZoom, bool viewportDomainValid) noexcept {
if (!active) {
return {};
}
active = false;
const auto gestureStartZoom = startZoom;
if (endZoom >= gestureStartZoom - ParticleViewportReseedZoomEpsilon) {
return {ParticleViewportReseedAction::None, gestureStartZoom, endZoom};
}
return {viewportDomainValid ? ParticleViewportReseedAction::Reseed
: ParticleViewportReseedAction::SkipInvalidDomain,
gestureStartZoom,
endZoom};
}
private:
bool active = false;
double startZoom = 0.0;
};
} // namespace mln

View File

@@ -106,6 +106,27 @@ public:
// Returns true if the layer has a pattern property and is actively crossfading.
virtual bool hasCrossfade() const = 0;
// Returns true when an active animated layer needs another MapLibre frame.
virtual bool needsRepaint() const { return false; }
// Renderer-internal classification used by the retained-underlay CPU fast
// path. It is intentionally narrower than startsRetainedDynamicRange():
// WeatherColor starts the dynamic range but is not a Particle animation.
virtual bool isParticleAnimationLayer() const { return false; }
virtual bool isParticleAnimationActive() const { return false; }
// Renderer-internal boundary for retained static-underlay rendering. This is
// deliberately not exposed through the style or Host APIs.
virtual bool startsRetainedDynamicRange() const { return false; }
// Only layers that still issue commands through RenderLayer::render() need
// legacy ordering protection in the retained-underlay path.
virtual bool usesLegacyRendering() const { return false; }
// Exceptional source-driven cleanup for a hidden layer. The default keeps
// normal MapLibre hidden-layer lifecycle unchanged.
virtual bool needsHiddenLayerSourceCleanup(const RenderSource&) const { return false; }
// Returns true if layer writes to depth buffer by drawing using PaintParameters::depthModeFor3D().
virtual bool is3D() const { return false; }

View File

@@ -1,4 +1,5 @@
#include <mln/renderer/render_orchestrator.hpp>
#include <mln/renderer/continuous_layer_range.hpp>
#include <mln/annotation/annotation_manager.hpp>
#include <mln/layermanager/layer_manager.hpp>
@@ -7,6 +8,7 @@
#include <mln/renderer/render_source.hpp>
#include <mln/renderer/render_layer.hpp>
#include <mln/renderer/render_static_data.hpp>
#include <mln/renderer/retained_underlay_cache.hpp>
#include <mln/renderer/render_tree.hpp>
#include <mln/renderer/update_parameters.hpp>
#include <mln/renderer/upload_parameters.hpp>
@@ -30,6 +32,7 @@
#include <mln/util/logging.hpp>
#include <algorithm>
#include <string_view>
namespace mln {
@@ -62,6 +65,51 @@ RendererObserver& nullObserver() {
return observer;
}
const char* sourceTypeText(style::SourceType type) noexcept {
switch (type) {
case style::SourceType::Vector: return "vector";
case style::SourceType::Raster: return "raster";
case style::SourceType::RasterDEM: return "raster-dem";
case style::SourceType::GeoJSON: return "geojson";
case style::SourceType::Video: return "video";
case style::SourceType::Annotations: return "annotations";
case style::SourceType::Image: return "image";
case style::SourceType::CustomVector: return "customvector";
case style::SourceType::CustomMVTVector: return "unknown";
case style::SourceType::RasterArray: return "raster-array";
}
return "unknown";
}
const char* tileOperationText(TileOperation op) noexcept {
switch (op) {
case TileOperation::RequestedFromCache: return "RequestedFromCache";
case TileOperation::RequestedFromNetwork: return "RequestedFromNetwork";
case TileOperation::LoadFromNetwork: return "LoadFromNetwork";
case TileOperation::LoadFromCache: return "LoadFromCache";
case TileOperation::StartParse: return "StartParse";
case TileOperation::EndParse: return "EndParse";
case TileOperation::Error: return "Error";
case TileOperation::Cancelled: return "Cancelled";
case TileOperation::NullOp: return "NullOp";
}
return "Unknown";
}
const char* readinessEventType(RetainedUnderlayReason reason) noexcept {
switch (reason) {
case RetainedUnderlayReason::SourceMutation: return "SourceMutation";
case RetainedUnderlayReason::TileMutation: return "TileMutation";
case RetainedUnderlayReason::ImageMutation: return "ImageMutation";
case RetainedUnderlayReason::GlyphMutation: return "GlyphMutation";
case RetainedUnderlayReason::StyleMutation: return "StyleMutation";
case RetainedUnderlayReason::LayerMutation:
case RetainedUnderlayReason::LayerGroupMutation: return "LayerMutation";
case RetainedUnderlayReason::ContextReset: return "ContextReset";
default: return "OtherMutation";
}
}
class RenderTreeImpl final : public RenderTree {
public:
RenderTreeImpl(std::unique_ptr<RenderTreeParameters> parameters_,
@@ -144,10 +192,139 @@ RenderOrchestrator::~RenderOrchestrator() {
}
}
// Wait for any deferred cleanup tasks to complete before releasing and potentially
// destroying the scheduler. Those cleanup tasks must not hold the final reference
// to the scheduler because it cannot be destroyed from one of its own pool threads.
// Finish work that can still reach the source observers before detaching them.
threadPool.waitForEmpty();
for (auto& entry : renderSources) {
entry.second->setObserver(nullptr);
}
glyphManager->setObserver(nullptr);
imageManager->setObserver(nullptr);
// Tile destructors can synchronously report Cancelled and enqueue further work.
// Destroy sources while the readiness state and its mutex are still alive.
renderSources.clear();
// Drain work submitted by source/tile destruction before the scheduler and
// the remaining orchestrator state are torn down.
threadPool.waitForEmpty();
}
void RenderOrchestrator::recordReadinessMutation(const char* eventType,
RetainedUnderlayReason reason,
const std::optional<std::string>& sourceId,
const std::string& tileAction) noexcept {
try {
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
const auto sequence = ++readinessEventSequence;
lastReadinessMutation.sequence = sequence;
lastReadinessMutation.contentRevision = retainedUnderlayContentRevision.load(std::memory_order_acquire);
lastReadinessMutation.eventType = eventType;
lastReadinessMutation.reason = retainedUnderlayReasonText(reason);
lastReadinessMutation.sourceId = sourceId;
lastReadinessMutation.affectedSegmentIndices.clear();
lastReadinessMutation.dependencyScope = sourceId ? "unresolved-until-snapshot" : "shared/global/unknown";
if (sourceId) {
auto& state = sourceReadinessEvents[*sourceId];
state.lastMutationReason = retainedUnderlayReasonText(reason);
if (reason == RetainedUnderlayReason::SourceMutation) state.lastSourceMutationSequence = sequence;
if (reason == RetainedUnderlayReason::TileMutation) state.lastTileChangedSequence = sequence;
if (!tileAction.empty()) {
state.lastTileActionSequence = sequence;
state.lastTileAction = tileAction;
}
}
} catch (...) {
// Diagnostic recording is best-effort and must not escape into renderer mutation paths.
}
}
void RenderOrchestrator::advanceRetainedSourceRevision(RetainedUnderlayReason reason,
const std::string& sourceId) noexcept {
if (sourceId.empty()) {
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
retainedSegmentRevisions.advanceGlobal();
return;
}
try {
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
retainedSegmentRevisions.advanceSource(sourceId);
} catch (...) {
// Correctness state must fail closed if a source revision cannot be recorded.
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
retainedSegmentRevisions.advanceGlobal();
}
recordReadinessMutation(readinessEventType(reason), reason, sourceId);
}
void RenderOrchestrator::advanceRetainedUnderlayContentRevision(
RetainedUnderlayReason reason,
const std::optional<std::string>& sourceId) noexcept {
retainedUnderlayContentReason.store(reason, std::memory_order_release);
retainedUnderlayContentRevision.fetch_add(1, std::memory_order_acq_rel);
if (sourceId && (reason == RetainedUnderlayReason::SourceMutation ||
reason == RetainedUnderlayReason::TileMutation)) {
advanceRetainedSourceRevision(reason, *sourceId);
return;
}
{
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
retainedSegmentRevisions.advanceGlobal();
}
recordReadinessMutation(readinessEventType(reason), reason, sourceId);
}
RendererReadinessDiagnostics RenderOrchestrator::buildReadinessDiagnostics(bool styleLoaded,
bool spriteLoaded) const {
RendererReadinessDiagnostics result;
result.styleLoaded = styleLoaded;
result.imageManagerLoaded = imageManager->isLoaded();
result.spriteLoaded = spriteLoaded;
result.contentRevision = retainedUnderlayContentRevision.load(std::memory_order_acquire);
std::unordered_map<std::string, SourceReadinessEventState> sourceEvents;
{
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
// Snapshot-only: do not prune or otherwise mutate diagnostic history from a query path.
// Entries for sources no longer present are naturally ignored below because we enumerate
// the current renderSources set and only look up matching ids.
sourceEvents = sourceReadinessEvents;
result.lastMutation = lastReadinessMutation;
}
result.renderOrchestratorLoaded = result.imageManagerLoaded;
result.sources.reserve(renderSources.size());
for (const auto& [sourceId, renderSource] : renderSources) {
const auto stats = renderSource->getReadinessDiagnostics();
RenderSourceReadinessDiagnostics source;
source.sourceId = sourceId;
source.sourceType = sourceTypeText(renderSource->baseImpl->type);
source.enabled = renderSource->isEnabled();
source.isLoaded = renderSource->isLoaded();
source.tileBacked = stats.tileBacked;
source.tileCount = stats.tileCount;
source.incompleteTileCount = stats.incompleteTileCount;
source.loadedTileCount = stats.loadedTileCount;
source.hasFadingTiles = stats.hasFadingTiles;
result.hasFadingTiles = result.hasFadingTiles || stats.hasFadingTiles;
result.renderOrchestratorLoaded = result.renderOrchestratorLoaded && source.isLoaded;
if (const auto it = sourceEvents.find(sourceId); it != sourceEvents.end()) {
source.lastSourceMutationSequence = it->second.lastSourceMutationSequence;
source.lastTileChangedSequence = it->second.lastTileChangedSequence;
source.lastTileActionSequence = it->second.lastTileActionSequence;
source.lastTileAction = it->second.lastTileAction;
source.lastMutationReason = it->second.lastMutationReason;
}
result.sources.push_back(std::move(source));
}
std::ranges::sort(result.sources, {}, &RenderSourceReadinessDiagnostics::sourceId);
return result;
}
void RenderOrchestrator::markContextLost() {
contextLost = true;
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::ContextReset);
}
void RenderOrchestrator::setObserver(RendererObserver* observer_) {
@@ -160,6 +337,20 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
const auto startTime = util::MonotonicTimer::now().count();
const auto fontFacesIdentity = static_cast<const void*>(updateParameters->fontFaces.get());
if (retainedInputStateInitialized &&
(retainedGlyphURL != updateParameters->glyphURL ||
retainedFontFacesIdentity != fontFacesIdentity)) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::GlyphMutation);
}
if (retainedInputStateInitialized && retainedSpriteLoaded != updateParameters->spriteLoaded) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::ImageMutation);
}
retainedInputStateInitialized = true;
retainedGlyphURL = updateParameters->glyphURL;
retainedFontFacesIdentity = fontFacesIdentity;
retainedSpriteLoaded = updateParameters->spriteLoaded;
const bool isMapModeContinuous = updateParameters->mode == MapMode::Continuous;
if (!isMapModeContinuous) {
// Reset zoom history state.
@@ -210,6 +401,7 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
const bool lightChanged = renderLight.impl != updateParameters->light;
if (lightChanged) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::StyleMutation);
renderLight.impl = updateParameters->light;
renderLight.transition(transitionParameters);
}
@@ -219,6 +411,9 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
}
const ImageDifference imageDiff = diffImages(imageImpls, updateParameters->images);
if (!imageDiff.added.empty() || !imageDiff.removed.empty() || !imageDiff.changed.empty()) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::ImageMutation);
}
imageImpls = updateParameters->images;
// Only trigger tile reparse for changed images. Changed images only need a
@@ -248,6 +443,8 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
imageManager->setLoaded(updateParameters->spriteLoaded);
const LayerDifference layerDiff = diffLayers(layerImpls, updateParameters->layers);
// Layer mutations are tracked by per-segment immutable-Impl fingerprints below. Source,
// tile, image, glyph and camera changes remain deliberately global invalidations.
layerImpls = updateParameters->layers;
const bool layersAddedOrRemoved = !layerDiff.added.empty() || !layerDiff.removed.empty();
@@ -326,6 +523,19 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
}
const SourceDifference sourceDiff = diffSources(sourceImpls, updateParameters->sources);
if (!sourceDiff.added.empty() || !sourceDiff.removed.empty() || !sourceDiff.changed.empty()) {
retainedUnderlayContentReason.store(RetainedUnderlayReason::SourceMutation, std::memory_order_release);
retainedUnderlayContentRevision.fetch_add(1, std::memory_order_acq_rel);
for (const auto& entry : sourceDiff.added) {
advanceRetainedSourceRevision(RetainedUnderlayReason::SourceMutation, entry.first);
}
for (const auto& entry : sourceDiff.removed) {
advanceRetainedSourceRevision(RetainedUnderlayReason::SourceMutation, entry.first);
}
for (const auto& entry : sourceDiff.changed) {
advanceRetainedSourceRevision(RetainedUnderlayReason::SourceMutation, entry.first);
}
}
sourceImpls = updateParameters->sources;
// Remove render layers for removed sources.
@@ -364,6 +574,64 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
// Track which layers are flagged for rendering
std::vector<bool> updateList(orderedLayers.size());
RetainedDynamicBoundaryTracker boundaryTracker;
for (std::size_t index = 0; index < orderedLayers.size(); ++index) {
boundaryTracker.consider(static_cast<int32_t>(index),
orderedLayers[index].get().startsRetainedDynamicRange());
}
retainedDynamicBoundary = boundaryTracker.value();
std::vector<std::string> orderedLayerIds;
orderedLayerIds.reserve(orderedLayers.size());
for (const auto& layer : orderedLayers) orderedLayerIds.push_back(layer.get().getID());
continuousLayerRange = resolveContinuousLayerRange(
updateParameters->retainedUnderlayState
? updateParameters->retainedUnderlayState->continuousLayerRangeConfig()
: ContinuousLayerRangeConfig{},
orderedLayerIds);
if (continuousLayerRange.configured && !updateParameters->styleLoaded) {
continuousLayerRange.valid = false;
continuousLayerRange.startIndex = -1;
continuousLayerRange.endIndex = -1;
continuousLayerRange.reason = "style-not-ready";
}
if (updateParameters->retainedUnderlayState) {
updateParameters->retainedUnderlayState->recordRangeResolution(
continuousLayerRange,
continuousLayerRange.bottomPresent(),
continuousLayerRange.topPresent(static_cast<int32_t>(orderedLayers.size())));
const auto segmentConfig = updateParameters->retainedUnderlayState->renderSegmentPlanConfig();
const bool segmentMetadataChanged = renderSegmentPlan.configRevision != segmentConfig.revision ||
renderSegmentPlan.layerCount != static_cast<int32_t>(orderedLayers.size()) ||
renderSegmentStyleReady != updateParameters->styleLoaded || layersAddedOrRemoved ||
!layerDiff.changed.empty();
if (segmentMetadataChanged) {
renderSegmentPlan = resolveRenderSegmentPlan(
segmentConfig, orderedLayerIds, updateParameters->styleLoaded, retainedDynamicBoundary);
renderSegmentStyleReady = updateParameters->styleLoaded;
std::vector<StyleLayerDiagnostics> styleLayerDiagnostics;
styleLayerDiagnostics.reserve(orderedLayers.size());
for (std::size_t index = 0; index < orderedLayers.size(); ++index) {
const auto& layer = orderedLayers[index].get();
const auto& impl = *layer.baseImpl;
StyleLayerDiagnostics value;
value.layerIndex = static_cast<int32_t>(index);
value.layerId = layer.getID();
value.layerType = impl.getTypeInfo()->type;
value.visible = impl.visibility != style::VisibilityType::None;
if (!impl.source.empty()) value.sourceId = impl.source;
if (!impl.sourceLayer.empty()) value.sourceLayer = impl.sourceLayer;
styleLayerDiagnostics.push_back(std::move(value));
}
retainedStyleLayers = styleLayerDiagnostics;
updateParameters->retainedUnderlayState->recordRenderSegmentResolution(
renderSegmentPlan, std::move(styleLayerDiagnostics));
}
} else {
renderSegmentPlan = {};
retainedStyleLayers.clear();
}
// Update all sources and initialize renderItems.
for (const auto& sourceImpl : *sourceImpls) {
MLN_TRACE_ZONE(update source);
@@ -427,7 +695,17 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
addChanges(changes);
}
// Readiness is sampled for diagnostics only. Preserve the renderer's formal load gate.
renderTreeParameters->loaded = updateParameters->styleLoaded && isLoaded();
if (updateParameters->retainedUnderlayState) {
try {
auto readinessDiagnostics =
buildReadinessDiagnostics(updateParameters->styleLoaded, updateParameters->spriteLoaded);
updateParameters->retainedUnderlayState->recordRendererReadiness(std::move(readinessDiagnostics));
} catch (...) {
// A failed diagnostic snapshot must not affect the render tree or its formal loaded gate.
}
}
if (!isMapModeContinuous && !renderTreeParameters->loaded) {
return nullptr;
}
@@ -443,6 +721,18 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
}
}
// Only weather layers that explicitly opt in may clean up after Host clear.
// Ordinary hidden layers retain MapLibre's existing lifecycle.
for (auto& entry : orderedLayers) {
auto& layer = entry.get();
const auto source = renderSources.find(layer.baseImpl->source);
if (!layer.isLayerRenderable() && source != renderSources.end() &&
layer.needsHiddenLayerSourceCleanup(*source->second)) {
layer.prepare({.source = source->second.get(), .imageManager = *imageManager,
.patternAtlas = *patternAtlas, .lineAtlas = *lineAtlas,
.state = updateParameters->transformState});
}
}
auto opaquePassCutOffEstimation = layerRenderItems.size();
for (auto& renderItem : layerRenderItems) {
RenderLayer& renderLayer = renderItem.layer;
@@ -731,6 +1021,7 @@ void RenderOrchestrator::setFeatureState(const std::string& sourceID,
if (RenderSource* renderSource = getRenderSource(sourceID)) {
renderSource->setFeatureState(sourceLayerID, featureID, state);
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation, sourceID);
}
}
@@ -753,6 +1044,7 @@ void RenderOrchestrator::removeFeatureState(const std::string& sourceID,
if (RenderSource* renderSource = getRenderSource(sourceID)) {
renderSource->removeFeatureState(sourceLayerID, featureID, stateKey);
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::SourceMutation, sourceID);
}
}
@@ -818,7 +1110,7 @@ bool RenderOrchestrator::hasTransitions(TimePoint timePoint) const {
}
for (const auto& entry : renderLayers) {
if (entry.second->hasTransition()) {
if (entry.second->hasTransition() || entry.second->needsRepaint()) {
return true;
}
}
@@ -836,6 +1128,164 @@ bool RenderOrchestrator::hasTransitions(TimePoint timePoint) const {
return false;
}
bool RenderOrchestrator::retainedUnderlayUsesPlacement(int32_t boundary) const {
return retainedRangeUsesPlacement(0, boundary);
}
bool RenderOrchestrator::retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const {
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
for (int32_t index = first; index < last; ++index) {
if (orderedLayers[index].get().needsPlacement()) return true;
}
return false;
}
bool RenderOrchestrator::retainedRangeHasPlacementCapableLayer(int32_t begin, int32_t endExclusive) const {
return renderSegmentHasPlacementCapableLayer(retainedStyleLayers, begin, endExclusive);
}
bool RenderOrchestrator::retainedPlacementTransitionActive(TimePoint timePoint) const {
return placementController.hasTransitions(timePoint);
}
bool RenderOrchestrator::retainedUnderlayIsStable(TimePoint timePoint, int32_t boundary) const {
return retainedRangeIsStable(timePoint, 0, boundary);
}
bool RenderOrchestrator::retainedRangeIsStable(TimePoint timePoint, int32_t begin, int32_t endExclusive) const {
if (renderLight.hasTransition()) return false;
if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasTransitions(timePoint)) return false;
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
for (int32_t index = first; index < last; ++index) {
const auto& layer = orderedLayers[index].get();
if (layer.hasTransition() || layer.needsRepaint()) return false;
}
for (const auto& entry : renderSources) {
if (entry.second->hasFadingTiles()) return false;
}
return true;
}
RenderOrchestrator::RetainedSegmentSourceState RenderOrchestrator::retainedSegmentSourceState(
int32_t begin, int32_t endExclusive) const {
RetainedSegmentSourceState result;
const auto dependencies = renderSegmentDependencies(retainedStyleLayers, begin, endExclusive);
if (dependencies.unknown) return result;
// The new upstream source has no retained-cache revision contract yet.
// Keep its ordinary renderer active and fail closed only for cache reuse.
for (const auto& sourceId : dependencies.sourceIds) {
const auto source = renderSources.find(sourceId);
if (source != renderSources.end() && source->second->baseImpl->type == style::SourceType::CustomMVTVector) {
return result;
}
}
// The global epoch contains only changes whose effect cannot be scoped to one source.
{
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
const auto value = retainedSegmentRevisions.revisionFor(dependencies);
if (!value) return result;
result.revision = *value;
}
const auto assessment = assessRenderSegmentSources(dependencies, [&](const std::string& sourceId)
-> std::optional<RenderSegmentSourceStatus> {
const auto source = renderSources.find(sourceId);
if (source == renderSources.end()) return std::nullopt;
return RenderSegmentSourceStatus{source->second->isLoaded(), source->second->hasFadingTiles()};
});
result.ready = assessment.ready;
result.stable = assessment.stable;
result.reason = assessment.reason;
return result;
}
bool RenderOrchestrator::retainedSegmentIsStable(TimePoint timePoint,
int32_t begin,
int32_t endExclusive) const {
if (renderLight.hasTransition()) return false;
if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasTransitions(timePoint)) return false;
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
for (int32_t index = first; index < last; ++index) {
const auto& layer = orderedLayers[index].get();
if (layer.hasTransition() || layer.needsRepaint()) return false;
}
return true;
}
bool RenderOrchestrator::retainedGlobalResourcesReady() const {
return imageManager->isLoaded();
}
bool RenderOrchestrator::retainedGlobalLightTransition() const {
return renderLight.hasTransition();
}
bool RenderOrchestrator::retainedUnderlayHasUnsupportedLayer(int32_t boundary) const {
return retainedRangeHasUnsupportedLayer(0, boundary);
}
bool RenderOrchestrator::retainedRangeHasUnsupportedLayer(int32_t begin, int32_t endExclusive) const {
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
for (int32_t index = first; index < last; ++index) {
const std::string_view type = orderedLayers[index].get().baseImpl->getTypeInfo()->type;
if (type == "custom" || type == "custom-drawable") return true;
}
return false;
}
bool RenderOrchestrator::continuousRangeSupportsParticleFastPath(int32_t begin, int32_t endExclusive) const {
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
bool particle = false;
for (int32_t index = first; index < last; ++index) {
const auto& layer = orderedLayers[index].get();
if (!layer.startsRetainedDynamicRange()) return false;
particle = particle || layer.isParticleAnimationLayer();
}
return particle;
}
uint64_t RenderOrchestrator::retainedRangeContentFingerprint(int32_t begin, int32_t endExclusive) const {
const auto first = std::clamp<int32_t>(begin, 0, static_cast<int32_t>(orderedLayers.size()));
const auto last = std::clamp<int32_t>(endExclusive, first, static_cast<int32_t>(orderedLayers.size()));
uint64_t value = 1469598103934665603ULL;
for (int32_t index = first; index < last; ++index) {
const auto& layer = orderedLayers[static_cast<std::size_t>(index)].get();
const auto identity = reinterpret_cast<std::uintptr_t>(layer.baseImpl.get());
value ^= static_cast<uint64_t>(identity);
value *= 1099511628211ULL;
value ^= static_cast<uint64_t>(index);
value *= 1099511628211ULL;
}
return value;
}
ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(TimePoint timePoint) const {
ParticleFastPathDynamicState result;
result.blockingTransition = renderLight.hasTransition();
for (const auto& entry : renderLayers) {
const auto& layer = *entry.second;
result.blockingTransition = result.blockingTransition || layer.hasTransition();
if (layer.isLayerRenderable() && layer.isParticleAnimationActive()) result.particleNeedsRepaint = true;
if (layer.needsRepaint()) {
if (!layer.isParticleAnimationLayer()) {
result.nonParticleNeedsRepaint = true;
}
}
}
result.placementTransition = placementController.hasTransitions(timePoint);
for (const auto& entry : renderSources) {
result.fadingTiles = result.fadingTiles || entry.second->hasFadingTiles();
}
result.pendingChanges = !pendingChanges.empty();
return result;
}
bool RenderOrchestrator::isLoaded() const {
MLN_TRACE_FUNC();
@@ -878,6 +1328,14 @@ void RenderOrchestrator::clearData() {
imageManager->clear();
glyphManager->evict(fontStacks(*layerImpls));
try {
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
sourceReadinessEvents.clear();
lastReadinessMutation = {};
} catch (...) {
// Keep renderer style cleanup independent from optional diagnostic history cleanup.
}
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::StyleMutation);
}
void RenderOrchestrator::addChanges(UniqueChangeRequestVec& changes) {
@@ -970,6 +1428,15 @@ void RenderOrchestrator::updateLayers(gfx::ShaderRegistry& shaders,
std::vector<std::unique_ptr<ChangeRequest>> changes;
changes.reserve(items.size() * 3);
for (auto& entry : orderedLayers) {
auto& layer = entry.get();
const auto source = renderSources.find(layer.baseImpl->source);
if (!layer.isLayerRenderable() && source != renderSources.end() &&
layer.needsHiddenLayerSourceCleanup(*source->second)) {
layer.update(shaders, context, state, updateParameters, paintParameters, renderTree, changes);
}
}
for (const auto& item : items) {
auto& renderLayer = item.layer.get();
#if MLN_RENDER_BACKEND_OPENGL
@@ -988,6 +1455,34 @@ void RenderOrchestrator::updateLayers(gfx::ShaderRegistry& shaders,
addChanges(changes);
}
void RenderOrchestrator::updateParticleLayers(gfx::ShaderRegistry& shaders,
gfx::Context& context,
const TransformState& state,
const std::shared_ptr<UpdateParameters>& updateParameters,
const PaintParameters& paintParameters,
const RenderTree& renderTree) {
MLN_TRACE_FUNC();
const auto& items = renderTree.getLayerRenderItemMap();
std::vector<std::unique_ptr<ChangeRequest>> changes;
changes.reserve(8);
for (const auto& item : items) {
auto& renderLayer = item.layer.get();
if (!renderLayer.isParticleAnimationLayer()) continue;
#if MLN_RENDER_BACKEND_OPENGL
if (androidGoldfishMitigationEnabled) {
renderLayer.removeAllDrawables();
}
#endif
try {
renderLayer.update(shaders, context, state, updateParameters, paintParameters, renderTree, changes);
} catch (...) {
observer->onRenderError(std::current_exception());
}
}
addChanges(changes);
}
void RenderOrchestrator::processChanges() {
auto localChanges = std::move(pendingChanges);
for (auto& change : localChanges) {
@@ -1022,6 +1517,7 @@ void RenderOrchestrator::updateDebugLayerGroups(const RenderTree& renderTree, Pa
}
void RenderOrchestrator::onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& range) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::GlyphMutation);
observer->onGlyphsLoaded(fontStack, range);
}
@@ -1056,9 +1552,10 @@ void RenderOrchestrator::onTileError(RenderSource& source, const OverscaledTileI
observer->onResourceError(error);
}
void RenderOrchestrator::onTileChanged(RenderSource&, const OverscaledTileID&) {
void RenderOrchestrator::onTileChanged(RenderSource& source, const OverscaledTileID&) {
MLN_TRACE_FUNC();
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::TileMutation, source.baseImpl->id);
observer->onInvalidate();
}
@@ -1066,6 +1563,15 @@ void RenderOrchestrator::onTileAction(RenderSource&,
TileOperation op,
const OverscaledTileID& id,
const std::string& sourceID) {
try {
std::lock_guard<std::mutex> lock(readinessDiagnosticsMutex);
const auto sequence = ++readinessEventSequence;
auto& state = sourceReadinessEvents[sourceID];
state.lastTileActionSequence = sequence;
state.lastTileAction = tileOperationText(op);
} catch (...) {
// Tile action observation is best-effort; always forward the original observer callback.
}
observer->onTileAction(op, id, sourceID);
}
@@ -1076,12 +1582,16 @@ void RenderOrchestrator::onSymbolError(RenderSource&, const std::string& message
void RenderOrchestrator::onStyleImageMissing(const std::string& id, const std::function<void()>& done) {
MLN_TRACE_FUNC();
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::ImageMutation);
observer->onStyleImageMissing(id, done);
}
void RenderOrchestrator::onRemoveUnusedStyleImages(const std::vector<std::string>& unusedImageIDs) {
MLN_TRACE_FUNC();
if (!unusedImageIDs.empty()) {
advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::ImageMutation);
}
observer->onRemoveUnusedStyleImages(unusedImageIDs);
}

View File

@@ -15,9 +15,13 @@
#include <mln/renderer/image_manager_observer.hpp>
#include <mln/text/placement.hpp>
#include <mln/renderer/render_tree.hpp>
#include <mln/renderer/retained_underlay_cache.hpp>
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
@@ -51,6 +55,15 @@ class LayerProperties;
using ImmutableLayer = Immutable<style::Layer::Impl>;
struct ParticleFastPathDynamicState {
bool particleNeedsRepaint = false;
bool nonParticleNeedsRepaint = false;
bool blockingTransition = false;
bool placementTransition = false;
bool fadingTiles = false;
bool pendingChanges = false;
};
class RenderOrchestrator final : public GlyphManagerObserver, public ImageManagerObserver, public RenderSourceObserver {
public:
RenderOrchestrator(bool backgroundLayerAsColor_,
@@ -62,7 +75,7 @@ public:
void enableAndroidEmulatorGoldfishMitigation(bool enable) { androidGoldfishMitigationEnabled = enable; }
#endif
void markContextLost() { contextLost = true; };
void markContextLost();
// TODO: Introduce RenderOrchestratorObserver.
void setObserver(RendererObserver*);
@@ -106,6 +119,37 @@ public:
bool addLayerGroup(LayerGroupBasePtr);
bool removeLayerGroup(const LayerGroupBasePtr&);
size_t numLayerGroups() const noexcept;
std::optional<int32_t> getRetainedDynamicBoundary() const noexcept { return retainedDynamicBoundary; }
const ContinuousLayerRangeResolution& getContinuousLayerRange() const noexcept { return continuousLayerRange; }
const ResolvedRenderSegmentPlan& getRenderSegmentPlan() const noexcept { return renderSegmentPlan; }
int32_t getOrderedLayerCount() const noexcept { return static_cast<int32_t>(orderedLayers.size()); }
bool retainedUnderlayUsesPlacement(int32_t boundary) const;
bool retainedUnderlayIsStable(TimePoint, int32_t boundary) const;
bool retainedUnderlayHasUnsupportedLayer(int32_t boundary) const;
bool retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const;
bool retainedRangeHasPlacementCapableLayer(int32_t begin, int32_t endExclusive) const;
bool retainedPlacementTransitionActive(TimePoint timePoint) const;
bool retainedRangeIsStable(TimePoint, int32_t begin, int32_t endExclusive) const;
struct RetainedSegmentSourceState {
uint64_t revision = 0;
bool ready = false;
bool stable = false;
const char* reason = "unknown-dependency";
};
RetainedSegmentSourceState retainedSegmentSourceState(int32_t begin, int32_t endExclusive) const;
bool retainedSegmentIsStable(TimePoint, int32_t begin, int32_t endExclusive) const;
bool retainedGlobalResourcesReady() const;
bool retainedGlobalLightTransition() const;
bool retainedRangeHasUnsupportedLayer(int32_t begin, int32_t endExclusive) const;
bool continuousRangeSupportsParticleFastPath(int32_t begin, int32_t endExclusive) const;
uint64_t retainedRangeContentFingerprint(int32_t begin, int32_t endExclusive) const;
ParticleFastPathDynamicState particleFastPathDynamicState(TimePoint) const;
uint64_t getRetainedUnderlayContentRevision() const noexcept {
return retainedUnderlayContentRevision.load(std::memory_order_acquire);
}
RetainedUnderlayReason getRetainedUnderlayContentReason() const noexcept {
return retainedUnderlayContentReason.load(std::memory_order_acquire);
}
void updateLayerIndex(LayerGroupBasePtr, int32_t newIndex);
template <typename Func /* void(LayerGroupBase&) */>
@@ -121,6 +165,104 @@ public:
}
}
template <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsInRange(int32_t begin, int32_t endExclusive, Func f, bool* failed = nullptr) {
size_t ordinal = static_cast<size_t>(std::distance(layerGroupsByLayerIndex.begin(),
layerGroupsByLayerIndex.lower_bound(begin)));
for (auto it = layerGroupsByLayerIndex.lower_bound(begin);
it != layerGroupsByLayerIndex.end() && it->first < endExclusive; ++it, ++ordinal) {
if (it->second) {
try { f(*it->second, ordinal); } catch (...) {
if (failed) *failed = true;
observer->onRenderError(std::current_exception());
}
}
}
}
template <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsInRangeReversed(int32_t begin, int32_t endExclusive, Func f, bool* failed = nullptr) {
auto first = layerGroupsByLayerIndex.lower_bound(begin);
auto last = layerGroupsByLayerIndex.lower_bound(endExclusive);
size_t ordinal = static_cast<size_t>(std::distance(layerGroupsByLayerIndex.begin(), last));
while (last != first) {
--last;
--ordinal;
if (last->second) {
try { f(*last->second, ordinal); } catch (...) {
if (failed) *failed = true;
observer->onRenderError(std::current_exception());
}
}
}
}
template <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsBefore(int32_t boundary, Func f) {
size_t ordinal = 0;
for (auto& pair : layerGroupsByLayerIndex) {
if (!retainedLayerGroupInRange(pair.first, boundary, RetainedLayerGroupRange::Before)) break;
if (pair.second) {
try {
f(*pair.second, ordinal);
} catch (...) {
observer->onRenderError(std::current_exception());
}
}
++ordinal;
}
}
template <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsFrom(int32_t boundary, Func f) {
size_t ordinal = static_cast<size_t>(std::distance(layerGroupsByLayerIndex.begin(),
layerGroupsByLayerIndex.lower_bound(boundary)));
for (auto it = layerGroupsByLayerIndex.lower_bound(boundary); it != layerGroupsByLayerIndex.end(); ++it) {
if (!retainedLayerGroupInRange(it->first, boundary, RetainedLayerGroupRange::From)) continue;
if (it->second) {
try {
f(*it->second, ordinal);
} catch (...) {
observer->onRenderError(std::current_exception());
}
}
++ordinal;
}
}
template <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsBeforeReversed(int32_t boundary, Func f) {
auto end = layerGroupsByLayerIndex.lower_bound(boundary);
size_t ordinal = static_cast<size_t>(std::distance(layerGroupsByLayerIndex.begin(), end));
while (end != layerGroupsByLayerIndex.begin()) {
--end;
--ordinal;
if (end->second) {
try {
f(*end->second, ordinal);
} catch (...) {
observer->onRenderError(std::current_exception());
}
}
}
}
template <typename Func /* void(LayerGroupBase&, size_t originalOrdinal) */>
void visitLayerGroupsFromReversed(int32_t boundary, Func f) {
auto begin = layerGroupsByLayerIndex.lower_bound(boundary);
size_t ordinal = layerGroupsByLayerIndex.size();
for (auto it = layerGroupsByLayerIndex.rbegin(); it.base() != begin; ++it) {
--ordinal;
if (it->second) {
try {
f(*it->second, ordinal);
} catch (...) {
observer->onRenderError(std::current_exception());
}
}
}
}
template <typename Func /* void(LayerGroupBase&) */>
void visitLayerGroupsReversed(Func f) {
for (auto rit = layerGroupsByLayerIndex.rbegin(); rit != layerGroupsByLayerIndex.rend(); ++rit) {
@@ -140,6 +282,12 @@ public:
const std::shared_ptr<UpdateParameters>&,
const PaintParameters& paintParameters,
const RenderTree&);
void updateParticleLayers(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const PaintParameters&,
const RenderTree&);
void processChanges();
@@ -167,6 +315,9 @@ public:
const ZoomHistory& getZoomHistory() const { return zoomHistory; }
private:
// Allows the focused teardown test to install a pending tile-backed source.
friend struct RenderOrchestratorTeardownTestAccess;
bool isLoaded() const;
bool hasTransitions(TimePoint) const;
@@ -200,6 +351,15 @@ private:
/// Move changes into the pending set, clearing the provided collection
void addChanges(UniqueChangeRequestVec&);
void advanceRetainedUnderlayContentRevision(
RetainedUnderlayReason,
const std::optional<std::string>& sourceId = std::nullopt) noexcept;
void advanceRetainedSourceRevision(RetainedUnderlayReason, const std::string& sourceId) noexcept;
void recordReadinessMutation(const char* eventType,
RetainedUnderlayReason reason,
const std::optional<std::string>& sourceId = std::nullopt,
const std::string& tileAction = {}) noexcept;
RendererReadinessDiagnostics buildReadinessDiagnostics(bool styleLoaded, bool spriteLoaded) const;
RendererObserver* observer;
@@ -226,6 +386,25 @@ private:
bool contextLost = false;
bool placedSymbolDataCollected = false;
bool tileCacheEnabled = true;
bool retainedInputStateInitialized = false;
bool retainedSpriteLoaded = false;
std::string retainedGlyphURL;
const void* retainedFontFacesIdentity = nullptr;
std::atomic<uint64_t> retainedUnderlayContentRevision{1};
RenderSegmentRevisionBook retainedSegmentRevisions;
std::atomic<RetainedUnderlayReason> retainedUnderlayContentReason{RetainedUnderlayReason::None};
struct SourceReadinessEventState {
uint64_t lastSourceMutationSequence = 0;
uint64_t lastTileChangedSequence = 0;
uint64_t lastTileActionSequence = 0;
std::string lastTileAction{"none"};
std::string lastMutationReason{"none"};
};
mutable std::mutex readinessDiagnosticsMutex;
uint64_t readinessEventSequence = 0;
std::unordered_map<std::string, SourceReadinessEventState> sourceReadinessEvents;
RenderMutationDiagnostics lastReadinessMutation;
#if MLN_RENDER_BACKEND_OPENGL
bool androidGoldfishMitigationEnabled{false};
@@ -235,6 +414,9 @@ private:
// reallocation on each frame.
std::vector<Immutable<style::LayerProperties>> filteredLayersForSource;
RenderLayerReferences orderedLayers;
std::vector<StyleLayerDiagnostics> retainedStyleLayers;
ResolvedRenderSegmentPlan renderSegmentPlan;
bool renderSegmentStyleReady = false;
RenderLayerReferences layersNeedPlacement;
TaggedScheduler threadPool;
@@ -243,6 +425,8 @@ private:
using LayerGroupMap = std::multimap<int32_t, LayerGroupBasePtr>;
LayerGroupMap layerGroupsByLayerIndex;
std::optional<int32_t> retainedDynamicBoundary;
ContinuousLayerRangeResolution continuousLayerRange;
std::vector<RenderTargetPtr> renderTargets;
RenderItem::DebugLayerGroupMap debugLayerGroups;

View File

@@ -0,0 +1,230 @@
#pragma once
#include <mln/renderer/render_segment.hpp>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
namespace mln {
inline std::optional<RenderSegmentPlan> parseRenderSegmentPlanJSON(const std::string& json) {
rapidjson::Document document;
if (document.Parse<rapidjson::kParseValidateEncodingFlag>(json.data(), json.size()).HasParseError() ||
!document.IsObject() ||
!document.HasMember("schemaVersion") || !document["schemaVersion"].IsUint() ||
document["schemaVersion"].GetUint() != 1 || !document.HasMember("segments") ||
!document["segments"].IsArray() || document.MemberCount() != 2) {
return std::nullopt;
}
std::set<std::string_view> rootKeys;
for (auto member = document.MemberBegin(); member != document.MemberEnd(); ++member) {
const std::string_view name(member->name.GetString(), member->name.GetStringLength());
if (!rootKeys.insert(name).second || (name != "schemaVersion" && name != "segments")) {
return std::nullopt;
}
}
RenderSegmentPlan plan;
for (const auto& value : document["segments"].GetArray()) {
if (!value.IsObject() || !value.HasMember("strategy") || !value["strategy"].IsString() ||
value.MemberCount() > 2) {
return std::nullopt;
}
std::set<std::string_view> segmentKeys;
for (auto member = value.MemberBegin(); member != value.MemberEnd(); ++member) {
const std::string_view name(member->name.GetString(), member->name.GetStringLength());
if (!segmentKeys.insert(name).second || (name != "strategy" && name != "startLayerId")) {
return std::nullopt;
}
}
RenderSegmentDefinition definition;
const auto& strategyValue = value["strategy"];
const std::string strategy(strategyValue.GetString(), strategyValue.GetStringLength());
if (strategy.find('\0') != std::string::npos) return std::nullopt;
if (strategy == "RETAINED") {
definition.strategy = RenderSegmentStrategy::Retained;
} else if (strategy == "CONTINUOUS") {
definition.strategy = RenderSegmentStrategy::Continuous;
} else {
return std::nullopt;
}
if (value.HasMember("startLayerId")) {
const auto& startLayerId = value["startLayerId"];
if (!startLayerId.IsString()) return std::nullopt;
std::string layerId(startLayerId.GetString(), startLayerId.GetStringLength());
if (layerId.find('\0') != std::string::npos) return std::nullopt;
definition.startLayerId = std::move(layerId);
}
plan.segments.push_back(std::move(definition));
}
return plan;
}
// Android and iOS bridges use the same fail-closed conversion so malformed
// JSON always replaces (and therefore clears) a previously accepted plan.
inline RenderSegmentPlan renderSegmentPlanFromJSONOrInvalid(const std::string& json) {
if (auto plan = parseRenderSegmentPlanJSON(json)) return std::move(*plan);
RenderSegmentPlan invalid;
invalid.schemaVersion = 0;
return invalid;
}
template <typename Writer>
inline void writeOptionalString(Writer& writer, const char* key, const std::optional<std::string>& value) {
writer.Key(key);
if (value) writer.String(value->c_str()); else writer.Null();
}
template <typename Writer>
inline void writeOptionalUint32(Writer& writer, const char* key, const std::optional<uint32_t>& value) {
writer.Key(key);
if (value) writer.Uint(*value); else writer.Null();
}
template <typename Writer>
inline void writeStringArray(Writer& writer, const char* key, const std::vector<std::string>& values) {
writer.Key(key);
writer.StartArray();
for (const auto& value : values) writer.String(value.c_str());
writer.EndArray();
}
template <typename Writer>
inline void writeStyleLayerDiagnostics(Writer& writer, const StyleLayerDiagnostics& layer) {
writer.StartObject();
writer.Key("layerIndex"); writer.Int(layer.layerIndex);
writer.Key("layerId"); writer.String(layer.layerId.c_str());
writer.Key("layerType"); writer.String(layer.layerType.c_str());
writer.Key("visible"); writer.Bool(layer.visible);
writeOptionalString(writer, "sourceId", layer.sourceId);
writeOptionalString(writer, "sourceLayer", layer.sourceLayer);
writer.EndObject();
}
inline std::string renderSegmentDiagnosticsToJSON(const RenderSegmentDiagnostics& snapshot) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
writer.StartObject();
writer.Key("schemaVersion"); writer.Uint(snapshot.schemaVersion);
writer.Key("enabled"); writer.Bool(snapshot.enabled);
writer.Key("configured"); writer.Bool(snapshot.configured);
writer.Key("valid"); writer.Bool(snapshot.valid);
writer.Key("configSource"); writer.String(snapshot.configSource.c_str());
writer.Key("configRevision"); writer.Uint64(snapshot.configRevision);
writer.Key("topologyRevision"); writer.Uint64(snapshot.topologyRevision);
writer.Key("styleLayerCount"); writer.Uint(snapshot.styleLayerCount);
writer.Key("segmentCount"); writer.Uint(snapshot.segmentCount);
writer.Key("retainedSegmentCount"); writer.Uint(snapshot.retainedSegmentCount);
writer.Key("continuousSegmentCount"); writer.Uint(snapshot.continuousSegmentCount);
writer.Key("coverageComplete"); writer.Bool(snapshot.coverageComplete);
writer.Key("fallback"); writer.Bool(snapshot.fallback);
writer.Key("lastReason"); writer.String(snapshot.lastReason.c_str());
writer.Key("fastPathEligible"); writer.Bool(snapshot.fastPathEligible);
writer.Key("fastPathActive"); writer.Bool(snapshot.fastPathActive);
writer.Key("fastPathFallbackReason"); writer.String(snapshot.fastPathFallbackReason.c_str());
writer.Key("fastPathHit"); writer.Uint64(snapshot.fastPathHit);
writer.Key("fastPathMiss"); writer.Uint64(snapshot.fastPathMiss);
writer.Key("fullCpuFrame"); writer.Uint64(snapshot.fullCpuFrame);
writer.Key("placementChanged"); writer.Bool(snapshot.placementChanged);
writer.Key("symbolFadeChange"); writer.Double(snapshot.symbolFadeChange);
writer.Key("placementTransitionActive"); writer.Bool(snapshot.placementTransitionActive);
writer.Key("crossSourceCollisions"); writer.Bool(snapshot.crossSourceCollisions);
writer.Key("mainBufferExecutionMode"); writer.String(snapshot.mainBufferExecutionMode.c_str());
writer.Key("globalLiveFallbackReason"); writer.String(snapshot.globalLiveFallbackReason.c_str());
writer.Key("styleLoaded"); writer.Bool(snapshot.styleLoaded);
writer.Key("imageManagerLoaded"); writer.Bool(snapshot.imageManagerLoaded);
writer.Key("renderOrchestratorLoaded"); writer.Bool(snapshot.renderOrchestratorLoaded);
writer.Key("spriteLoaded"); writer.Bool(snapshot.spriteLoaded);
writer.Key("hasFadingTiles"); writer.Bool(snapshot.hasFadingTiles);
writer.Key("contentRevision"); writer.Uint64(snapshot.contentRevision);
writer.Key("sources"); writer.StartArray();
for (const auto& source : snapshot.sources) {
writer.StartObject();
writer.Key("sourceId"); writer.String(source.sourceId.c_str());
writer.Key("sourceType"); writer.String(source.sourceType.c_str());
writer.Key("enabled"); writer.Bool(source.enabled);
writer.Key("isLoaded"); writer.Bool(source.isLoaded);
writer.Key("tileBacked"); writer.Bool(source.tileBacked);
writeOptionalUint32(writer, "tileCount", source.tileCount);
writeOptionalUint32(writer, "incompleteTileCount", source.incompleteTileCount);
writeOptionalUint32(writer, "loadedTileCount", source.loadedTileCount);
writer.Key("hasFadingTiles"); writer.Bool(source.hasFadingTiles);
writer.Key("lastSourceMutationSequence"); writer.Uint64(source.lastSourceMutationSequence);
writer.Key("lastTileChangedSequence"); writer.Uint64(source.lastTileChangedSequence);
writer.Key("lastTileActionSequence"); writer.Uint64(source.lastTileActionSequence);
writer.Key("lastTileAction"); writer.String(source.lastTileAction.c_str());
writer.Key("lastMutationReason"); writer.String(source.lastMutationReason.c_str());
writer.EndObject();
}
writer.EndArray();
writer.Key("lastMutation"); writer.StartObject();
writer.Key("sequence"); writer.Uint64(snapshot.lastMutation.sequence);
writer.Key("contentRevision"); writer.Uint64(snapshot.lastMutation.contentRevision);
writer.Key("eventType"); writer.String(snapshot.lastMutation.eventType.c_str());
writer.Key("reason"); writer.String(snapshot.lastMutation.reason.c_str());
writeOptionalString(writer, "sourceId", snapshot.lastMutation.sourceId);
writer.Key("affectedSegmentIndices"); writer.StartArray();
for (const auto index : snapshot.lastMutation.affectedSegmentIndices) writer.Uint(index);
writer.EndArray();
writer.Key("dependencyScope"); writer.String(snapshot.lastMutation.dependencyScope.c_str());
writer.EndObject();
writer.Key("styleLayers"); writer.StartArray();
for (const auto& layer : snapshot.styleLayers) writeStyleLayerDiagnostics(writer, layer);
writer.EndArray();
writer.Key("segments"); writer.StartArray();
for (const auto& segment : snapshot.segments) {
writer.StartObject();
writer.Key("segmentIndex"); writer.Uint(segment.segmentIndex);
writer.Key("strategy"); writer.String(renderSegmentStrategyText(segment.strategy));
writeOptionalString(writer, "configuredStartLayerId", segment.configuredStartLayerId);
writer.Key("beginIndex"); writer.Int(segment.beginIndex);
writer.Key("endExclusive"); writer.Int(segment.endExclusive);
writeOptionalString(writer, "resolvedStartLayerId", segment.resolvedStartLayerId);
writeOptionalString(writer, "resolvedEndLayerId", segment.resolvedEndLayerId);
writer.Key("layerCount"); writer.Uint(static_cast<uint32_t>(segment.layers.size()));
writer.Key("layers"); writer.StartArray();
for (const auto& layer : segment.layers) writeStyleLayerDiagnostics(writer, layer);
writer.EndArray();
writeStringArray(writer, "dependentSourceIds", segment.dependentSourceIds);
writeStringArray(writer, "unknownDependencies", segment.unknownDependencies);
writer.Key("placementCapable"); writer.Bool(segment.placementCapable);
writer.Key("placementGatePassed"); writer.Bool(segment.placementGatePassed);
writer.Key("actualExecution"); writer.String(segment.actualExecution.c_str());
writer.Key("cache");
if (!segment.cache) {
writer.Null();
} else {
const auto& cache = *segment.cache;
writer.StartObject();
writer.Key("cacheState"); writer.String(cache.cacheState.c_str());
writer.Key("contentRevision"); writer.Uint64(cache.contentRevision);
writer.Key("build"); writer.Uint64(cache.build);
writer.Key("reuse"); writer.Uint64(cache.reuse);
writer.Key("invalidation"); writer.Uint64(cache.invalidation);
writer.Key("fallbackCount"); writer.Uint64(cache.fallbackCount);
writer.Key("finalDecision"); writer.String(cache.finalDecision.c_str());
writer.Key("lastFallbackReason"); writer.String(cache.lastFallbackReason.c_str());
writer.Key("sourceReady"); writer.Bool(cache.sourceReady);
writer.Key("sourceStable"); writer.Bool(cache.sourceStable);
writer.Key("supported"); writer.Bool(cache.supported);
writer.Key("sourceRevision"); writer.Uint64(cache.sourceRevision);
writer.Key("lastInvalidationReason"); writer.String(cache.lastInvalidationReason.c_str());
writer.Key("textureCreate"); writer.Uint64(cache.textureCreate);
writer.Key("compositeCreate"); writer.Uint64(cache.compositeCreate);
writer.Key("resourceRelease"); writer.Uint64(cache.resourceRelease);
writer.EndObject();
}
writer.EndObject();
}
writer.EndArray();
writer.EndObject();
return buffer.GetString();
}
} // namespace mln

View File

@@ -0,0 +1,172 @@
#pragma once
#include <mln/renderer/render_segment.hpp>
#include <algorithm>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace mln {
enum class RenderSegmentConfigSource : uint8_t { UnconfiguredLegacy, ExplicitPlan, ContinuousRangeCompat };
inline const char* renderSegmentConfigSourceText(RenderSegmentConfigSource source) noexcept {
switch (source) {
case RenderSegmentConfigSource::ExplicitPlan: return "explicit-render-segment-plan";
case RenderSegmentConfigSource::ContinuousRangeCompat: return "continuous-range-compat";
case RenderSegmentConfigSource::UnconfiguredLegacy: return "unconfigured/legacy";
}
return "unconfigured/legacy";
}
struct RenderSegmentConfig {
RenderSegmentConfigSource source = RenderSegmentConfigSource::UnconfiguredLegacy;
RenderSegmentPlan plan;
std::string continuousStartLayerId;
std::string continuousEndLayerId;
uint64_t revision = 0;
};
inline ResolvedRenderSegmentPlan resolveRenderSegmentPlan(
const RenderSegmentConfig& config,
const std::vector<std::string>& orderedLayerIds,
bool styleReady,
std::optional<int32_t> legacyBoundary = std::nullopt) {
ResolvedRenderSegmentPlan result;
result.configSource = renderSegmentConfigSourceText(config.source);
result.configRevision = config.revision;
result.layerCount = static_cast<int32_t>(orderedLayerIds.size());
result.configured = config.source != RenderSegmentConfigSource::UnconfiguredLegacy;
if (!styleReady || orderedLayerIds.empty()) {
result.reason = "style-not-ready";
return result;
}
std::unordered_map<std::string, int32_t> indices;
indices.reserve(orderedLayerIds.size());
for (std::size_t i = 0; i < orderedLayerIds.size(); ++i) {
indices.emplace(orderedLayerIds[i], static_cast<int32_t>(i));
}
if (config.source == RenderSegmentConfigSource::UnconfiguredLegacy) {
if (!legacyBoundary || *legacyBoundary <= 0 || *legacyBoundary >= result.layerCount) {
result.reason = "not-configured";
return result;
}
result.segments = {
{0, RenderSegmentStrategy::Retained, std::nullopt, 0, *legacyBoundary},
{1,
RenderSegmentStrategy::Continuous,
orderedLayerIds[*legacyBoundary],
*legacyBoundary,
result.layerCount},
};
result.valid = true;
result.reason = "none";
return result;
}
if (config.source == RenderSegmentConfigSource::ContinuousRangeCompat) {
const auto start = indices.find(config.continuousStartLayerId);
const auto end = indices.find(config.continuousEndLayerId);
if (start == indices.end()) {
result.reason = "start-layer-missing";
return result;
}
if (end == indices.end()) {
result.reason = "end-layer-missing";
return result;
}
if (start->second > end->second) {
result.reason = "range-order-invalid";
return result;
}
uint32_t segmentIndex = 0;
if (start->second > 0) {
result.segments.push_back(
{segmentIndex++, RenderSegmentStrategy::Retained, std::nullopt, 0, start->second});
}
result.segments.push_back({segmentIndex++,
RenderSegmentStrategy::Continuous,
config.continuousStartLayerId,
start->second,
end->second + 1});
if (end->second + 1 < result.layerCount) {
result.segments.push_back({segmentIndex,
RenderSegmentStrategy::Retained,
orderedLayerIds[static_cast<std::size_t>(end->second + 1)],
end->second + 1,
result.layerCount});
}
result.valid = true;
result.reason = "none";
return result;
}
if (config.plan.schemaVersion != 1) {
result.reason = "unsupported-schema-version";
return result;
}
if (config.plan.segments.empty()) {
result.reason = "empty-segments";
return result;
}
if (config.plan.segments.front().startLayerId && !config.plan.segments.front().startLayerId->empty()) {
result.reason = "first-segment-start-layer-forbidden";
return result;
}
std::vector<int32_t> begins(config.plan.segments.size(), 0);
for (std::size_t i = 1; i < config.plan.segments.size(); ++i) {
const auto& startLayerId = config.plan.segments[i].startLayerId;
if (!startLayerId || startLayerId->empty()) {
result.reason = "segment-start-layer-required";
return result;
}
const auto found = indices.find(*startLayerId);
if (found == indices.end()) {
result.reason = "boundary-layer-missing";
return result;
}
begins[i] = found->second;
if (begins[i] <= begins[i - 1]) {
result.reason = begins[i] == begins[i - 1] ? "boundary-layer-duplicate" : "boundary-order-invalid";
return result;
}
}
result.segments.reserve(config.plan.segments.size());
for (std::size_t i = 0; i < config.plan.segments.size(); ++i) {
const int32_t endExclusive = i + 1 < begins.size() ? begins[i + 1] : result.layerCount;
if (begins[i] >= endExclusive) {
result.segments.clear();
result.reason = "empty-segment";
return result;
}
result.segments.push_back({static_cast<uint32_t>(i),
config.plan.segments[i].strategy,
config.plan.segments[i].startLayerId,
begins[i],
endExclusive});
}
result.valid = true;
result.reason = "none";
return result;
}
inline bool renderSegmentCoverageComplete(const ResolvedRenderSegmentPlan& plan) noexcept {
if (!plan.valid || plan.segments.empty() || plan.segments.front().beginIndex != 0 ||
plan.segments.back().endExclusive != plan.layerCount) {
return false;
}
for (std::size_t i = 0; i < plan.segments.size(); ++i) {
if (plan.segments[i].beginIndex >= plan.segments[i].endExclusive) return false;
if (i > 0 && plan.segments[i - 1].endExclusive != plan.segments[i].beginIndex) return false;
}
return true;
}
} // namespace mln

View File

@@ -9,7 +9,9 @@
#include <mln/renderer/sources/render_image_source.hpp>
#include <mln/renderer/sources/render_raster_dem_source.hpp>
#include <mln/renderer/sources/render_raster_source.hpp>
#include <mln/renderer/sources/render_raster_array_source.hpp>
#include <mln/renderer/sources/render_vector_source.hpp>
#include <mln/style/sources/raster_array_source.hpp>
#include <mln/renderer/tile_parameters.hpp>
#include <mln/renderer/update_parameters.hpp>
#include <mln/tile/tile.hpp>
@@ -53,6 +55,8 @@ std::unique_ptr<RenderSource> RenderSource::create(const Immutable<Source::Impl>
case SourceType::CustomMVTVector:
return std::make_unique<RenderCustomVectorSource>(staticImmutableCast<CustomVectorSource::Impl>(impl),
threadPool);
case SourceType::RasterArray:
return std::make_unique<RenderRasterArraySource>(staticImmutableCast<RasterArraySource::Impl>(impl));
}
// Not reachable, but placate GCC.
@@ -71,7 +75,7 @@ RenderSource::RenderSource(Immutable<style::Source::Impl> impl)
RenderSource::~RenderSource() = default;
void RenderSource::setObserver(RenderSourceObserver* observer_) {
observer = observer_;
observer = observer_ ? observer_ : &nullObserver;
}
void RenderSource::onTileChanged(Tile& tile) {

View File

@@ -11,8 +11,10 @@
#include <mln/style/source_impl.hpp>
#include <mln/style/layer_properties.hpp>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <unordered_map>
#include <vector>
@@ -49,6 +51,14 @@ public:
using RenderTiles = std::shared_ptr<const std::vector<std::reference_wrapper<const RenderTile>>>;
struct RenderSourceReadinessStats {
bool tileBacked = false;
std::optional<uint32_t> tileCount;
std::optional<uint32_t> incompleteTileCount;
std::optional<uint32_t> loadedTileCount;
bool hasFadingTiles = false;
};
class RenderSource : protected TileObserver {
public:
static std::unique_ptr<RenderSource> create(const Immutable<style::Source::Impl>&, const TaggedScheduler&);
@@ -68,6 +78,17 @@ public:
virtual void prepare(const SourcePrepareParameters&) = 0;
virtual void updateFadingTiles() = 0;
virtual bool hasFadingTiles() const = 0;
// Read on the renderer thread while the source and its tile pyramid are owned by RenderOrchestrator.
// This is an observational summary and must not define or modify isLoaded().
virtual RenderSourceReadinessStats getReadinessDiagnostics() const {
return {
.tileBacked = false,
.tileCount = std::nullopt,
.incompleteTileCount = std::nullopt,
.loadedTileCount = std::nullopt,
.hasFadingTiles = hasFadingTiles(),
};
}
// If supported, returns a shared list of RenderTiles, sorted by tile id and
// excluding tiles hold for fade; returns nullptr otherwise.
virtual RenderTiles getRenderTiles() const { return nullptr; }

View File

@@ -21,6 +21,10 @@ const gfx::Texture2DPtr& RenderTarget::getTexture() {
return offscreenTexture->getTexture();
};
bool RenderTarget::isRenderable() {
return offscreenTexture && offscreenTexture->isRenderable();
}
bool RenderTarget::addLayerGroup(LayerGroupBasePtr layerGroup, const bool replace) {
const auto index = layerGroup->getLayerIndex();
const auto result = layerGroupsByLayerIndex.insert(std::make_pair(index, LayerGroupBasePtr{}));
@@ -70,9 +74,7 @@ void RenderTarget::render(RenderOrchestrator& orchestrator, const RenderTree& re
.clearColor = Color{0.0f, 0.0f, 0.0f, 1.0f},
.clearDepth = {},
.clearStencil = {}});
#if MLN_RENDER_BACKEND_OPENGL
parameters.updateStencilBufferAvailability();
#endif
parameters.beginRenderTarget();
const gfx::ScissorRect prevScissorRect = parameters.scissorRect;
const auto& size = getTexture()->getSize();

View File

@@ -99,6 +99,17 @@ public:
// Parameters
const RenderTreeParameters& getParameters() const { return *parameters; }
// Rebase the cached prepared tree for a Particle-only frame. All semantic
// inputs are checked by Renderer before this is called; only frame-local
// time/repaint bookkeeping is allowed to change here.
void prepareParticleFastFrame(TimePoint timePoint) {
parameters->timePoint = timePoint;
parameters->needsRepaint = true;
parameters->placementChanged = false;
parameters->symbolFadeChange = 0.0f;
startTime = util::MonotonicTimer::now().count();
}
double getElapsedTime() const { return util::MonotonicTimer::now().count() - startTime; }
protected:

View File

@@ -25,6 +25,7 @@ Renderer::~Renderer() {
}
void Renderer::markContextLost() {
impl->clearPreparedRenderTree();
impl->orchestrator.markContextLost();
}
@@ -41,15 +42,25 @@ void Renderer::render(const std::shared_ptr<UpdateParameters>& updateParameters)
// `SymbolErrorObserver`).
const ErrorScope errorScope{impl->observer};
impl->synchronizeRetainedUnderlayState(updateParameters->retainedUnderlayState);
const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded;
impl->styleLoaded = updateParameters->styleLoaded;
if (!impl->dynamicTextureAtlas || styleChanged) {
if (styleChanged) impl->clearPreparedRenderTree();
auto& context = impl->backend.getContext();
impl->dynamicTextureAtlas = std::make_unique<gfx::DynamicTextureAtlas>(context);
}
if (impl->tryRenderParticleFastFrame(updateParameters)) return;
// A full createRenderTree may replace RenderLayer/RenderSource instances.
// Drop the old prepared tree first so its reference-based render items can
// never outlive the orchestrator objects they describe.
impl->clearPreparedRenderTree();
if (auto renderTree = impl->orchestrator.createRenderTree(updateParameters, impl->dynamicTextureAtlas)) {
renderTree->prepare();
impl->render(*renderTree, updateParameters);
impl->render(*renderTree, updateParameters, false);
impl->commitPreparedRenderTree(std::move(renderTree), updateParameters);
} else {
impl->clearPreparedRenderTree();
}
}
@@ -167,11 +178,13 @@ bool Renderer::getTileCacheEnabled() const {
void Renderer::reduceMemoryUse() {
gfx::BackendScope guard{impl->backend};
impl->clearPreparedRenderTree();
impl->reduceMemoryUse();
impl->orchestrator.reduceMemoryUse();
}
void Renderer::clearData() {
impl->clearPreparedRenderTree();
impl->orchestrator.clearData();
}

View File

@@ -1,6 +1,7 @@
#include <mln/renderer/renderer_impl.hpp>
#include <mln/geometry/line_atlas.hpp>
#include <mln/annotation/annotation_manager.hpp>
#include <mln/gfx/backend_scope.hpp>
#include <mln/gfx/context.hpp>
#include <mln/gfx/cull_face_mode.hpp>
@@ -20,9 +21,16 @@
#include <mln/util/logging.hpp>
#include <mln/util/instrumentation.hpp>
#ifndef NDEBUG
#include <sstream>
#endif
#include <mln/gfx/drawable_tweaker.hpp>
#include <mln/renderer/layer_tweaker.hpp>
#include <mln/renderer/render_target.hpp>
#include <mln/renderer/retained_underlay_cache.hpp>
#include <mln/particle/renderer_debug_counters.hpp>
#include <string_view>
#if MLN_RENDER_BACKEND_METAL
#include <mln/mtl/renderer_backend.hpp>
@@ -44,11 +52,54 @@ using namespace style;
namespace {
const ResolvedRenderSegment* singleContinuousSegment(const ResolvedRenderSegmentPlan& plan) {
const ResolvedRenderSegment* result = nullptr;
for (const auto& segment : plan.segments) {
if (segment.strategy != RenderSegmentStrategy::Continuous) continue;
if (result) return nullptr;
result = &segment;
}
return result;
}
RendererObserver& nullObserver() {
static RendererObserver observer;
return observer;
}
#ifndef NDEBUG
constexpr uint64_t RetainedCounterLogIntervalFrames = 120;
void logRetainedRendererCounters(const RetainedUnderlayCache& cache, ParticleFastPathMissReason fastPathReason) {
const auto detailed = particle::rendererDebugCounterSnapshot();
const auto retained = cache.getDiagnostics();
if (detailed.fullRendererFrame == 0 ||
detailed.fullRendererFrame % RetainedCounterLogIntervalFrames != 0) {
return;
}
std::ostringstream message;
message << "ParticleRetained:"
<< " frame=" << detailed.fullRendererFrame
<< " enabled=" << (retained.enabled ? "true" : "false")
<< " build=" << retained.build
<< " reuse=" << retained.reuse
<< " fallback=" << retained.fallback
<< " invalidation=" << retained.invalidation
<< " reason=" << retained.lastReason
<< " staticRevision=" << cache.getStaticRevision()
<< " buildRevision=" << cache.getBuildRevision()
<< " staticDraw=" << detailed.retainedUnderlayStaticGroupDraw
<< " dynamicDraw=" << detailed.retainedDynamicGroupDraw
<< " particleAdvance=" << detailed.particleSimulationAdvance
<< " fastHit=" << detailed.particleFastFrameHit
<< " fastMiss=" << detailed.particleFastFrameMiss
<< " particleFull=" << detailed.particleFullFrame
<< " fastReason=" << particleFastPathMissReasonText(fastPathReason);
Log::Info(Event::Render, message.str());
}
#endif
} // namespace
Renderer::Impl::Impl(gfx::RendererBackend& backend_,
@@ -89,8 +140,191 @@ void Renderer::Impl::setObserver(RendererObserver* observer_) {
observer = observer_ ? observer_ : &nullObserver();
}
void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<UpdateParameters>& updateParameters) {
void Renderer::Impl::synchronizeRetainedUnderlayState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& state) {
retainedRuntimeState = state;
for (auto& cache : retainedSegmentCaches) {
if (cache) cache->synchronizeRuntimeState(state);
}
}
Renderer::Impl::ParticleFastPathUpdateSignature Renderer::Impl::makeParticleFastPathUpdateSignature(
const UpdateParameters& parameters) const {
ParticleFastPathUpdateSignature result;
result.viewport = parameters.transformState.getSize();
parameters.transformState.getProjMatrix(result.cameraMatrix);
result.pixelRatio = parameters.pixelRatio;
result.mode = parameters.mode;
result.debugOptions = parameters.debugOptions;
result.styleLoaded = parameters.styleLoaded;
result.spriteLoaded = parameters.spriteLoaded;
result.transformChanging = parameters.transformState.isChanging();
result.gestureInProgress = parameters.transformState.isGestureInProgress();
result.light = parameters.light.get();
result.images = parameters.images.get();
result.sources = parameters.sources.get();
result.layers = parameters.layers.get();
result.fontFaces = parameters.fontFaces.get();
result.fileSource = parameters.fileSource.get();
result.glyphURL = parameters.glyphURL;
result.transitionDuration = parameters.transitionOptions.duration;
result.transitionDelay = parameters.transitionOptions.delay;
result.placementTransitions = parameters.transitionOptions.enablePlacementTransitions;
result.prefetchZoomDelta = parameters.prefetchZoomDelta;
result.crossSourceCollisions = parameters.crossSourceCollisions;
result.fastPFOREnabled = parameters.fastPFOREnabled;
result.tileLodMinRadius = parameters.tileLodMinRadius;
result.tileLodScale = parameters.tileLodScale;
result.tileLodPitchThreshold = parameters.tileLodPitchThreshold;
result.tileLodZoomShift = parameters.tileLodZoomShift;
result.tileLodMode = parameters.tileLodMode;
return result;
}
bool Renderer::Impl::particleFastPathNonCameraInputsMatch(const ParticleFastPathUpdateSignature& lhs,
const ParticleFastPathUpdateSignature& rhs) {
return lhs.pixelRatio == rhs.pixelRatio && lhs.mode == rhs.mode && lhs.debugOptions == rhs.debugOptions &&
lhs.styleLoaded == rhs.styleLoaded && lhs.spriteLoaded == rhs.spriteLoaded &&
lhs.transformChanging == rhs.transformChanging && lhs.gestureInProgress == rhs.gestureInProgress &&
lhs.light == rhs.light && lhs.images == rhs.images && lhs.sources == rhs.sources && lhs.layers == rhs.layers &&
lhs.fontFaces == rhs.fontFaces && lhs.fileSource == rhs.fileSource && lhs.glyphURL == rhs.glyphURL &&
lhs.transitionDuration == rhs.transitionDuration && lhs.transitionDelay == rhs.transitionDelay &&
lhs.placementTransitions == rhs.placementTransitions && lhs.prefetchZoomDelta == rhs.prefetchZoomDelta &&
lhs.crossSourceCollisions == rhs.crossSourceCollisions && lhs.fastPFOREnabled == rhs.fastPFOREnabled &&
lhs.tileLodMinRadius == rhs.tileLodMinRadius && lhs.tileLodScale == rhs.tileLodScale &&
lhs.tileLodPitchThreshold == rhs.tileLodPitchThreshold && lhs.tileLodZoomShift == rhs.tileLodZoomShift &&
lhs.tileLodMode == rhs.tileLodMode;
}
bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr<UpdateParameters>& updateParameters) {
const auto signature = makeParticleFastPathUpdateSignature(*updateParameters);
const auto dynamicState = orchestrator.particleFastPathDynamicState(updateParameters->timePoint);
bool annotationMutation = false;
if (auto guard = updateParameters->annotationManager.lock(); updateParameters->annotationManager) {
annotationMutation = updateParameters->annotationManager->hasPendingChanges();
}
const auto retained = retainedRuntimeState ? retainedRuntimeState->renderSegmentSnapshot()
: RenderSegmentDiagnostics{};
const auto& plan = orchestrator.getRenderSegmentPlan();
const auto* continuous = singleContinuousSegment(plan);
const bool oneContinuous = plan.valid && continuous;
const std::optional<int32_t> currentBoundary =
oneContinuous ? std::optional<int32_t>(continuous->beginIndex) : std::nullopt;
const bool hasSignature = particlePreparedSignature.has_value();
const bool viewportMatches = hasSignature && particlePreparedSignature->viewport == signature.viewport &&
particlePreparedBackendSize == backend.getDefaultRenderable().getSize();
const bool cameraMatches = hasSignature && particlePreparedSignature->cameraMatrix == signature.cameraMatrix;
const bool updateParametersMatch =
hasSignature && particleFastPathNonCameraInputsMatch(*particlePreparedSignature, signature);
bool retainedRevisionsMatch = retainedSegmentCaches.size() == plan.segments.size();
bool retainedCachesValid = retainedRevisionsMatch;
for (std::size_t i = 0; i < retainedSegmentCaches.size(); ++i) {
if (plan.segments[i].strategy != RenderSegmentStrategy::Retained) continue;
const auto& cache = retainedSegmentCaches[i];
retainedRevisionsMatch = retainedRevisionsMatch && cache &&
cache->getBuildRevision() == cache->getStaticRevision();
retainedCachesValid = retainedCachesValid && cache && cache->isValid();
}
const bool revisionMatches = retainedRevisionsMatch &&
particlePreparedStaticRevision == retainedUnderlayRevisionTracker.current();
const bool boundaryMatches = particlePreparedBoundary == currentBoundary &&
particlePreparedContinuousEnd ==
(oneContinuous ? std::optional<int32_t>(continuous->endExclusive) : std::nullopt) &&
particlePreparedRangeConfigRevision == plan.configRevision;
const bool topologyMatches = particlePreparedTopologyRevision == retainedUnderlayTopologyTracker.current();
const bool contentRevisionMatches =
particlePreparedContentRevision == orchestrator.getRetainedUnderlayContentRevision();
bool staticStable = plan.valid;
for (const auto& segment : plan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
staticStable = staticStable && orchestrator.retainedRangeIsStable(
updateParameters->timePoint, segment.beginIndex, segment.endExclusive);
}
}
const ParticleFastPathGateInput input{
.hasPreparedTree = !particlePreparedTree.empty(),
.retainedEnabled = retained.enabled,
.cacheValid = retainedCachesValid,
.revisionMatches = revisionMatches,
.contentRevisionMatches = contentRevisionMatches,
.cameraMatches = cameraMatches,
.viewportMatches = viewportMatches,
.updateParametersMatch = updateParametersMatch,
.styleLoaded = updateParameters->styleLoaded,
.boundaryMatches = boundaryMatches,
.topologyMatches = topologyMatches,
.staticStable = staticStable,
// placementChanged/symbolFadeChange belong to the previous prepared
// full frame. The current controller transition is the authoritative
// semantic state: while active we fail closed, and once settled the
// old full-frame snapshot must not permanently block the fast path.
.hasPlacementTransition = dynamicState.placementTransition,
.hasPendingChanges = dynamicState.pendingChanges,
.hasNonParticleTransition = dynamicState.blockingTransition,
.hasNonParticleRepaint = dynamicState.nonParticleNeedsRepaint,
.hasFadingTiles = dynamicState.fadingTiles,
.particleNeedsRepaint = dynamicState.particleNeedsRepaint,
.stillImageRequest = updateParameters->stillImageRequest,
.continuousMode = updateParameters->mode == MapMode::Continuous,
.debugDisabled = updateParameters->debugOptions == MapDebugOptions::NoDebug,
.unsupportedContinuousLayer = particlePreparedContinuousUnsupported || !oneContinuous,
.supportedScene = particlePreparedSceneSupported && oneContinuous,
.annotationMutation = annotationMutation,
};
const auto decision = evaluateParticleFastPath(input);
particleFastPathLastReason = decision.reason;
if (retainedRuntimeState) retainedRuntimeState->recordFastPathDecision(decision.candidate, decision.reason);
if (!decision.candidate) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFastFrameMiss);
return false;
}
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFastFrameHit);
particlePreparedTree.get()->prepareParticleFastFrame(updateParameters->timePoint);
render(*particlePreparedTree.get(), updateParameters, true);
return true;
}
void Renderer::Impl::commitPreparedRenderTree(std::unique_ptr<RenderTree> renderTree,
const std::shared_ptr<UpdateParameters>& updateParameters) {
const auto& plan = orchestrator.getRenderSegmentPlan();
const auto* continuous = singleContinuousSegment(plan);
particlePreparedSignature = makeParticleFastPathUpdateSignature(*updateParameters);
particlePreparedContentRevision = orchestrator.getRetainedUnderlayContentRevision();
particlePreparedTopologyRevision = retainedUnderlayTopologyTracker.current();
particlePreparedStaticRevision = retainedUnderlayRevisionTracker.current();
particlePreparedBoundary = continuous ? std::optional<int32_t>(continuous->beginIndex) : std::nullopt;
particlePreparedContinuousEnd = continuous ? std::optional<int32_t>(continuous->endExclusive) : std::nullopt;
particlePreparedRangeConfigRevision = plan.configRevision;
particlePreparedBackendSize = backend.getDefaultRenderable().getSize();
particlePreparedTree.replace(std::move(renderTree));
}
void Renderer::Impl::clearPreparedRenderTree() {
particlePreparedTree.reset();
particlePreparedSignature.reset();
particlePreparedContentRevision = 0;
particlePreparedTopologyRevision = 0;
particlePreparedStaticRevision = 0;
particlePreparedBoundary.reset();
particlePreparedContinuousEnd.reset();
particlePreparedRangeConfigRevision = 0;
particlePreparedContinuousUnsupported = false;
particlePreparedBackendSize = {};
particlePreparedSceneSupported = false;
}
void Renderer::Impl::render(const RenderTree& renderTree,
const std::shared_ptr<UpdateParameters>& updateParameters,
bool particleFastFrame) {
MLN_TRACE_FUNC();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FullRendererFrame);
if (!particleFastFrame) {
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleFullFrame);
if (retainedRuntimeState) retainedRuntimeState->recordFullCpuFrame();
}
auto& context = backend.getContext();
context.setObserver(this);
@@ -233,7 +467,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
// - UPLOAD PASS -------------------------------------------------------------------------------
// Uploads all required buffers and images before we do any actual rendering.
{
if (!particleFastFrame) {
const auto uploadPass = parameters.encoder->createUploadPass("upload",
parameters.backend.getDefaultRenderable());
#if !defined(NDEBUG)
@@ -255,16 +489,209 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
// - LAYER GROUP UPDATE ------------------------------------------------------------------------
// Updates all layer groups and process changes
if (staticData && staticData->shaders) {
orchestrator.updateLayers(*staticData->shaders,
context,
renderTreeParameters.transformParams.state,
updateParameters,
parameters,
renderTree);
if (particleFastFrame) {
orchestrator.updateParticleLayers(*staticData->shaders,
context,
renderTreeParameters.transformParams.state,
updateParameters,
parameters,
renderTree);
} else {
orchestrator.updateLayers(*staticData->shaders,
context,
renderTreeParameters.transformParams.state,
updateParameters,
parameters,
renderTree);
}
}
orchestrator.processChanges();
const auto& segmentPlan = orchestrator.getRenderSegmentPlan();
const bool retainedMode = segmentPlan.valid;
const auto* singleContinuous = singleContinuousSegment(segmentPlan);
const auto segmentSnapshot = retainedRuntimeState ? retainedRuntimeState->renderSegmentSnapshot()
: RenderSegmentDiagnostics{};
if (retainedCacheTopologyRevision != segmentSnapshot.topologyRevision ||
retainedSegmentCaches.size() != segmentPlan.segments.size()) {
retainedSegmentCaches.clear();
retainedSegmentCaches.resize(segmentPlan.segments.size());
retainedSegmentFingerprints.assign(segmentPlan.segments.size(), 0);
bool continuousSeen = false;
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Continuous) {
continuousSeen = true;
continue;
}
auto cache = std::make_unique<RetainedUnderlayCache>(
segment.segmentIndex,
"retained-segment-" + std::to_string(segment.segmentIndex),
continuousSeen ? RetainedRangeRole::Top : RetainedRangeRole::Bottom);
cache->setSegmentTopologyRevision(segmentSnapshot.topologyRevision);
cache->synchronizeRuntimeState(retainedRuntimeState);
retainedSegmentCaches[segment.segmentIndex] = std::move(cache);
}
retainedCacheTopologyRevision = segmentSnapshot.topologyRevision;
}
std::vector<std::optional<RetainedUnderlayKey>> segmentKeys(segmentPlan.segments.size());
std::vector<RetainedUnderlayDecision> segmentDecisions(
segmentPlan.segments.size(), RetainedUnderlayDecision::Disabled);
const std::optional<int32_t> retainedBoundary = singleContinuous
? std::optional<int32_t>(singleContinuous->beginIndex) : std::nullopt;
RetainedUnderlayTopologySnapshot retainedTopology{
.boundary = retainedBoundary,
.staticPrefix = {},
.continuousEndExclusive = singleContinuous
? std::optional<int32_t>(singleContinuous->endExclusive)
: std::nullopt,
.staticSuffix = {}};
mat4 retainedCameraMatrix;
state.getProjMatrix(retainedCameraMatrix);
for (const auto& segment : segmentPlan.segments) {
retainedTopology.staticPrefix.push_back(
{.layerIndex = segment.beginIndex,
.groupName = std::string(renderSegmentStrategyText(segment.strategy)) + ":" +
std::to_string(segment.endExclusive),
.groupType = static_cast<uint8_t>(segment.strategy)});
if (segment.strategy == RenderSegmentStrategy::Retained) {
segmentKeys[segment.segmentIndex] = RetainedUnderlayKey{
parameters.renderableSize, segment.beginIndex, segment.endExclusive, retainedCameraMatrix};
}
}
const auto retainedTopologyRevision = particleFastFrame ? retainedUnderlayTopologyTracker.current()
: retainedUnderlayTopologyTracker.observe(retainedTopology);
bool legacyInRetainedRange = false;
std::vector<bool> legacyInSegment(segmentPlan.segments.size(), false);
if (retainedMode) {
for (const auto& item : layerRenderItems) {
const auto index = static_cast<int32_t>(item.index);
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained && index >= segment.beginIndex &&
index < segment.endExclusive && item.layer.get().usesLegacyRendering()) {
legacyInSegment[segment.segmentIndex] = true;
legacyInRetainedRange = true;
}
}
}
}
bool retainedStaticUsesPlacement = false;
bool retainedStaticStable = retainedMode;
bool retainedUnsupportedStaticLayer = false;
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
retainedStaticUsesPlacement = retainedStaticUsesPlacement ||
orchestrator.retainedRangeUsesPlacement(segment.beginIndex, segment.endExclusive);
retainedStaticStable = retainedStaticStable && orchestrator.retainedRangeIsStable(
renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive);
retainedUnsupportedStaticLayer = retainedUnsupportedStaticLayer ||
orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive);
}
const bool retainedPlacementStable =
!retainedStaticUsesPlacement ||
(!renderTreeParameters.placementChanged && renderTreeParameters.symbolFadeChange == 0.0f);
const bool placementTransitionActive = orchestrator.retainedPlacementTransitionActive(
renderTreeParameters.timePoint);
const auto retainedContentRevision = orchestrator.getRetainedUnderlayContentRevision();
const auto retainedContentReason = orchestrator.getRetainedUnderlayContentReason();
const RetainedUnderlayRevisionState retainedRevisionState{
.viewport = parameters.renderableSize,
.cameraMatrix = retainedCameraMatrix,
.boundary = retainedBoundary,
.continuousEndExclusive = singleContinuous
? std::optional<int32_t>(singleContinuous->endExclusive)
: std::nullopt,
.rangeConfigRevision = segmentPlan.configRevision,
.pixelRatio = updateParameters->pixelRatio,
.contentRevision = retainedContentRevision,
.topologyRevision = retainedTopologyRevision,
.mapMode = static_cast<uint8_t>(updateParameters->mode),
.debugOptions = static_cast<uint32_t>(renderTreeParameters.debugOptions),
.styleLoaded = updateParameters->styleLoaded,
.renderTreeLoaded = renderTreeParameters.loaded,
.staticStable = retainedStaticStable,
.placementStable = retainedPlacementStable,
.has3D = renderTreeParameters.has3D,
.unsupportedStaticLayer = retainedUnsupportedStaticLayer,
};
retainedUnderlayRevisionTracker.observe(
retainedRevisionState, retainedUnderlayReasonText(retainedContentReason));
const bool retainedSupported = retainedMode &&
renderTreeParameters.loaded && !renderTreeParameters.has3D &&
renderTreeParameters.debugOptions == MapDebugOptions::NoDebug &&
retainedStaticStable && retainedPlacementStable &&
!legacyInRetainedRange && !retainedUnsupportedStaticLayer;
const bool fastContinuousSupported = singleContinuous &&
orchestrator.continuousRangeSupportsParticleFastPath(
singleContinuous->beginIndex, singleContinuous->endExclusive);
if (!particleFastFrame) particlePreparedContinuousUnsupported = !singleContinuous || !fastContinuousSupported;
if (retainedRuntimeState) retainedRuntimeState->recordFastPathEligibility(
retainedSupported && fastContinuousSupported);
if (!particleFastFrame) particlePreparedSceneSupported = retainedSupported && fastContinuousSupported;
const auto wantsResources = [](RetainedUnderlayDecision decision) {
return decision == RetainedUnderlayDecision::Build || decision == RetainedUnderlayDecision::Reuse;
};
std::vector<RenderOrchestrator::RetainedSegmentSourceState> segmentSourceStates(segmentPlan.segments.size());
std::vector<bool> segmentSupportsCache(segmentPlan.segments.size(), false);
std::vector<bool> segmentPlacementGates(segmentPlan.segments.size(), true);
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
auto& cache = retainedSegmentCaches[segment.segmentIndex];
auto& decision = segmentDecisions[segment.segmentIndex];
const auto fingerprint = orchestrator.retainedRangeContentFingerprint(
segment.beginIndex, segment.endExclusive);
const bool layerContentChanged = retainedSegmentFingerprints[segment.segmentIndex] != 0 &&
retainedSegmentFingerprints[segment.segmentIndex] != fingerprint;
retainedSegmentFingerprints[segment.segmentIndex] = fingerprint;
const auto sourceState = orchestrator.retainedSegmentSourceState(
segment.beginIndex, segment.endExclusive);
segmentSourceStates[segment.segmentIndex] = sourceState;
const auto segmentContentRevision = sourceState.revision ^
(fingerprint + 0x9e3779b97f4a7c15ULL + (sourceState.revision << 6U) +
(sourceState.revision >> 2U)) ^
static_cast<uint64_t>(updateParameters->pixelRatio * 1000000.0f);
const bool segmentStable = sourceState.stable && orchestrator.retainedSegmentIsStable(
renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive);
const bool placementCapable = orchestrator.retainedRangeHasPlacementCapableLayer(
segment.beginIndex, segment.endExclusive);
const bool segmentPlacementGatePassed = renderSegmentPlacementGatePassed(
placementCapable, renderTreeParameters.placementChanged,
renderTreeParameters.symbolFadeChange, placementTransitionActive);
segmentPlacementGates[segment.segmentIndex] = segmentPlacementGatePassed;
const bool segmentSupported = retainedMode && updateParameters->styleLoaded &&
updateParameters->spriteLoaded && orchestrator.retainedGlobalResourcesReady() &&
updateParameters->mode == MapMode::Continuous && !renderTreeParameters.has3D &&
renderTreeParameters.debugOptions == MapDebugOptions::NoDebug && segmentStable &&
segmentPlacementGatePassed && !legacyInSegment[segment.segmentIndex] &&
!orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive);
segmentSupportsCache[segment.segmentIndex] = segmentSupported;
const char* segmentFallbackReason = !sourceState.ready ? sourceState.reason
: !sourceState.stable ? sourceState.reason
: !updateParameters->styleLoaded || !updateParameters->spriteLoaded ||
!orchestrator.retainedGlobalResourcesReady() ? "style-not-loaded"
: !segmentPlacementGatePassed ? "static-placement"
: !segmentStable ? "static-transition"
: legacyInSegment[segment.segmentIndex] ? "legacy-ordering"
: orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive) ? "unsupported"
: !retainedMode ? "no-boundary"
: renderTreeParameters.has3D ? "3d"
: renderTreeParameters.debugOptions != MapDebugOptions::NoDebug ? "debug-options"
: updateParameters->mode != MapMode::Continuous ? "map-mode"
: nullptr;
decision = cache->evaluate(segmentKeys[segment.segmentIndex],
segmentContentRevision,
segmentSupported,
segmentFallbackReason,
layerContentChanged ? "layer-mutation"
: "source-or-global-mutation");
if (wantsResources(decision) &&
(!staticData->shaders || !cache->prepareResources(
context, *staticData->shaders, *segmentKeys[segment.segmentIndex]))) {
cache->markBuildFailed("resource-or-compositor");
decision = RetainedUnderlayDecision::Fallback;
}
}
// Upload layer groups
{
const auto uploadPass = parameters.encoder->createUploadPass("layerGroup-upload",
@@ -273,29 +700,55 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
const auto debugGroup = uploadPass->createDebugGroup("layerGroup-upload");
#endif
// Update the debug layer groups
orchestrator.updateDebugLayerGroups(renderTree, parameters);
// Update the debug layer groups only on the full path. Debug rendering
// is excluded by the fast-path gate.
if (!particleFastFrame) orchestrator.updateDebugLayerGroups(renderTree, parameters);
// Tweakers are run in the upload pass so they can set up uniforms.
parameters.currentLayer = 0;
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
parameters.currentLayer = 0;
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
if (particleFastFrame && singleContinuous) {
orchestrator.visitLayerGroupsInRange(
singleContinuous->beginIndex, singleContinuous->endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(ordinal);
layerGroup.runTweakers(renderTree, parameters);
});
} else {
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
parameters.currentLayer = 0;
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.runTweakers(renderTree, parameters);
parameters.currentLayer++;
});
}
// Give the layers a chance to upload
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
if (particleFastFrame && singleContinuous) {
orchestrator.visitLayerGroupsInRange(
singleContinuous->beginIndex, singleContinuous->endExclusive,
[&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); });
} else {
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
}
// Give the render targets a chance to upload
orchestrator.visitRenderTargets([&](RenderTarget& renderTarget) { renderTarget.upload(*uploadPass); });
// Upload the Debug layer group
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
if (!particleFastFrame) {
orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); });
}
if (retainedMode) {
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained &&
wantsResources(segmentDecisions[segment.segmentIndex])) {
retainedSegmentCaches[segment.segmentIndex]->upload(*uploadPass);
}
}
}
}
const Size atlasSize = parameters.patternAtlas.getPixelSize();
@@ -357,6 +810,57 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
[&](RenderTarget& renderTarget) { renderTarget.render(orchestrator, renderTree, parameters); });
};
const auto buildRetainedRange = [&](RetainedUnderlayCache& cache,
RetainedUnderlayDecision& decision,
int32_t begin,
int32_t endExclusive) {
if (decision != RetainedUnderlayDecision::Build || begin >= endExclusive) return;
bool drawFailed = false;
bool globalUniformsBound = false;
try {
cache.beginBuild(parameters);
context.bindGlobalUniformBuffers(*parameters.renderPass);
globalUniformsBound = true;
parameters.depthRangeSize = 1 - (orchestrator.numLayerGroups() + 2) * PaintParameters::numSublayers *
PaintParameters::depthEpsilon;
parameters.pass = RenderPass::Opaque;
orchestrator.visitLayerGroupsInRangeReversed(begin, endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups() - 1 - ordinal);
layerGroup.render(orchestrator, parameters);
particle::incrementRendererDebugCounter(
particle::RendererDebugCounter::RetainedUnderlayStaticGroupDraw);
}, &drawFailed);
if (!drawFailed) {
parameters.pass = RenderPass::Translucent;
orchestrator.visitLayerGroupsInRange(begin, endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups() - 1 - ordinal);
layerGroup.render(orchestrator, parameters);
particle::incrementRendererDebugCounter(
particle::RendererDebugCounter::RetainedUnderlayStaticGroupDraw);
}, &drawFailed);
}
context.unbindGlobalUniformBuffers(*parameters.renderPass);
globalUniformsBound = false;
cache.finishBuild(parameters);
} catch (...) {
drawFailed = true;
const auto error = std::current_exception();
if (globalUniformsBound && parameters.renderPass) {
context.unbindGlobalUniformBuffers(*parameters.renderPass);
}
observer->onRenderError(error);
parameters.renderPass.reset();
}
if (drawFailed) {
cache.markBuildFailed("resource-or-compositor");
decision = RetainedUnderlayDecision::Fallback;
} else {
cache.markBuildSucceeded();
}
};
const auto commonClearPass = [&] {
// - CLEAR
// -------------------------------------------------------------------------------------
@@ -375,42 +879,60 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
.clearColor = color,
.clearDepth = 1.0f,
.clearStencil = 0});
#if MLN_RENDER_BACKEND_OPENGL
parameters.updateStencilBufferAvailability();
#endif
parameters.beginRenderTarget();
}
};
// Actually render the layers
// Drawables
const auto drawableOpaquePass = [&] {
const auto drawableOpaquePass = [&](int32_t begin, int32_t endExclusive) {
const auto debugGroup(parameters.renderPass->createDebugGroup("drawables-opaque"));
parameters.pass = RenderPass::Opaque;
parameters.depthRangeSize = 1 - (orchestrator.numLayerGroups() + 2) * PaintParameters::numSublayers *
PaintParameters::depthEpsilon;
// draw layer groups, opaque pass
parameters.currentLayer = 0;
orchestrator.visitLayerGroupsReversed([&](LayerGroupBase& layerGroup) {
layerGroup.render(orchestrator, parameters);
parameters.currentLayer++;
});
if (retainedMode) {
orchestrator.visitLayerGroupsInRangeReversed(
begin, endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups() - 1 - ordinal);
layerGroup.render(orchestrator, parameters);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedDynamicGroupDraw);
});
} else {
parameters.currentLayer = 0;
orchestrator.visitLayerGroupsReversed([&](LayerGroupBase& layerGroup) {
layerGroup.render(orchestrator, parameters);
parameters.currentLayer++;
});
}
};
const auto drawableTranslucentPass = [&] {
const auto drawableTranslucentPass = [&](int32_t begin, int32_t endExclusive) {
const auto debugGroup(parameters.renderPass->createDebugGroup("drawables-translucent"));
parameters.pass = RenderPass::Translucent;
parameters.depthRangeSize = 1 - (orchestrator.numLayerGroups() + 2) * PaintParameters::numSublayers *
PaintParameters::depthEpsilon;
// draw layer groups, translucent pass
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups()) - 1;
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.render(orchestrator, parameters);
if (parameters.currentLayer > 0) {
parameters.currentLayer--;
}
});
if (retainedMode) {
orchestrator.visitLayerGroupsInRange(
begin, endExclusive,
[&](LayerGroupBase& layerGroup, size_t ordinal) {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups() - 1 - ordinal);
layerGroup.render(orchestrator, parameters);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedDynamicGroupDraw);
});
} else {
parameters.currentLayer = static_cast<uint32_t>(orchestrator.numLayerGroups()) - 1;
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) {
layerGroup.render(orchestrator, parameters);
if (parameters.currentLayer > 0) {
parameters.currentLayer--;
}
});
}
// Finally, render any legacy layers which have not been converted to drawables.
// Note that they may be out of order, this is just a temporary fix for `RenderLocationIndicatorLayer` (#2216)
@@ -419,14 +941,19 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
int32_t i = static_cast<int32_t>(layerRenderItems.size()) - 1;
for (auto it = layerRenderItems.begin(); it != layerRenderItems.end() && i >= 0; ++it, --i) {
parameters.currentLayer = i;
const RenderItem& item = *it;
if (item.hasRenderPass(parameters.pass)) {
const LayerRenderItem& layerItem = *it;
const RenderItem& item = layerItem;
const bool itemInLiveRange = !retainedMode ||
(static_cast<int32_t>(layerItem.index) >= begin &&
static_cast<int32_t>(layerItem.index) < endExclusive);
if (itemInLiveRange && item.hasRenderPass(parameters.pass)) {
item.render(parameters);
}
}
};
const auto drawableDebugOverlays = [&] {
if (particleFastFrame) return;
// Renders debug overlays.
{
const auto debugGroup(parameters.renderPass->createDebugGroup("debug"));
@@ -443,14 +970,75 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
drawable3DPass();
}
drawableTargetsPass();
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy == RenderSegmentStrategy::Retained) {
buildRetainedRange(*retainedSegmentCaches[segment.segmentIndex],
segmentDecisions[segment.segmentIndex],
segment.beginIndex,
segment.endExclusive);
}
}
if (retainedRuntimeState) {
for (const auto& segment : segmentPlan.segments) {
if (segment.strategy != RenderSegmentStrategy::Retained) continue;
const auto index = segment.segmentIndex;
const auto decision = segmentDecisions[index];
const char* decisionText = decision == RetainedUnderlayDecision::Build ? "build" :
decision == RetainedUnderlayDecision::Reuse ? "reuse" :
decision == RetainedUnderlayDecision::Fallback ? "fallback" : "disabled";
const auto& source = segmentSourceStates[index];
retainedRuntimeState->recordSegmentExecution(index, source.ready, source.stable,
segmentSupportsCache[index], source.revision, decisionText,
decision == RetainedUnderlayDecision::Fallback
? retainedSegmentCaches[index]->getLastInvalidationReason().c_str() : nullptr);
}
}
commonClearPass();
context.bindGlobalUniformBuffers(*parameters.renderPass);
drawableOpaquePass();
drawableTranslucentPass();
const char* globalLiveFallbackReason = renderSegmentGlobalLiveFallbackReason({
retainedMode,
renderTreeParameters.has3D,
renderTreeParameters.debugOptions != MapDebugOptions::NoDebug,
updateParameters->mode == MapMode::Continuous,
updateParameters->spriteLoaded,
orchestrator.retainedGlobalResourcesReady(),
orchestrator.retainedGlobalLightTransition(),
});
const bool globalLiveFallback = std::string_view(globalLiveFallbackReason) != "none";
std::vector<std::string> actualExecutions(segmentPlan.segments.size(), "live");
if (globalLiveFallback) {
drawableOpaquePass(0, segmentPlan.layerCount);
drawableTranslucentPass(0, segmentPlan.layerCount);
} else {
visitRenderSegmentExecution(segmentPlan,
[&](uint32_t index) { return wantsResources(segmentDecisions[index]); },
[&](uint32_t index) {
retainedSegmentCaches[index]->composite(orchestrator, parameters);
actualExecutions[index] = "composite";
},
[&](int32_t begin, int32_t endExclusive) {
drawableOpaquePass(begin, endExclusive);
drawableTranslucentPass(begin, endExclusive);
});
}
if (retainedRuntimeState) retainedRuntimeState->recordMainBufferExecution(
segmentSnapshot.topologyRevision, renderTreeParameters.placementChanged,
renderTreeParameters.symbolFadeChange, placementTransitionActive,
updateParameters->crossSourceCollisions, globalLiveFallback ? "global-live" : "mixed",
globalLiveFallbackReason, segmentPlacementGates, actualExecutions);
drawableDebugOverlays();
// Give the layers a chance to do cleanup
orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.postRender(orchestrator, parameters); });
if (particleFastFrame && singleContinuous) {
orchestrator.visitLayerGroupsInRange(
singleContinuous->beginIndex, singleContinuous->endExclusive,
[&](LayerGroupBase& layerGroup, size_t) {
layerGroup.postRender(orchestrator, parameters);
});
} else {
orchestrator.visitLayerGroups(
[&](LayerGroupBase& layerGroup) { layerGroup.postRender(orchestrator, parameters); });
}
context.unbindGlobalUniformBuffers(*parameters.renderPass);
// Ends the RenderPass
@@ -492,6 +1080,15 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
observer->onDidFinishRenderingMap();
}
#ifndef NDEBUG
for (const auto& cache : retainedSegmentCaches) {
if (cache) {
logRetainedRendererCounters(*cache, particleFastPathLastReason);
break;
}
}
#endif
frameCount += 1;
MLN_END_FRAME();
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include <mln/renderer/render_orchestrator.hpp>
#include <mln/renderer/particle_fast_path.hpp>
#include <mln/gfx/context_observer.hpp>
#if MLN_RENDER_BACKEND_METAL
@@ -9,13 +10,17 @@
#endif // MLN_RENDER_BACKEND_METAL
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace mln {
class RendererObserver;
class RenderStaticData;
class RenderTree;
class RetainedUnderlayCache;
class RetainedUnderlayRuntimeState;
namespace gfx {
class RendererBackend;
@@ -40,7 +45,11 @@ private:
void setObserver(RendererObserver*);
void render(const RenderTree&, const std::shared_ptr<UpdateParameters>&);
void render(const RenderTree&, const std::shared_ptr<UpdateParameters>&, bool particleFastFrame);
void synchronizeRetainedUnderlayState(const std::shared_ptr<RetainedUnderlayRuntimeState>&);
bool tryRenderParticleFastFrame(const std::shared_ptr<UpdateParameters>&);
void commitPreparedRenderTree(std::unique_ptr<RenderTree>, const std::shared_ptr<UpdateParameters>&);
void clearPreparedRenderTree();
void reduceMemoryUse();
@@ -53,9 +62,62 @@ private:
const float pixelRatio;
std::unique_ptr<RenderStaticData> staticData;
std::vector<std::unique_ptr<RetainedUnderlayCache>> retainedSegmentCaches;
std::vector<uint64_t> retainedSegmentFingerprints;
std::shared_ptr<RetainedUnderlayRuntimeState> retainedRuntimeState;
uint64_t retainedCacheTopologyRevision = 0;
RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker;
RetainedUnderlayTopologyTracker retainedUnderlayTopologyTracker;
ParticleFastPathPreparedTree<RenderTree> particlePreparedTree;
gfx::DynamicTextureAtlasPtr dynamicTextureAtlas;
bool styleLoaded = false;
struct ParticleFastPathUpdateSignature {
Size viewport;
mat4 cameraMatrix{};
float pixelRatio = 1.0f;
MapMode mode = MapMode::Continuous;
MapDebugOptions debugOptions = MapDebugOptions::NoDebug;
bool styleLoaded = false;
bool spriteLoaded = false;
bool transformChanging = false;
bool gestureInProgress = false;
const void* light = nullptr;
const void* images = nullptr;
const void* sources = nullptr;
const void* layers = nullptr;
const void* fontFaces = nullptr;
const void* fileSource = nullptr;
std::string glyphURL;
std::optional<Duration> transitionDuration;
std::optional<Duration> transitionDelay;
bool placementTransitions = true;
uint8_t prefetchZoomDelta = 0;
bool crossSourceCollisions = false;
bool fastPFOREnabled = false;
double tileLodMinRadius = 0;
double tileLodScale = 0;
double tileLodPitchThreshold = 0;
double tileLodZoomShift = 0;
TileLodMode tileLodMode = TileLodMode::Default;
};
ParticleFastPathUpdateSignature makeParticleFastPathUpdateSignature(const UpdateParameters&) const;
static bool particleFastPathNonCameraInputsMatch(const ParticleFastPathUpdateSignature&,
const ParticleFastPathUpdateSignature&);
std::optional<ParticleFastPathUpdateSignature> particlePreparedSignature;
uint64_t particlePreparedContentRevision = 0;
uint64_t particlePreparedTopologyRevision = 0;
uint64_t particlePreparedStaticRevision = 0;
std::optional<int32_t> particlePreparedBoundary;
std::optional<int32_t> particlePreparedContinuousEnd;
uint64_t particlePreparedRangeConfigRevision = 0;
bool particlePreparedContinuousUnsupported = false;
Size particlePreparedBackendSize;
bool particlePreparedSceneSupported = false;
ParticleFastPathMissReason particleFastPathLastReason = ParticleFastPathMissReason::NoPreparedTree;
enum class RenderState {
Never,
Partial,

View File

@@ -0,0 +1,383 @@
#include <mln/renderer/retained_underlay_cache.hpp>
#include <mln/gfx/color_mode.hpp>
#include <mln/gfx/context.hpp>
#include <mln/gfx/cull_face_mode.hpp>
#include <mln/gfx/drawable_builder.hpp>
#include <mln/gfx/offscreen_texture.hpp>
#include <mln/gfx/render_pass.hpp>
#include <mln/gfx/upload_pass.hpp>
#include <mln/gfx/vertex_attribute.hpp>
#include <mln/particle/renderer_debug_counters.hpp>
#include <mln/renderer/buckets/heatmap_bucket.hpp>
#include <mln/renderer/layer_group.hpp>
#include <mln/renderer/paint_parameters.hpp>
#include <mln/renderer/render_orchestrator.hpp>
#include <mln/renderer/render_static_data.hpp>
#include <mln/shaders/shader_defines.hpp>
#include <string_view>
namespace mln {
namespace {
constexpr auto CompositeShaderName = "RetainedUnderlayCompositeShader";
RetainedUnderlayReason retainedReason(const char* reason) noexcept {
if (!reason) return RetainedUnderlayReason::Unknown;
const std::string_view value(reason);
if (value == "runtime-disabled") return RetainedUnderlayReason::RuntimeDisabled;
if (value == "first-frame") return RetainedUnderlayReason::FirstFrame;
if (value == "viewport") return RetainedUnderlayReason::Viewport;
if (value == "camera-or-boundary") return RetainedUnderlayReason::CameraOrBoundary;
if (value == "no-boundary") return RetainedUnderlayReason::NoBoundary;
if (value == "empty-static-prefix") return RetainedUnderlayReason::EmptyStaticPrefix;
if (value == "style-not-loaded") return RetainedUnderlayReason::StyleNotLoaded;
if (value == "3d") return RetainedUnderlayReason::ThreeDimensional;
if (value == "debug-options") return RetainedUnderlayReason::DebugOptions;
if (value == "static-transition") return RetainedUnderlayReason::StaticTransition;
if (value == "static-placement") return RetainedUnderlayReason::StaticPlacement;
if (value == "legacy-ordering") return RetainedUnderlayReason::LegacyOrdering;
if (value == "resource-or-compositor") return RetainedUnderlayReason::ResourceOrCompositor;
if (value == "style-mutation") return RetainedUnderlayReason::StyleMutation;
if (value == "layer-mutation") return RetainedUnderlayReason::LayerMutation;
if (value == "source-mutation") return RetainedUnderlayReason::SourceMutation;
if (value == "source-or-global-mutation") return RetainedUnderlayReason::SourceOrGlobalMutation;
if (value == "source-not-loaded") return RetainedUnderlayReason::SourceNotLoaded;
if (value == "source-fading") return RetainedUnderlayReason::SourceFading;
if (value == "missing-source") return RetainedUnderlayReason::MissingSource;
if (value == "unknown-dependency") return RetainedUnderlayReason::UnknownDependency;
if (value == "tile-mutation") return RetainedUnderlayReason::TileMutation;
if (value == "image-mutation") return RetainedUnderlayReason::ImageMutation;
if (value == "glyph-mutation") return RetainedUnderlayReason::GlyphMutation;
if (value == "layer-group-mutation") return RetainedUnderlayReason::LayerGroupMutation;
if (value == "context-reset") return RetainedUnderlayReason::ContextReset;
if (value == "pixel-ratio") return RetainedUnderlayReason::PixelRatio;
if (value == "static-revision") return RetainedUnderlayReason::StaticRevision;
if (value == "unsupported") return RetainedUnderlayReason::Unsupported;
return RetainedUnderlayReason::Unknown;
}
}
uint64_t RetainedUnderlayTopologyTracker::observe(const RetainedUnderlayTopologySnapshot& next) {
if (!snapshot) {
snapshot = next;
return revision;
}
if (*snapshot == next) return revision;
snapshot = next;
return ++revision;
}
uint64_t RetainedUnderlayRevisionTracker::observe(const RetainedUnderlayRevisionState& next,
const char* contentReason) {
if (!state) {
state = next;
return revision;
}
if (*state == next) return revision;
const auto& previous = *state;
if (previous.viewport != next.viewport) {
lastReason = "viewport";
} else if (previous.pixelRatio != next.pixelRatio) {
lastReason = "pixel-ratio";
} else if (previous.cameraMatrix != next.cameraMatrix || previous.boundary != next.boundary ||
previous.continuousEndExclusive != next.continuousEndExclusive ||
previous.rangeConfigRevision != next.rangeConfigRevision) {
lastReason = "camera-or-boundary";
} else if (previous.topologyRevision != next.topologyRevision) {
lastReason = "layer-group-mutation";
} else if (previous.contentRevision != next.contentRevision) {
lastReason = contentReason ? contentReason : "static-revision";
} else if (previous.styleLoaded != next.styleLoaded) {
lastReason = "style-mutation";
} else if (previous.renderTreeLoaded != next.renderTreeLoaded) {
lastReason = "tile-mutation";
} else if (previous.staticStable != next.staticStable) {
lastReason = "static-transition";
} else if (previous.placementStable != next.placementStable) {
lastReason = "static-placement";
} else if (previous.debugOptions != next.debugOptions) {
lastReason = "debug-options";
} else if (previous.has3D != next.has3D) {
lastReason = "3d";
} else if (previous.unsupportedStaticLayer != next.unsupportedStaticLayer ||
previous.mapMode != next.mapMode) {
lastReason = "unsupported";
} else {
lastReason = "static-revision";
}
state = next;
++revision;
return revision;
}
RetainedUnderlayCache::RetainedUnderlayCache(RetainedRangeRole role_)
: runtimeState(std::make_shared<RetainedUnderlayRuntimeState>()), role(role_) {}
RetainedUnderlayCache::RetainedUnderlayCache(uint32_t segmentIndex_,
std::string label_,
RetainedRangeRole compatibilityRole)
: runtimeState(std::make_shared<RetainedUnderlayRuntimeState>()),
role(compatibilityRole),
renderSegmentIndex(segmentIndex_),
renderSegmentLabel(std::move(label_)) {}
RetainedUnderlayCache::~RetainedUnderlayCache() {
releaseResources();
}
void RetainedUnderlayCache::synchronizeRuntimeState(
const std::shared_ptr<RetainedUnderlayRuntimeState>& nextState) {
if (nextState && nextState != runtimeState) {
releaseResources();
runtimeState = nextState;
observedControlRevision = runtimeState->getControlRevision();
}
if (!runtimeState) return;
const auto revision = runtimeState->getControlRevision();
if (revision == observedControlRevision) return;
observedControlRevision = revision;
// GPU resources are owned and released on the renderer thread. The public setter changes
// the shared enabled bit first and schedules this synchronization through Map::onUpdate().
if (valid || key) {
if (runtimeState->isEnabled()) {
invalidate("camera-or-boundary");
} else {
// setEnabled(false) already records the aggregate runtime-disabled invalidation once.
// Preserve that legacy aggregate semantic while still exposing the actual range cache invalidation.
valid = false;
lastInvalidationReason = "runtime-disabled";
runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top);
runtimeState->recordRangeInvalidationReason(
role == RetainedRangeRole::Top, RetainedUnderlayReason::RuntimeDisabled);
if (renderSegmentIndex) {
runtimeState->recordSegmentInvalidation(*renderSegmentIndex, RetainedUnderlayReason::RuntimeDisabled);
}
}
}
releaseResources();
if (!runtimeState->isEnabled()) {
lastInvalidationReason = "runtime-disabled";
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
}
RetainedUnderlayDecision RetainedUnderlayCache::evaluate(const std::optional<RetainedUnderlayKey>& nextKey,
uint64_t staticRevision,
bool supported,
const char* fallbackReason,
const char* revisionReason) {
const bool revisionChanged = observedStaticRevision != 0 && observedStaticRevision != staticRevision;
observedStaticRevision = staticRevision;
if (!nextKey) {
if (valid || key) {
invalidate(fallbackReason ? fallbackReason : "no-boundary");
releaseResources();
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Disabled);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Disabled);
}
return RetainedUnderlayDecision::Disabled;
}
if (runtimeState && !runtimeState->isEnabled()) {
lastInvalidationReason = "runtime-disabled";
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Fallback);
}
return RetainedUnderlayDecision::Fallback;
}
if (!supported) {
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(fallbackReason ? fallbackReason : "unsupported");
if (revisionChanged) releaseResources();
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Fallback);
}
return RetainedUnderlayDecision::Fallback;
}
if (revisionChanged) {
invalidate(revisionReason ? revisionReason : "static-revision");
// A content or backend generation change may have made any retained GPU object stale.
// Releasing here is deliberately conservative; these events are not the continuous
// Particle repaint itself and therefore do not affect the steady-state reuse path.
releaseResources();
key = nextKey;
} else if (!key || *key != *nextKey) {
const char* reason = !key ? "first-frame"
: (key->viewport != nextKey->viewport ? "viewport" : "camera-or-boundary");
invalidate(reason);
key = nextKey;
}
if (valid && buildRevision != observedStaticRevision) {
invalidate("static-revision");
releaseResources();
key = nextKey;
}
if (valid) {
++reuseCount;
runtimeState->recordReuse();
runtimeState->recordRangeReuse(role == RetainedRangeRole::Top);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayReuse);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Reuse);
if (renderSegmentIndex) {
runtimeState->recordSegmentReuse(*renderSegmentIndex);
runtimeState->recordSegmentCacheState(
*renderSegmentIndex, RetainedRangeCacheState::Reuse, observedStaticRevision);
}
return RetainedUnderlayDecision::Reuse;
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::BuildPending);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(
*renderSegmentIndex, RetainedRangeCacheState::BuildPending, observedStaticRevision);
}
return RetainedUnderlayDecision::Build;
}
bool RetainedUnderlayCache::prepareResources(gfx::Context& context,
gfx::ShaderRegistry& shaders,
const RetainedUnderlayKey& nextKey) {
try {
if (!offscreenTexture || offscreenTexture->getSize() != nextKey.viewport) {
offscreenTexture = context.createOffscreenTexture(
nextKey.viewport, gfx::TextureChannelDataType::UnsignedByte, true, true);
runtimeState->recordRangeTextureCreate(role == RetainedRangeRole::Top);
if (renderSegmentIndex) runtimeState->recordSegmentTextureCreate(*renderSegmentIndex);
compositeLayerGroup.reset();
}
if (!offscreenTexture || !offscreenTexture->isRenderable()) return false;
return compositeLayerGroup || rebuildComposite(context, shaders);
} catch (...) {
return false;
}
}
bool RetainedUnderlayCache::rebuildComposite(gfx::Context& context, gfx::ShaderRegistry& shaders) {
auto shader = context.getGenericShader(shaders, CompositeShaderName);
auto group = context.createLayerGroup(
0,
1,
renderSegmentIndex ? renderSegmentLabel + "-composite"
: (role == RetainedRangeRole::Top ? "retained-top-composite" : "retained-bottom-composite"));
if (!shader || !group || !offscreenTexture) return false;
auto vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
RenderStaticData::heatmapTextureVertices());
auto attributes = context.createVertexAttributeArray();
if (const auto& attribute = attributes->set(shaders::idRetainedUnderlayCompositePosVertexAttribute)) {
attribute->setSharedRawData(vertices,
offsetof(HeatmapTextureLayoutVertex, a1),
0,
sizeof(HeatmapTextureLayoutVertex),
gfx::AttributeDataType::Short2);
}
auto builder = context.createDrawableBuilder("retained-underlay-composite");
builder->setShader(std::move(shader));
builder->setEnableDepth(false);
builder->setColorMode(gfx::ColorMode::alphaBlended());
builder->setCullFaceMode(gfx::CullFaceMode::disabled());
builder->setRenderPass(RenderPass::Translucent);
builder->setVertexAttributes(std::move(attributes));
builder->setRawVertices({}, vertices->elements(), gfx::AttributeDataType::Short2);
auto segments = RenderStaticData::heatmapTextureSegments();
builder->setSegments(gfx::Triangles(),
RenderStaticData::quadTriangleIndices().vector(),
segments.data(),
segments.size());
builder->setTexture(offscreenTexture->getTexture(), shaders::idRetainedUnderlayCompositeTexture);
builder->flush(context);
for (auto& drawable : builder->clearDrawables()) group->addDrawable(std::move(drawable));
compositeLayerGroup = std::move(group);
runtimeState->recordRangeCompositeCreate(role == RetainedRangeRole::Top);
if (renderSegmentIndex) runtimeState->recordSegmentCompositeCreate(*renderSegmentIndex);
return !compositeLayerGroup->empty();
}
void RetainedUnderlayCache::upload(gfx::UploadPass& uploadPass) {
if (compositeLayerGroup) compositeLayerGroup->upload(uploadPass);
}
void RetainedUnderlayCache::beginBuild(PaintParameters& parameters) {
parameters.renderPass = parameters.encoder->createRenderPass(
role == RetainedRangeRole::Top ? "retained top range" : "retained bottom range",
{.renderable = *offscreenTexture,
.clearColor = Color{0.0f, 0.0f, 0.0f, 0.0f},
.clearDepth = 1.0f,
.clearStencil = 0});
parameters.beginRenderTarget();
}
void RetainedUnderlayCache::finishBuild(PaintParameters& parameters) {
parameters.renderPass.reset();
parameters.encoder->present(*offscreenTexture);
}
void RetainedUnderlayCache::composite(RenderOrchestrator& orchestrator, PaintParameters& parameters) {
if (!compositeLayerGroup) return;
parameters.pass = RenderPass::Translucent;
parameters.currentLayer = 0;
compositeLayerGroup->render(orchestrator, parameters);
}
void RetainedUnderlayCache::markBuildSucceeded() {
valid = true;
buildRevision = observedStaticRevision;
++buildCount;
runtimeState->recordBuild();
runtimeState->recordRangeBuild(role == RetainedRangeRole::Top);
if (renderSegmentIndex) {
runtimeState->recordSegmentBuild(*renderSegmentIndex);
runtimeState->recordSegmentCacheState(
*renderSegmentIndex, RetainedRangeCacheState::Ready, observedStaticRevision);
}
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Ready);
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayBuild);
}
void RetainedUnderlayCache::markBuildFailed(const char* reason) {
++fallbackCount;
runtimeState->recordFallback();
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayFallback);
invalidate(reason);
runtimeState->recordRangeCacheState(role == RetainedRangeRole::Top, RetainedRangeCacheState::Fallback);
if (renderSegmentIndex) {
runtimeState->recordSegmentCacheState(*renderSegmentIndex, RetainedRangeCacheState::Fallback);
}
releaseResources();
}
void RetainedUnderlayCache::invalidate(const char* reason) {
valid = false;
lastInvalidationReason = reason ? reason : "unknown";
runtimeState->recordInvalidation(retainedReason(reason));
runtimeState->recordRangeInvalidation(role == RetainedRangeRole::Top);
runtimeState->recordRangeInvalidationReason(role == RetainedRangeRole::Top, retainedReason(reason));
if (renderSegmentIndex) runtimeState->recordSegmentInvalidation(*renderSegmentIndex, retainedReason(reason));
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::RetainedUnderlayInvalidation);
}
void RetainedUnderlayCache::releaseResources() {
if (offscreenTexture || compositeLayerGroup) {
runtimeState->recordRangeResourceRelease(role == RetainedRangeRole::Top);
if (renderSegmentIndex) {
runtimeState->recordSegmentResourceRelease(*renderSegmentIndex, segmentTopologyRevision);
}
}
valid = false;
buildRevision = 0;
key.reset();
compositeLayerGroup.reset();
offscreenTexture.reset();
}
} // namespace mln

View File

@@ -0,0 +1,173 @@
#pragma once
#include <mln/util/mat4.hpp>
#include <mln/util/size.hpp>
#include <mln/renderer/retained_underlay_runtime_state.hpp>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace mln {
class LayerGroup;
class PaintParameters;
class RenderOrchestrator;
namespace gfx {
class Context;
class OffscreenTexture;
class ShaderRegistry;
class UploadPass;
} // namespace gfx
struct RetainedUnderlayKey {
Size viewport;
int32_t begin = 0;
int32_t endExclusive = 0;
mat4 cameraMatrix{};
bool operator==(const RetainedUnderlayKey&) const = default;
};
enum class RetainedRangeRole : uint8_t { Bottom, Top };
struct RetainedUnderlayTopologyEntry {
int32_t layerIndex = 0;
std::string groupName;
uint8_t groupType = 0;
bool operator==(const RetainedUnderlayTopologyEntry&) const = default;
};
struct RetainedUnderlayTopologySnapshot {
std::optional<int32_t> boundary;
std::vector<RetainedUnderlayTopologyEntry> staticPrefix;
std::optional<int32_t> continuousEndExclusive;
std::vector<RetainedUnderlayTopologyEntry> staticSuffix;
bool operator==(const RetainedUnderlayTopologySnapshot&) const = default;
};
class RetainedUnderlayTopologyTracker {
public:
uint64_t observe(const RetainedUnderlayTopologySnapshot&);
uint64_t current() const noexcept { return revision; }
private:
std::optional<RetainedUnderlayTopologySnapshot> snapshot;
uint64_t revision = 1;
};
struct RetainedUnderlayRevisionState {
Size viewport;
mat4 cameraMatrix{};
std::optional<int32_t> boundary;
std::optional<int32_t> continuousEndExclusive;
uint64_t rangeConfigRevision = 0;
float pixelRatio = 1.0f;
uint64_t contentRevision = 0;
uint64_t topologyRevision = 0;
uint8_t mapMode = 0;
uint32_t debugOptions = 0;
bool styleLoaded = false;
bool renderTreeLoaded = false;
bool staticStable = false;
bool placementStable = false;
bool has3D = false;
bool unsupportedStaticLayer = false;
bool operator==(const RetainedUnderlayRevisionState&) const = default;
};
class RetainedUnderlayRevisionTracker {
public:
uint64_t observe(const RetainedUnderlayRevisionState&, const char* contentReason = nullptr);
uint64_t current() const noexcept { return revision; }
const std::string& reason() const noexcept { return lastReason; }
private:
std::optional<RetainedUnderlayRevisionState> state;
uint64_t revision = 1;
std::string lastReason{"first-frame"};
};
enum class RetainedUnderlayDecision : uint8_t { Disabled, Build, Reuse, Fallback };
class RetainedDynamicBoundaryTracker {
public:
void consider(int32_t layerIndex, bool candidate) {
if (candidate && (!boundary || layerIndex < *boundary)) boundary = layerIndex;
}
std::optional<int32_t> value() const noexcept { return boundary; }
private:
std::optional<int32_t> boundary;
};
enum class RetainedLayerGroupRange : uint8_t { Before, From };
inline bool retainedLayerGroupInRange(int32_t layerIndex, int32_t boundary, RetainedLayerGroupRange range) {
return range == RetainedLayerGroupRange::Before ? layerIndex < boundary : layerIndex >= boundary;
}
class RetainedUnderlayCache {
public:
explicit RetainedUnderlayCache(RetainedRangeRole = RetainedRangeRole::Bottom);
RetainedUnderlayCache(uint32_t segmentIndex, std::string label, RetainedRangeRole compatibilityRole);
~RetainedUnderlayCache();
RetainedUnderlayDecision evaluate(const std::optional<RetainedUnderlayKey>&,
uint64_t staticRevision,
bool supported,
const char* fallbackReason = nullptr,
const char* revisionReason = nullptr);
void synchronizeRuntimeState(const std::shared_ptr<RetainedUnderlayRuntimeState>&);
bool prepareResources(gfx::Context&, gfx::ShaderRegistry&, const RetainedUnderlayKey&);
void upload(gfx::UploadPass&);
void beginBuild(PaintParameters&);
void finishBuild(PaintParameters&);
void composite(RenderOrchestrator&, PaintParameters&);
void markBuildSucceeded();
void markBuildFailed(const char* reason);
void invalidate(const char* reason);
bool isValid() const noexcept { return valid; }
uint64_t getBuildCount() const noexcept { return buildCount; }
uint64_t getReuseCount() const noexcept { return reuseCount; }
uint64_t getFallbackCount() const noexcept { return fallbackCount; }
uint64_t getStaticRevision() const noexcept { return observedStaticRevision; }
uint64_t getBuildRevision() const noexcept { return buildRevision; }
const std::string& getLastInvalidationReason() const noexcept { return lastInvalidationReason; }
RetainedUnderlayDiagnostics getDiagnostics() const { return runtimeState->snapshot(); }
void recordFastPathEligibility(bool eligible) { runtimeState->recordFastPathEligibility(eligible); }
void recordFastPathDecision(bool active, ParticleFastPathMissReason reason) {
runtimeState->recordFastPathDecision(active, reason);
}
void recordFullCpuFrame() { runtimeState->recordFullCpuFrame(); }
void setSegmentTopologyRevision(uint64_t revision) noexcept { segmentTopologyRevision = revision; }
private:
bool rebuildComposite(gfx::Context&, gfx::ShaderRegistry&);
void releaseResources();
std::optional<RetainedUnderlayKey> key;
std::unique_ptr<gfx::OffscreenTexture> offscreenTexture;
std::shared_ptr<LayerGroup> compositeLayerGroup;
bool valid = false;
uint64_t buildCount = 0;
uint64_t reuseCount = 0;
uint64_t fallbackCount = 0;
uint64_t observedStaticRevision = 0;
uint64_t buildRevision = 0;
std::string lastInvalidationReason;
std::shared_ptr<RetainedUnderlayRuntimeState> runtimeState;
uint64_t observedControlRevision = 0;
RetainedRangeRole role;
std::optional<uint32_t> renderSegmentIndex;
uint64_t segmentTopologyRevision = 0;
std::string renderSegmentLabel;
};
} // namespace mln

View File

@@ -0,0 +1,538 @@
#pragma once
#include <mln/renderer/particle_fast_path.hpp>
#include <mln/renderer/retained_underlay.hpp>
#include <mln/renderer/render_segment.hpp>
#include <mln/renderer/render_segment_plan.hpp>
#include <algorithm>
#include <atomic>
#include <mutex>
#include <set>
#include <string_view>
#include <tuple>
#include <utility>
namespace mln {
enum class RetainedRangeCacheState : uint8_t { Disabled, BuildPending, Ready, Reuse, Fallback };
inline const char* retainedRangeCacheStateText(RetainedRangeCacheState state) noexcept {
switch (state) {
case RetainedRangeCacheState::Disabled: return "disabled";
case RetainedRangeCacheState::BuildPending: return "build-pending";
case RetainedRangeCacheState::Ready: return "ready";
case RetainedRangeCacheState::Reuse: return "reuse";
case RetainedRangeCacheState::Fallback: return "fallback";
}
return "disabled";
}
enum class RetainedUnderlayReason : uint8_t {
None,
RuntimeDisabled,
FirstFrame,
Viewport,
CameraOrBoundary,
NoBoundary,
EmptyStaticPrefix,
StyleNotLoaded,
ThreeDimensional,
DebugOptions,
StaticTransition,
StaticPlacement,
LegacyOrdering,
ResourceOrCompositor,
StyleMutation,
LayerMutation,
SourceMutation,
SourceOrGlobalMutation,
SourceNotLoaded,
SourceFading,
MissingSource,
UnknownDependency,
TileMutation,
ImageMutation,
GlyphMutation,
LayerGroupMutation,
ContextReset,
PixelRatio,
StaticRevision,
Unsupported,
Unknown,
};
inline const char* retainedUnderlayReasonText(RetainedUnderlayReason reason) noexcept {
switch (reason) {
case RetainedUnderlayReason::None: return "none";
case RetainedUnderlayReason::RuntimeDisabled: return "runtime-disabled";
case RetainedUnderlayReason::FirstFrame: return "first-frame";
case RetainedUnderlayReason::Viewport: return "viewport";
case RetainedUnderlayReason::CameraOrBoundary: return "camera-or-boundary";
case RetainedUnderlayReason::NoBoundary: return "no-boundary";
case RetainedUnderlayReason::EmptyStaticPrefix: return "empty-static-prefix";
case RetainedUnderlayReason::StyleNotLoaded: return "style-not-loaded";
case RetainedUnderlayReason::ThreeDimensional: return "3d";
case RetainedUnderlayReason::DebugOptions: return "debug-options";
case RetainedUnderlayReason::StaticTransition: return "static-transition";
case RetainedUnderlayReason::StaticPlacement: return "static-placement";
case RetainedUnderlayReason::LegacyOrdering: return "legacy-ordering";
case RetainedUnderlayReason::ResourceOrCompositor: return "resource-or-compositor";
case RetainedUnderlayReason::StyleMutation: return "style-mutation";
case RetainedUnderlayReason::LayerMutation: return "layer-mutation";
case RetainedUnderlayReason::SourceMutation: return "source-mutation";
case RetainedUnderlayReason::SourceOrGlobalMutation: return "source-or-global-mutation";
case RetainedUnderlayReason::SourceNotLoaded: return "source-not-loaded";
case RetainedUnderlayReason::SourceFading: return "source-fading";
case RetainedUnderlayReason::MissingSource: return "missing-source";
case RetainedUnderlayReason::UnknownDependency: return "unknown-dependency";
case RetainedUnderlayReason::TileMutation: return "tile-mutation";
case RetainedUnderlayReason::ImageMutation: return "image-mutation";
case RetainedUnderlayReason::GlyphMutation: return "glyph-mutation";
case RetainedUnderlayReason::LayerGroupMutation: return "layer-group-mutation";
case RetainedUnderlayReason::ContextReset: return "context-reset";
case RetainedUnderlayReason::PixelRatio: return "pixel-ratio";
case RetainedUnderlayReason::StaticRevision: return "static-revision";
case RetainedUnderlayReason::Unsupported: return "unsupported";
case RetainedUnderlayReason::Unknown: return "unknown";
}
return "unknown";
}
class RetainedUnderlayRuntimeState {
public:
bool setEnabled(bool value) noexcept {
const bool previous = enabled.exchange(value, std::memory_order_acq_rel);
if (previous == value) return false;
controlRevision.fetch_add(1, std::memory_order_release);
if (!value) {
invalidation.fetch_add(1, std::memory_order_relaxed);
lastReason.store(RetainedUnderlayReason::RuntimeDisabled, std::memory_order_release);
}
return true;
}
bool isEnabled() const noexcept { return enabled.load(std::memory_order_acquire); }
uint64_t getControlRevision() const noexcept { return controlRevision.load(std::memory_order_acquire); }
bool setContinuousLayerRange(std::string startLayerId, std::string endLayerId) {
if (startLayerId.empty() || endLayerId.empty()) return false;
{
std::lock_guard<std::mutex> lock(rangeMutex);
if (rangeConfig.configured && rangeConfig.startLayerId == startLayerId &&
rangeConfig.endLayerId == endLayerId) {
return true;
}
rangeConfig.configured = true;
rangeConfig.startLayerId = std::move(startLayerId);
rangeConfig.endLayerId = std::move(endLayerId);
rangeConfig.revision++;
rangeResolution = {};
rangeResolution.configured = true;
rangeResolution.startLayerId = rangeConfig.startLayerId;
rangeResolution.endLayerId = rangeConfig.endLayerId;
rangeResolution.configRevision = rangeConfig.revision;
rangeResolution.reason = "style-not-ready";
renderSegmentConfig.source = RenderSegmentConfigSource::ContinuousRangeCompat;
renderSegmentConfig.continuousStartLayerId = rangeConfig.startLayerId;
renderSegmentConfig.continuousEndLayerId = rangeConfig.endLayerId;
renderSegmentConfig.plan = {};
renderSegmentConfig.revision++;
fallback.fetch_add(1, std::memory_order_relaxed);
}
controlRevision.fetch_add(1, std::memory_order_release);
return true;
}
bool setRenderSegmentPlan(RenderSegmentPlan plan) {
bool accepted = plan.schemaVersion == 1 && !plan.segments.empty();
if (accepted && plan.segments.front().startLayerId && !plan.segments.front().startLayerId->empty()) {
accepted = false;
}
for (std::size_t i = 1; accepted && i < plan.segments.size(); ++i) {
if (!plan.segments[i].startLayerId || plan.segments[i].startLayerId->empty()) accepted = false;
}
{
std::lock_guard<std::mutex> lock(rangeMutex);
if (renderSegmentConfig.source == RenderSegmentConfigSource::ExplicitPlan &&
renderSegmentConfig.plan == plan) {
return accepted;
}
renderSegmentConfig.source = RenderSegmentConfigSource::ExplicitPlan;
renderSegmentConfig.plan = std::move(plan);
renderSegmentConfig.continuousStartLayerId.clear();
renderSegmentConfig.continuousEndLayerId.clear();
renderSegmentConfig.revision++;
rangeConfig = {};
rangeResolution = {};
bottomRangePresent = false;
topRangePresent = false;
}
controlRevision.fetch_add(1, std::memory_order_release);
return accepted;
}
RenderSegmentConfig renderSegmentPlanConfig() const {
std::lock_guard<std::mutex> lock(rangeMutex);
return renderSegmentConfig;
}
ContinuousLayerRangeConfig continuousLayerRangeConfig() const {
std::lock_guard<std::mutex> lock(rangeMutex);
if (renderSegmentConfig.source == RenderSegmentConfigSource::ExplicitPlan) return {};
return rangeConfig;
}
void recordRenderSegmentResolution(const ResolvedRenderSegmentPlan& resolution,
std::vector<StyleLayerDiagnostics> styleLayers) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (resolution.configRevision != renderSegmentConfig.revision && resolution.configured) return;
std::vector<std::tuple<RenderSegmentStrategy, int32_t, int32_t>> signature;
signature.reserve(resolution.segments.size());
for (const auto& segment : resolution.segments) {
signature.emplace_back(segment.strategy, segment.beginIndex, segment.endExclusive);
}
const bool topologyChanged = signature != renderSegmentTopologySignature;
if (topologyChanged) {
++renderSegmentTopologyRevision;
renderSegmentTopologySignature = signature;
}
RenderSegmentDiagnostics next;
next.enabled = isEnabled();
next.configured = resolution.configured;
next.valid = resolution.valid;
next.configSource = resolution.configSource;
next.configRevision = resolution.configRevision;
next.topologyRevision = renderSegmentTopologyRevision;
next.styleLayerCount = static_cast<uint32_t>(styleLayers.size());
next.coverageComplete = renderSegmentCoverageComplete(resolution);
next.fallback = resolution.configured && !resolution.valid;
next.lastReason = resolution.reason;
next.styleLayers = std::move(styleLayers);
next.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
next.fastPathActive = fastPathActive.load(std::memory_order_acquire);
next.fastPathFallbackReason = particleFastPathMissReasonText(fastPathReason.load(std::memory_order_acquire));
next.fastPathHit = fastPathHit.load(std::memory_order_relaxed);
next.fastPathMiss = fastPathMiss.load(std::memory_order_relaxed);
next.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
next.segments.reserve(resolution.segments.size());
for (const auto& segment : resolution.segments) {
RenderSegmentDiagnosticsEntry entry;
entry.segmentIndex = segment.segmentIndex;
entry.strategy = segment.strategy;
entry.configuredStartLayerId = segment.configuredStartLayerId;
entry.beginIndex = segment.beginIndex;
entry.endExclusive = segment.endExclusive;
if (segment.beginIndex >= 0 && segment.beginIndex < static_cast<int32_t>(next.styleLayers.size())) {
entry.resolvedStartLayerId = next.styleLayers[segment.beginIndex].layerId;
}
if (segment.endExclusive > segment.beginIndex &&
segment.endExclusive <= static_cast<int32_t>(next.styleLayers.size())) {
entry.resolvedEndLayerId = next.styleLayers[segment.endExclusive - 1].layerId;
}
if (segment.beginIndex >= 0 && segment.endExclusive <= static_cast<int32_t>(next.styleLayers.size())) {
entry.layers.assign(next.styleLayers.begin() + segment.beginIndex,
next.styleLayers.begin() + segment.endExclusive);
}
const auto dependencies = renderSegmentDependencies(next.styleLayers,
segment.beginIndex, segment.endExclusive);
entry.dependentSourceIds = dependencies.sourceIds;
entry.unknownDependencies = dependencies.globalPrerequisites;
if (dependencies.unknown) entry.unknownDependencies.push_back("unknown/source-or-custom");
entry.placementCapable = renderSegmentHasPlacementCapableLayer(
next.styleLayers, segment.beginIndex, segment.endExclusive);
if (segment.strategy == RenderSegmentStrategy::Retained) {
++next.retainedSegmentCount;
entry.cache.emplace();
if (!topologyChanged && segment.segmentIndex < renderSegmentDiagnostics.segments.size() &&
renderSegmentDiagnostics.segments[segment.segmentIndex].cache) {
entry.cache = renderSegmentDiagnostics.segments[segment.segmentIndex].cache;
}
} else {
++next.continuousSegmentCount;
}
next.segments.push_back(std::move(entry));
}
next.segmentCount = static_cast<uint32_t>(next.segments.size());
renderSegmentDiagnostics = std::move(next);
}
void recordRendererReadiness(RendererReadinessDiagnostics readiness) {
std::lock_guard<std::mutex> lock(rangeMutex);
renderSegmentDiagnostics.styleLoaded = readiness.styleLoaded;
renderSegmentDiagnostics.imageManagerLoaded = readiness.imageManagerLoaded;
renderSegmentDiagnostics.renderOrchestratorLoaded = readiness.renderOrchestratorLoaded;
renderSegmentDiagnostics.spriteLoaded = readiness.spriteLoaded;
renderSegmentDiagnostics.hasFadingTiles = readiness.hasFadingTiles;
renderSegmentDiagnostics.contentRevision = readiness.contentRevision;
renderSegmentDiagnostics.sources = std::move(readiness.sources);
auto mutation = std::move(readiness.lastMutation);
mutation.affectedSegmentIndices.clear();
if (mutation.sequence != 0) {
if (mutation.sourceId) {
for (const auto& segment : renderSegmentDiagnostics.segments) {
if (std::find(segment.dependentSourceIds.begin(),
segment.dependentSourceIds.end(),
*mutation.sourceId) != segment.dependentSourceIds.end()) {
mutation.affectedSegmentIndices.push_back(segment.segmentIndex);
}
}
mutation.dependencyScope = mutation.affectedSegmentIndices.empty()
? "source-not-in-render-segments"
: "source-local-diagnostic";
} else {
mutation.dependencyScope = "shared/global/unknown";
}
}
renderSegmentDiagnostics.lastMutation = std::move(mutation);
}
RenderSegmentDiagnostics renderSegmentSnapshot() const {
std::lock_guard<std::mutex> lock(rangeMutex);
auto result = renderSegmentDiagnostics;
result.enabled = isEnabled();
result.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
result.fastPathActive = fastPathActive.load(std::memory_order_acquire);
result.fastPathFallbackReason = particleFastPathMissReasonText(fastPathReason.load(std::memory_order_acquire));
result.fastPathHit = fastPathHit.load(std::memory_order_relaxed);
result.fastPathMiss = fastPathMiss.load(std::memory_order_relaxed);
result.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
return result;
}
void recordSegmentCacheState(uint32_t segmentIndex,
RetainedRangeCacheState state,
uint64_t contentRevision = 0) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (segmentIndex >= renderSegmentDiagnostics.segments.size()) return;
auto& entry = renderSegmentDiagnostics.segments[segmentIndex];
if (!entry.cache) return;
entry.cache->cacheState = retainedRangeCacheStateText(state);
entry.cache->contentRevision = contentRevision;
}
void recordSegmentExecution(uint32_t index, bool sourceReady, bool sourceStable,
bool supported, uint64_t sourceRevision,
const char* decision, const char* fallbackReason) {
updateSegmentCache(index, [&](auto& value) {
value.sourceReady = sourceReady;
value.sourceStable = sourceStable;
value.supported = supported;
value.sourceRevision = sourceRevision;
value.finalDecision = decision;
if (fallbackReason) value.lastFallbackReason = fallbackReason;
if (std::string_view(decision) == "fallback") ++value.fallbackCount;
});
}
void recordMainBufferExecution(uint64_t topologyRevision,
bool placementChanged,
float symbolFadeChange,
bool placementTransitionActive,
bool crossSourceCollisions,
const char* mode,
const char* globalLiveFallbackReason,
const std::vector<bool>& placementGates,
const std::vector<std::string>& actualExecutions) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (topologyRevision != renderSegmentTopologyRevision ||
placementGates.size() != renderSegmentDiagnostics.segments.size() ||
actualExecutions.size() != renderSegmentDiagnostics.segments.size()) return;
auto& diagnostics = renderSegmentDiagnostics;
diagnostics.placementChanged = placementChanged;
diagnostics.symbolFadeChange = symbolFadeChange;
diagnostics.placementTransitionActive = placementTransitionActive;
diagnostics.crossSourceCollisions = crossSourceCollisions;
diagnostics.mainBufferExecutionMode = mode;
diagnostics.globalLiveFallbackReason = globalLiveFallbackReason;
for (std::size_t i = 0; i < diagnostics.segments.size(); ++i) {
diagnostics.segments[i].placementGatePassed = placementGates[i];
diagnostics.segments[i].actualExecution = actualExecutions[i];
}
}
void recordSegmentBuild(uint32_t index) { updateSegmentCache(index, [](auto& value) { ++value.build; }); }
void recordSegmentReuse(uint32_t index) { updateSegmentCache(index, [](auto& value) { ++value.reuse; }); }
void recordSegmentInvalidation(uint32_t index, RetainedUnderlayReason reason) {
updateSegmentCache(index, [reason](auto& value) {
++value.invalidation;
value.lastInvalidationReason = retainedUnderlayReasonText(reason);
});
}
void recordSegmentTextureCreate(uint32_t index) {
updateSegmentCache(index, [](auto& value) { ++value.textureCreate; });
}
void recordSegmentCompositeCreate(uint32_t index) {
updateSegmentCache(index, [](auto& value) { ++value.compositeCreate; });
}
void recordSegmentResourceRelease(uint32_t index, uint64_t topologyRevision) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (topologyRevision != renderSegmentTopologyRevision ||
index >= renderSegmentDiagnostics.segments.size()) return;
auto& cache = renderSegmentDiagnostics.segments[index].cache;
if (cache) ++cache->resourceRelease;
}
void recordRangeResolution(const ContinuousLayerRangeResolution& value, bool bottom, bool top) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (value.configRevision != rangeConfig.revision) return;
const bool changed = rangeResolution.configRevision != value.configRevision ||
rangeResolution.valid != value.valid ||
rangeResolution.startIndex != value.startIndex ||
rangeResolution.endIndex != value.endIndex ||
rangeResolution.reason != value.reason;
rangeResolution = value;
bottomRangePresent = bottom;
topRangePresent = top;
if (changed && value.configured && !value.valid) {
fallback.fetch_add(1, std::memory_order_relaxed);
}
}
RetainedUnderlayDiagnostics snapshot() const {
RetainedUnderlayDiagnostics result;
result.enabled = isEnabled();
result.build = build.load(std::memory_order_relaxed);
result.reuse = reuse.load(std::memory_order_relaxed);
result.fallback = fallback.load(std::memory_order_relaxed);
result.invalidation = invalidation.load(std::memory_order_relaxed);
result.lastReason = retainedUnderlayReasonText(lastReason.load(std::memory_order_acquire));
result.bottomBuild = bottomBuild.load(std::memory_order_relaxed);
result.bottomReuse = bottomReuse.load(std::memory_order_relaxed);
result.bottomInvalidation = bottomInvalidation.load(std::memory_order_relaxed);
result.topBuild = topBuild.load(std::memory_order_relaxed);
result.topReuse = topReuse.load(std::memory_order_relaxed);
result.topInvalidation = topInvalidation.load(std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(rangeMutex);
result.rangeConfigured = rangeResolution.configured;
result.rangeValid = rangeResolution.valid;
result.startLayerId = rangeResolution.startLayerId;
result.endLayerId = rangeResolution.endLayerId;
result.startIndex = rangeResolution.startIndex;
result.endIndex = rangeResolution.endIndex;
result.continuousBegin = rangeResolution.begin();
result.continuousEndExclusive = rangeResolution.endExclusive();
result.bottomPresent = bottomRangePresent;
result.topPresent = topRangePresent;
result.rangeFallback = rangeResolution.configured && !rangeResolution.valid;
if (result.rangeFallback) {
result.lastReason = rangeResolution.reason;
}
}
result.bottomCacheState = retainedRangeCacheStateText(bottomCacheState.load(std::memory_order_acquire));
result.bottomLastInvalidationReason =
retainedUnderlayReasonText(bottomLastInvalidationReason.load(std::memory_order_acquire));
result.bottomTextureCreate = bottomTextureCreate.load(std::memory_order_relaxed);
result.bottomCompositeCreate = bottomCompositeCreate.load(std::memory_order_relaxed);
result.bottomResourceRelease = bottomResourceRelease.load(std::memory_order_relaxed);
result.topCacheState = retainedRangeCacheStateText(topCacheState.load(std::memory_order_acquire));
result.topLastInvalidationReason =
retainedUnderlayReasonText(topLastInvalidationReason.load(std::memory_order_acquire));
result.topTextureCreate = topTextureCreate.load(std::memory_order_relaxed);
result.topCompositeCreate = topCompositeCreate.load(std::memory_order_relaxed);
result.topResourceRelease = topResourceRelease.load(std::memory_order_relaxed);
result.fastPathEligible = fastPathEligible.load(std::memory_order_acquire);
result.fastPathActive = fastPathActive.load(std::memory_order_acquire);
result.fastPathFallbackReason =
particleFastPathMissReasonText(fastPathReason.load(std::memory_order_acquire));
result.fastPathHit = fastPathHit.load(std::memory_order_relaxed);
result.fastPathMiss = fastPathMiss.load(std::memory_order_relaxed);
result.fullCpuFrame = fullCpuFrame.load(std::memory_order_relaxed);
return result;
}
void recordBuild() noexcept { build.fetch_add(1, std::memory_order_relaxed); }
void recordReuse() noexcept { reuse.fetch_add(1, std::memory_order_relaxed); }
void recordFallback() noexcept { fallback.fetch_add(1, std::memory_order_relaxed); }
void recordInvalidation(RetainedUnderlayReason reason) noexcept {
lastReason.store(reason, std::memory_order_release);
invalidation.fetch_add(1, std::memory_order_relaxed);
}
void recordRangeBuild(bool top) noexcept {
(top ? topBuild : bottomBuild).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeReuse(bool top) noexcept {
(top ? topReuse : bottomReuse).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeInvalidation(bool top) noexcept {
(top ? topInvalidation : bottomInvalidation).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeCacheState(bool top, RetainedRangeCacheState state) noexcept {
(top ? topCacheState : bottomCacheState).store(state, std::memory_order_release);
}
void recordRangeInvalidationReason(bool top, RetainedUnderlayReason reason) noexcept {
(top ? topLastInvalidationReason : bottomLastInvalidationReason).store(reason, std::memory_order_release);
}
void recordRangeTextureCreate(bool top) noexcept {
(top ? topTextureCreate : bottomTextureCreate).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeCompositeCreate(bool top) noexcept {
(top ? topCompositeCreate : bottomCompositeCreate).fetch_add(1, std::memory_order_relaxed);
}
void recordRangeResourceRelease(bool top) noexcept {
(top ? topResourceRelease : bottomResourceRelease).fetch_add(1, std::memory_order_relaxed);
}
void recordFastPathEligibility(bool eligible) noexcept {
fastPathEligible.store(eligible, std::memory_order_release);
}
void recordFastPathDecision(bool active, ParticleFastPathMissReason reason) noexcept {
fastPathActive.store(active, std::memory_order_release);
fastPathReason.store(reason, std::memory_order_release);
(active ? fastPathHit : fastPathMiss).fetch_add(1, std::memory_order_relaxed);
}
void recordFullCpuFrame() noexcept {
fullCpuFrame.fetch_add(1, std::memory_order_relaxed);
fastPathActive.store(false, std::memory_order_release);
}
private:
template <typename Fn>
void updateSegmentCache(uint32_t index, Fn&& fn) {
std::lock_guard<std::mutex> lock(rangeMutex);
if (index >= renderSegmentDiagnostics.segments.size()) return;
auto& cache = renderSegmentDiagnostics.segments[index].cache;
if (cache) fn(*cache);
}
std::atomic<bool> enabled{true};
std::atomic<uint64_t> controlRevision{0};
std::atomic<uint64_t> build{0};
std::atomic<uint64_t> reuse{0};
std::atomic<uint64_t> fallback{0};
std::atomic<uint64_t> invalidation{0};
std::atomic<RetainedUnderlayReason> lastReason{RetainedUnderlayReason::None};
std::atomic<uint64_t> bottomBuild{0};
std::atomic<uint64_t> bottomReuse{0};
std::atomic<uint64_t> bottomInvalidation{0};
std::atomic<uint64_t> topBuild{0};
std::atomic<uint64_t> topReuse{0};
std::atomic<uint64_t> topInvalidation{0};
std::atomic<RetainedRangeCacheState> bottomCacheState{RetainedRangeCacheState::Disabled};
std::atomic<RetainedUnderlayReason> bottomLastInvalidationReason{RetainedUnderlayReason::None};
std::atomic<uint64_t> bottomTextureCreate{0};
std::atomic<uint64_t> bottomCompositeCreate{0};
std::atomic<uint64_t> bottomResourceRelease{0};
std::atomic<RetainedRangeCacheState> topCacheState{RetainedRangeCacheState::Disabled};
std::atomic<RetainedUnderlayReason> topLastInvalidationReason{RetainedUnderlayReason::None};
std::atomic<uint64_t> topTextureCreate{0};
std::atomic<uint64_t> topCompositeCreate{0};
std::atomic<uint64_t> topResourceRelease{0};
std::atomic<bool> fastPathEligible{false};
std::atomic<bool> fastPathActive{false};
std::atomic<ParticleFastPathMissReason> fastPathReason{ParticleFastPathMissReason::NoPreparedTree};
std::atomic<uint64_t> fastPathHit{0};
std::atomic<uint64_t> fastPathMiss{0};
std::atomic<uint64_t> fullCpuFrame{0};
mutable std::mutex rangeMutex;
ContinuousLayerRangeConfig rangeConfig;
ContinuousLayerRangeResolution rangeResolution;
bool bottomRangePresent = false;
bool topRangePresent = false;
RenderSegmentConfig renderSegmentConfig;
RenderSegmentDiagnostics renderSegmentDiagnostics;
uint64_t renderSegmentTopologyRevision = 0;
std::vector<std::tuple<RenderSegmentStrategy, int32_t, int32_t>> renderSegmentTopologySignature;
};
} // namespace mln

View File

@@ -0,0 +1,69 @@
#pragma once
#include <mln/renderer/render_source.hpp>
#include <mln/renderer/render_tree.hpp>
#include <mln/style/sources/raster_array_source.hpp>
#include <mln/style/sources/raster_array_source_impl.hpp>
namespace mln {
class RasterArraySourceRenderData final : public RenderItem {
public:
explicit RasterArraySourceRenderData(std::string name_)
: name(std::move(name_)) {}
private:
void upload(gfx::UploadPass&) const override {}
void render(PaintParameters&) const override {}
bool hasRenderPass(RenderPass) const override { return false; }
const std::string& getName() const override { return name; }
void updateDebugDrawables(DebugLayerGroupMap&, PaintParameters&) const override {}
std::string name;
};
class RenderRasterArraySource final : public RenderSource {
public:
explicit RenderRasterArraySource(Immutable<style::RasterArraySource::Impl> impl)
: RenderSource(std::move(impl)) {}
bool isLoaded() const override {
return impl().field.has_value() || static_cast<bool>(impl().v4RendererState.color) ||
static_cast<bool>(impl().v4RendererState.particle);
}
void update(Immutable<style::Source::Impl> value,
const std::vector<Immutable<style::LayerProperties>>&,
bool needsRendering,
bool,
const TileParameters&) override {
baseImpl = std::move(value);
enabled = needsRendering && isLoaded();
}
std::unique_ptr<RenderItem> createRenderItem() override {
assert(renderData);
return std::move(renderData);
}
void prepare(const SourcePrepareParameters&) override {
assert(!renderData);
renderData = std::make_unique<RasterArraySourceRenderData>(baseImpl->id);
}
void updateFadingTiles() override {}
bool hasFadingTiles() const override { return false; }
std::unordered_map<std::string, std::vector<Feature>> queryRenderedFeatures(
const ScreenLineString&,
const TransformState&,
const std::unordered_map<std::string, const RenderLayer*>&,
const RenderedQueryOptions&,
const mat4&) const override {
return {};
}
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
void reduceMemoryUse() override {}
void dumpDebugLogs() const override {}
const style::RasterArraySource::Impl& impl() const {
return static_cast<const style::RasterArraySource::Impl&>(*baseImpl);
}
private:
std::unique_ptr<RasterArraySourceRenderData> renderData;
};
} // namespace mln

View File

@@ -486,6 +486,23 @@ bool RenderTileSource::hasFadingTiles() const {
return tilePyramid.hasFadingTiles();
}
RenderSourceReadinessStats RenderTileSource::getReadinessDiagnostics() const {
RenderSourceReadinessStats result;
result.tileBacked = true;
result.tileCount = static_cast<uint32_t>(tilePyramid.getTiles().size());
uint32_t incomplete = 0;
uint32_t loaded = 0;
for (const auto& entry : tilePyramid.getTiles()) {
const auto& tile = *entry.second;
if (!tile.isComplete()) ++incomplete;
if (tile.isLoaded()) ++loaded;
}
result.incompleteTileCount = incomplete;
result.loadedTileCount = loaded;
result.hasFadingTiles = tilePyramid.hasFadingTiles();
return result;
}
RenderTiles RenderTileSource::getRenderTiles() const {
if (!filteredRenderTiles) {
auto result = std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>();

View File

@@ -23,6 +23,7 @@ public:
void prepare(const SourcePrepareParameters&) override;
void updateFadingTiles() override;
bool hasFadingTiles() const override;
RenderSourceReadinessStats getReadinessDiagnostics() const override;
RenderTiles getRenderTiles() const override;
RenderTiles getRenderTilesSortedByYPosition() const override;

View File

@@ -9,8 +9,10 @@
#include <mln/text/glyph.hpp>
#include <mln/util/chrono.hpp>
#include <mln/util/immutable.hpp>
#include <mln/renderer/retained_underlay_runtime_state.hpp>
#include <numbers>
#include <memory>
#include <vector>
#include <mapbox/std/weak.hpp>
@@ -50,6 +52,8 @@ public:
const bool fastPFOREnabled = false;
std::shared_ptr<RetainedUnderlayRuntimeState> retainedUnderlayState;
double tileLodMinRadius = 3;
double tileLodScale = 1;
double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi;

View File

@@ -287,6 +287,33 @@ const std::vector<TextureInfo> HeatmapTextureShaderInfo::textures = {
TextureInfo{"u_color_ramp", idHeatmapColorRampTexture},
};
// Particle GPU Validation (30-02, OpenGL validation only)
using ParticleGpuValidationShaderInfo = ShaderInfo<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::OpenGL>;
const std::vector<UniformBlockInfo> ParticleGpuValidationShaderInfo::uniformBlocks = {
UniformBlockInfo{"ParticleGpuValidationUBO", idParticleGpuValidationUBO},
};
const std::vector<AttributeInfo> ParticleGpuValidationShaderInfo::attributes = {
AttributeInfo{"a_pos", idParticleGpuValidationPosVertexAttribute},
};
const std::vector<TextureInfo> ParticleGpuValidationShaderInfo::textures = {
TextureInfo{"u_previous", idParticleGpuValidationPreviousTexture},
TextureInfo{"u_mask_texture", idParticleGpuValidationMaskTexture},
TextureInfo{"u_field_texture", idParticleGpuValidationFieldTexture},
TextureInfo{"u_next_field_texture", idParticleGpuValidationNextFieldTexture},
TextureInfo{"u_state_texture", idParticleGpuValidationStateTexture},
};
using RetainedUnderlayCompositeShaderInfo =
ShaderInfo<BuiltIn::RetainedUnderlayCompositeShader, gfx::Backend::Type::OpenGL>;
const std::vector<UniformBlockInfo> RetainedUnderlayCompositeShaderInfo::uniformBlocks = {};
const std::vector<AttributeInfo> RetainedUnderlayCompositeShaderInfo::attributes = {
AttributeInfo{"a_pos", idRetainedUnderlayCompositePosVertexAttribute},
};
const std::vector<TextureInfo> RetainedUnderlayCompositeShaderInfo::textures = {
TextureInfo{"u_image", idRetainedUnderlayCompositeTexture},
};
// Hillshade Prepare
using HillshadePrepareShaderInfo = ShaderInfo<BuiltIn::HillshadePrepareShader, gfx::Backend::Type::OpenGL>;

View File

@@ -0,0 +1,14 @@
#include <mln/shaders/mtl/particle_gpu_validation.hpp>
#include <mln/shaders/shader_defines.hpp>
namespace mln::shaders {
using Source = ShaderSource<BuiltIn::ParticleGpuValidationShader, gfx::Backend::Type::Metal>;
const std::array<AttributeInfo, 1> Source::attributes = {
AttributeInfo{0, gfx::AttributeDataType::Short2, particleGpuValidationUBOCount, idParticleGpuValidationPosVertexAttribute},
};
const std::array<TextureInfo, 5> Source::textures = {
TextureInfo{0, idParticleGpuValidationPreviousTexture}, TextureInfo{1, idParticleGpuValidationMaskTexture},
TextureInfo{2, idParticleGpuValidationFieldTexture}, TextureInfo{3, idParticleGpuValidationNextFieldTexture},
TextureInfo{4, idParticleGpuValidationStateTexture},
};
} // namespace mln::shaders

View File

@@ -0,0 +1,12 @@
#include <mln/shaders/mtl/retained_underlay_composite.hpp>
#include <mln/shaders/shader_defines.hpp>
namespace mln::shaders {
using Source = ShaderSource<BuiltIn::RetainedUnderlayCompositeShader, gfx::Backend::Type::Metal>;
const std::array<AttributeInfo, 1> Source::attributes = {
AttributeInfo{0, gfx::AttributeDataType::Short2, 0, idRetainedUnderlayCompositePosVertexAttribute},
};
const std::array<TextureInfo, 1> Source::textures = {
TextureInfo{0, idRetainedUnderlayCompositeTexture},
};
} // namespace mln::shaders

View File

@@ -9,12 +9,14 @@
#include <mln/shaders/program_parameters.hpp>
#include <mln/shaders/shader_manifest.hpp>
#include <mln/util/logging.hpp>
#include <mln/util/hash.hpp>
#include <Metal/MTLLibrary.hpp>
#include <Metal/MTLRenderPass.hpp>
#include <Metal/MTLRenderPipeline.hpp>
#include <cstring>
#include <type_traits>
#include <utility>
#include <algorithm>
@@ -84,12 +86,6 @@ MTLRenderPipelineStatePtr ShaderProgram::getRenderPipelineState(const gfx::Rende
const MTLVertexDescriptorPtr& vertexDescriptor,
const gfx::ColorMode& colorMode,
const std::optional<std::size_t> reuseHash) const {
if (reuseHash.has_value()) {
// we'd like to reuse a previous value
if (auto it = renderPipelineStateCache.find(reuseHash.value()); it != renderPipelineStateCache.end())
return it->second;
}
auto pool = NS::TransferPtr(NS::AutoreleasePool::alloc()->init());
const auto& renderableResource = renderable.getResource<RenderableResource>();
@@ -115,6 +111,23 @@ MTLRenderPipelineStatePtr ShaderProgram::getRenderPipelineState(const gfx::Rende
}
}
std::optional<std::size_t> cacheKey;
if (reuseHash.has_value()) {
cacheKey = reuseHash.value();
mln::util::hash_combine(*cacheKey, static_cast<std::underlying_type_t<MTL::PixelFormat>>(colorFormat));
mln::util::hash_combine(
*cacheKey,
static_cast<std::underlying_type_t<MTL::PixelFormat>>(
depthFormat.value_or(MTL::PixelFormat::PixelFormatInvalid)));
mln::util::hash_combine(
*cacheKey,
static_cast<std::underlying_type_t<MTL::PixelFormat>>(
stencilFormat.value_or(MTL::PixelFormat::PixelFormatInvalid)));
if (auto it = renderPipelineStateCache.find(*cacheKey); it != renderPipelineStateCache.end()) {
return it->second;
}
}
auto desc = NS::TransferPtr(MTL::RenderPipelineDescriptor::alloc()->init());
desc->setLabel(NS::String::string(shaderName.data(), NS::UTF8StringEncoding));
desc->setVertexFunction(vertexFunction.get());
@@ -171,9 +184,9 @@ MTLRenderPipelineStatePtr ShaderProgram::getRenderPipelineState(const gfx::Rende
assert(false);
}
if (reuseHash.has_value()) {
// store the value for future reuse
renderPipelineStateCache[reuseHash.value()] = rps;
if (cacheKey.has_value()) {
// Pipeline attachment formats are part of Metal pipeline compatibility.
renderPipelineStateCache[*cacheKey] = rps;
}
return rps;

View File

@@ -6,42 +6,45 @@ namespace mln {
using namespace shaders;
MBGL_DEFINE_ENUM(BuiltIn,
{{BuiltIn::None, "None"},
{BuiltIn::Prelude, "Prelude"},
{BuiltIn::ClippingMaskProgram, "ClippingMaskProgram"},
{BuiltIn::BackgroundShader, "BackgroundShader"},
{BuiltIn::BackgroundPatternShader, "BackgroundPatternShader"},
{BuiltIn::CircleShader, "CircleShader"},
{BuiltIn::CollisionBoxShader, "CollisionBoxShader"},
{BuiltIn::CollisionCircleShader, "CollisionCircleShader"},
{BuiltIn::CustomGeometryShader, "CustomGeometryShader"},
{BuiltIn::CustomSymbolIconShader, "CustomSymbolIconShader"},
{BuiltIn::DebugShader, "DebugShader"},
{BuiltIn::FillShader, "FillShader"},
{BuiltIn::FillOutlineShader, "FillOutlineShader"},
{BuiltIn::FillPatternShader, "FillPatternShader"},
{BuiltIn::FillOutlinePatternShader, "FillOutlinePatternShader"},
{BuiltIn::FillOutlineTriangulatedShader, "FillOutlineTriangulatedShader"},
{BuiltIn::FillExtrusionShader, "FillExtrusionShader"},
{BuiltIn::FillExtrusionInstancedShader, "FillExtrusionInstancedShader"},
{BuiltIn::FillExtrusionPatternShader, "FillExtrusionPatternShader"},
{BuiltIn::FillExtrusionPatternInstancedShader, "FillExtrusionPatternInstancedShader"},
{BuiltIn::HeatmapShader, "HeatmapShader"},
{BuiltIn::HeatmapTextureShader, "HeatmapTextureShader"},
{BuiltIn::HillshadePrepareShader, "HillshadePrepareShader"},
{BuiltIn::ColorReliefShader, "ColorReliefShader"},
{BuiltIn::HillshadeShader, "HillshadeShader"},
{BuiltIn::LineShader, "LineShader"},
{BuiltIn::LineGradientShader, "LineGradientShader"},
{BuiltIn::LinePatternShader, "LinePatternShader"},
{BuiltIn::LocationIndicatorShader, "LocationIndicatorShader"},
{BuiltIn::LocationIndicatorTexturedShader, "LocationIndicatorTexturedShader"},
{BuiltIn::LineSDFShader, "LineSDFShader"},
{BuiltIn::RasterShader, "RasterShader"},
{BuiltIn::SymbolIconShader, "SymbolIconShader"},
{BuiltIn::SymbolSDFShader, "SymbolSDFShader"},
{BuiltIn::SymbolTextAndIconShader, "SymbolTextAndIconShader"},
{BuiltIn::WideVectorShader, "WideVectorShader"}});
MBGL_DEFINE_ENUM(BuiltIn, {
{BuiltIn::None, "None"},
{BuiltIn::Prelude, "Prelude"},
{BuiltIn::ClippingMaskProgram, "ClippingMaskProgram"},
{BuiltIn::BackgroundShader, "BackgroundShader"},
{BuiltIn::BackgroundPatternShader, "BackgroundPatternShader"},
{BuiltIn::CircleShader, "CircleShader"},
{BuiltIn::CollisionBoxShader, "CollisionBoxShader"},
{BuiltIn::CollisionCircleShader, "CollisionCircleShader"},
{BuiltIn::CustomGeometryShader, "CustomGeometryShader"},
{BuiltIn::CustomSymbolIconShader, "CustomSymbolIconShader"},
{BuiltIn::DebugShader, "DebugShader"},
{BuiltIn::FillShader, "FillShader"},
{BuiltIn::FillOutlineShader, "FillOutlineShader"},
{BuiltIn::FillPatternShader, "FillPatternShader"},
{BuiltIn::FillOutlinePatternShader, "FillOutlinePatternShader"},
{BuiltIn::FillOutlineTriangulatedShader, "FillOutlineTriangulatedShader"},
{BuiltIn::FillExtrusionShader, "FillExtrusionShader"},
{BuiltIn::FillExtrusionInstancedShader, "FillExtrusionInstancedShader"},
{BuiltIn::FillExtrusionPatternShader, "FillExtrusionPatternShader"},
{BuiltIn::FillExtrusionPatternInstancedShader, "FillExtrusionPatternInstancedShader"},
{BuiltIn::HeatmapShader, "HeatmapShader"},
{BuiltIn::HeatmapTextureShader, "HeatmapTextureShader"},
{BuiltIn::ParticleGpuValidationShader, "ParticleGpuValidationShader"},
{BuiltIn::RetainedUnderlayCompositeShader, "RetainedUnderlayCompositeShader"},
{BuiltIn::HillshadePrepareShader, "HillshadePrepareShader"},
{BuiltIn::ColorReliefShader, "ColorReliefShader"},
{BuiltIn::HillshadeShader, "HillshadeShader"},
{BuiltIn::LineShader, "LineShader"},
{BuiltIn::LineGradientShader, "LineGradientShader"},
{BuiltIn::LinePatternShader, "LinePatternShader"},
{BuiltIn::LocationIndicatorShader, "LocationIndicatorShader"},
{BuiltIn::LocationIndicatorTexturedShader, "LocationIndicatorTexturedShader"},
{BuiltIn::LineSDFShader, "LineSDFShader"},
{BuiltIn::RasterShader, "RasterShader"},
{BuiltIn::SymbolIconShader, "SymbolIconShader"},
{BuiltIn::SymbolSDFShader, "SymbolSDFShader"},
{BuiltIn::SymbolTextAndIconShader, "SymbolTextAndIconShader"},
{BuiltIn::WideVectorShader, "WideVectorShader"}
});
} // namespace mln

View File

@@ -10,6 +10,7 @@
#include <mln/style/sources/raster_dem_source.hpp>
#include <mln/style/sources/vector_source.hpp>
#include <mln/style/sources/image_source.hpp>
#include <mln/style/sources/raster_array_source.hpp>
#include <mln/util/geo.hpp>
namespace mln {
@@ -202,6 +203,109 @@ std::optional<std::unique_ptr<Source>> convertImageSource(const std::string& id,
return {std::move(result)};
}
std::optional<std::unique_ptr<Source>> convertRasterArraySource(const std::string& id,
const Convertible& value,
Error& error) {
if (const auto hostSubmissionValue = objectMember(value, "host-field-submission")) {
const auto hostSubmission = toBool(*hostSubmissionValue);
if (!hostSubmission || !*hostSubmission) {
error.message = "raster-array host-field-submission must be true";
return std::nullopt;
}
return {std::make_unique<RasterArraySource>(id)};
}
if (const auto localFieldsValue = objectMember(value, "production-fields")) {
if (!isArray(*localFieldsValue) || arrayLength(*localFieldsValue) < 2) {
error.message = "raster-array production-fields must contain at least two entries";
return std::nullopt;
}
std::vector<RasterArrayLocalFieldDescriptor> localFields;
for (size_t index = 0; index < arrayLength(*localFieldsValue); ++index) {
const auto entry = arrayMember(*localFieldsValue, index);
const auto metadata = objectMember(entry, "metadata");
const auto field = objectMember(entry, "field");
if (!metadata || !field) {
error.message = "raster-array production field requires metadata and field";
return std::nullopt;
}
const auto metadataURL = toString(*metadata);
const auto fieldURL = toString(*field);
if (!metadataURL || !fieldURL) {
error.message = "raster-array production field inputs must be strings";
return std::nullopt;
}
localFields.push_back({*metadataURL, *fieldURL});
}
return {std::make_unique<RasterArraySource>(id, std::move(localFields))};
}
const auto fixture = objectMember(value, "fixture");
const auto metadata = objectMember(value, "metadata");
const auto field = objectMember(value, "field");
if (!fixture || !metadata || !field) {
error.message = "raster-array source requires fixture, metadata, and field";
return std::nullopt;
}
const auto fixtureID = toString(*fixture);
const auto metadataURL = toString(*metadata);
const auto fieldURL = toString(*field);
if (!fixtureID || !metadataURL || !fieldURL) {
error.message = "raster-array fixture inputs must be strings";
return std::nullopt;
}
const auto nextFixture = objectMember(value, "next-fixture");
const auto nextMetadata = objectMember(value, "next-metadata");
const auto nextField = objectMember(value, "next-field");
if (static_cast<bool>(nextFixture) != static_cast<bool>(nextMetadata) ||
static_cast<bool>(nextFixture) != static_cast<bool>(nextField)) {
error.message = "raster-array next fixture requires next-fixture, next-metadata, and next-field";
return std::nullopt;
}
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields;
if (const auto runtimeFieldsValue = objectMember(value, "runtime-fields")) {
if (!isArray(*runtimeFieldsValue)) {
error.message = "raster-array runtime-fields must be an array";
return std::nullopt;
}
for (size_t index = 0; index < arrayLength(*runtimeFieldsValue); ++index) {
const auto runtimeField = arrayMember(*runtimeFieldsValue, index);
const auto runtimeFixture = objectMember(runtimeField, "fixture");
const auto runtimeMetadata = objectMember(runtimeField, "metadata");
const auto runtimeData = objectMember(runtimeField, "field");
if (!runtimeFixture || !runtimeMetadata || !runtimeData) {
error.message = "raster-array runtime field requires fixture, metadata, and field";
return std::nullopt;
}
const auto runtimeFixtureID = toString(*runtimeFixture);
const auto runtimeMetadataURL = toString(*runtimeMetadata);
const auto runtimeFieldURL = toString(*runtimeData);
if (!runtimeFixtureID || !runtimeMetadataURL || !runtimeFieldURL) {
error.message = "raster-array runtime field inputs must be strings";
return std::nullopt;
}
runtimeFields.push_back({*runtimeFixtureID, *runtimeMetadataURL, *runtimeFieldURL});
}
}
if (!nextFixture) {
return {std::make_unique<RasterArraySource>(
id, *fixtureID, *metadataURL, *fieldURL, "", "", "", std::move(runtimeFields))};
}
const auto nextFixtureID = toString(*nextFixture);
const auto nextMetadataURL = toString(*nextMetadata);
const auto nextFieldURL = toString(*nextField);
if (!nextFixtureID || !nextMetadataURL || !nextFieldURL) {
error.message = "raster-array next fixture inputs must be strings";
return std::nullopt;
}
return {std::make_unique<RasterArraySource>(id,
*fixtureID,
*metadataURL,
*fieldURL,
*nextFixtureID,
*nextMetadataURL,
*nextFieldURL,
std::move(runtimeFields))};
}
} // namespace
std::optional<std::unique_ptr<Source>> Converter<std::unique_ptr<Source>>::operator()(const Convertible& value,
@@ -234,6 +338,8 @@ std::optional<std::unique_ptr<Source>> Converter<std::unique_ptr<Source>>::opera
return convertGeoJSONSource(id, value, error);
} else if (tname == "image") {
return convertImageSource(id, value, error);
} else if (tname == "raster-array") {
return convertRasterArraySource(id, value, error);
} else {
error.message = "invalid source type";
return std::nullopt;

View File

@@ -0,0 +1,58 @@
#pragma once
#include <mln/style/layer_properties.hpp>
#include <cstdint>
#include <array>
namespace mln::style {
class ParticleRenderLayerProperties final : public LayerProperties {
public:
ParticleRenderLayerProperties(Immutable<Layer::Impl> impl,
bool active_,
uint8_t maskMode_,
uint8_t fixtureMode_,
float timeMix_,
uint32_t particleCount_,
float lineWidth_,
float particleLength_,
float particleOpacity_,
float trailOpacity_,
float trailFade_,
float advectionScale_,
bool speedColorEnabled_,
std::array<float, 4> particleColor_,
std::array<float, 4> trailColor_)
: LayerProperties(std::move(impl)),
active(active_),
maskMode(maskMode_),
fixtureMode(fixtureMode_),
timeMix(timeMix_),
particleCount(particleCount_),
lineWidth(lineWidth_),
particleLength(particleLength_),
particleOpacity(particleOpacity_),
trailOpacity(trailOpacity_),
trailFade(trailFade_),
advectionScale(advectionScale_),
speedColorEnabled(speedColorEnabled_),
particleColor(particleColor_), trailColor(trailColor_) {}
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
bool active;
uint8_t maskMode;
uint8_t fixtureMode;
float timeMix;
uint32_t particleCount;
float lineWidth;
float particleLength;
float particleOpacity;
float trailOpacity;
float trailFade;
float advectionScale;
bool speedColorEnabled;
std::array<float, 4> particleColor;
std::array<float, 4> trailColor;
};
} // namespace mln::style

View File

@@ -0,0 +1,168 @@
#include <mln/style/layers/particle_validation_layer.hpp>
#include <mln/style/conversion/constant.hpp>
#include <mln/style/conversion_impl.hpp>
#include <mln/style/layer_observer.hpp>
#include <mln/style/layers/particle_validation_layer_impl.hpp>
#include <mln/util/logging.hpp>
#include <cmath>
namespace mln {
namespace style {
namespace {
constexpr const char* ActiveProperty = "particle-validation-active";
constexpr const char* MaskModeProperty = "particle-validation-mask-mode";
constexpr const char* FixtureModeProperty = "particle-fixture-mode";
const LayerTypeInfo typeInfo{.type = "particle-validation",
.source = LayerTypeInfo::Source::NotRequired,
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
.layout = LayerTypeInfo::Layout::NotRequired,
.fadingTiles = LayerTypeInfo::FadingTiles::NotRequired,
.crossTileIndex = LayerTypeInfo::CrossTileIndex::NotRequired,
.tileKind = LayerTypeInfo::TileKind::NotRequired};
} // namespace
ParticleValidationLayer::Impl::Impl(const std::string& id)
: Layer::Impl(id, std::string()) {}
bool ParticleValidationLayer::Impl::hasLayoutDifference(const Layer::Impl&) const {
return false;
}
void ParticleValidationLayer::Impl::stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const {}
const LayerTypeInfo* ParticleValidationLayer::Impl::staticTypeInfo() noexcept {
return &typeInfo;
}
ParticleValidationLayer::ParticleValidationLayer(const std::string& layerID)
: Layer(makeMutable<Impl>(layerID)) {
Log::Info(Event::Render, "ParticleValidation: Style Layer create id=" + layerID + " (VALIDATION ONLY)");
}
ParticleValidationLayer::ParticleValidationLayer(Immutable<Impl> impl_)
: Layer(std::move(impl_)) {}
ParticleValidationLayer::~ParticleValidationLayer() {
Log::Info(Event::Render, "ParticleValidation: Style Layer destroy id=" + getID());
weakFactory.invalidateWeakPtrs();
}
const ParticleValidationLayer::Impl& ParticleValidationLayer::impl() const {
return static_cast<const Impl&>(*baseImpl);
}
Mutable<ParticleValidationLayer::Impl> ParticleValidationLayer::mutableImpl() const {
return makeMutable<Impl>(impl());
}
bool ParticleValidationLayer::isActive() const {
return impl().active;
}
uint8_t ParticleValidationLayer::getMaskMode() const {
return impl().maskMode;
}
uint8_t ParticleValidationLayer::getFixtureMode() const {
return impl().fixtureMode;
}
void ParticleValidationLayer::setActive(bool active) {
if (active == isActive()) {
return;
}
auto impl_ = mutableImpl();
impl_->active = active;
baseImpl = std::move(impl_);
observer->onLayerChanged(*this);
}
void ParticleValidationLayer::setMaskMode(uint8_t mode) {
if (mode > 2 || mode == getMaskMode()) {
return;
}
auto impl_ = mutableImpl();
impl_->maskMode = mode;
baseImpl = std::move(impl_);
observer->onLayerChanged(*this);
}
void ParticleValidationLayer::setFixtureMode(uint8_t mode) {
if (mode > 2 || mode == getFixtureMode()) {
return;
}
auto impl_ = mutableImpl();
impl_->fixtureMode = mode;
baseImpl = std::move(impl_);
observer->onLayerChanged(*this);
}
std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) {
if (name == MaskModeProperty) {
conversion::Error error;
auto converted = conversion::convert<float>(value, error);
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted) {
return conversion::Error{"particle-validation-mask-mode must be 0, 1, or 2"};
}
setMaskMode(static_cast<uint8_t>(*converted));
return std::nullopt;
}
if (name == FixtureModeProperty) {
conversion::Error error;
auto converted = conversion::convert<float>(value, error);
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted) {
return conversion::Error{"particle-fixture-mode must be 0, 1, or 2"};
}
setFixtureMode(static_cast<uint8_t>(*converted));
return std::nullopt;
}
if (name != ActiveProperty) {
return conversion::Error{"layer doesn't support this property"};
}
conversion::Error error;
auto converted = conversion::convert<bool>(value, error);
if (!converted) {
return error;
}
setActive(*converted);
return std::nullopt;
}
StyleProperty ParticleValidationLayer::getProperty(const std::string& name) const {
if (name == ActiveProperty) {
return StyleProperty(Value{isActive()}, StyleProperty::Kind::Constant);
}
if (name == MaskModeProperty) {
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
}
if (name == FixtureModeProperty) {
return StyleProperty(Value{static_cast<uint64_t>(getFixtureMode())}, StyleProperty::Kind::Constant);
}
return {};
}
Value ParticleValidationLayer::serialize() const {
auto result = Layer::serialize();
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
serializeProperty(result, getProperty(FixtureModeProperty), FixtureModeProperty, true);
return result;
}
std::unique_ptr<Layer> ParticleValidationLayer::cloneRef(const std::string& id) const {
auto impl_ = mutableImpl();
impl_->id = id;
return std::unique_ptr<Layer>(new ParticleValidationLayer(std::move(impl_)));
}
Mutable<Layer::Impl> ParticleValidationLayer::mutableBaseImpl() const {
return staticMutableCast<Layer::Impl>(mutableImpl());
}
} // namespace style
} // namespace mln

View File

@@ -0,0 +1,47 @@
#pragma once
#include <mln/style/layer_impl.hpp>
#include <mln/style/layer_properties.hpp>
#include <mln/style/layers/particle_validation_layer.hpp>
namespace mln {
namespace style {
// VALIDATION ONLY: no particle data or production API belongs here.
class ParticleValidationLayer::Impl final : public Layer::Impl {
public:
explicit Impl(const std::string& id);
Impl(const Impl&) = default;
bool hasLayoutDifference(const Layer::Impl&) const override;
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override;
bool active = true;
// VALIDATION ONLY: 0=GLOBAL, 1=OCEAN_ONLY_A, 2=OCEAN_ONLY_B.
uint8_t maskMode = 0;
// 40-01 offline Field selection: 0=uniform-east, 1=uniform-north, 2=vortex.
uint8_t fixtureMode = 0;
DECLARE_LAYER_TYPE_INFO;
};
class ParticleValidationLayerProperties final : public LayerProperties {
public:
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl,
bool active_,
uint8_t maskMode_,
uint8_t fixtureMode_)
: LayerProperties(std::move(impl)),
active(active_),
maskMode(maskMode_),
fixtureMode(fixtureMode_) {}
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
bool active;
uint8_t maskMode;
uint8_t fixtureMode;
};
} // namespace style
} // namespace mln

View File

@@ -0,0 +1,317 @@
#include <mln/style/layers/raster_particle_layer.hpp>
#include <mln/style/conversion/constant.hpp>
#include <mln/style/conversion_impl.hpp>
#include <mln/style/layer_observer.hpp>
#include <mln/style/layers/raster_particle_layer_impl.hpp>
#include <mln/style/layers/weather_color_stop.hpp>
#include <cmath>
namespace mln::style {
namespace {
constexpr const char* ActiveProperty = "raster-particle-active";
constexpr const char* MaskModeProperty = "raster-particle-mask-mode";
constexpr const char* TimeMixProperty = "raster-particle-time-mix";
constexpr const char* ParticleCountProperty = "raster-particle-particle-count";
constexpr const char* LineWidthProperty = "raster-particle-line-width";
constexpr const char* ParticleLengthProperty = "raster-particle-particle-length";
constexpr const char* ParticleOpacityProperty = "raster-particle-particle-opacity";
constexpr const char* TrailOpacityProperty = "raster-particle-trail-opacity";
constexpr const char* TrailFadeProperty = "raster-particle-trail-fade";
constexpr const char* AdvectionScaleProperty = "raster-particle-advection-scale";
constexpr const char* SpeedColorEnabledProperty = "raster-particle-speed-color-enabled";
constexpr uint32_t MinimumParticleCount = 256;
constexpr uint32_t MaximumParticleCount = 1024 * 1024;
bool isSupportedParticleCount(const uint32_t count) {
// The particle State texture is a ceil(sqrt(count)) grid. The final row
// may contain unused texels, but draw resources address only count
// particles. Keep the public validation presets explicit rather than
// accepting arbitrary resource sizes.
switch (count) {
case 256:
case 512:
case 1024:
case 2048:
case 4096:
case 8192:
case 16384:
case 65536:
case 262144:
case 1048576:
return count >= MinimumParticleCount && count <= MaximumParticleCount;
default:
return false;
}
}
const LayerTypeInfo typeInfo{.type = "raster-particle",
.source = LayerTypeInfo::Source::Required,
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
.layout = LayerTypeInfo::Layout::NotRequired,
.fadingTiles = LayerTypeInfo::FadingTiles::NotRequired,
.crossTileIndex = LayerTypeInfo::CrossTileIndex::NotRequired,
.tileKind = LayerTypeInfo::TileKind::NotRequired};
} // namespace
const LayerTypeInfo* RasterParticleLayer::Impl::staticTypeInfo() noexcept {
return &typeInfo;
}
RasterParticleLayer::RasterParticleLayer(const std::string& id, const std::string& source)
: Layer(makeMutable<Impl>(id, source)) {}
RasterParticleLayer::RasterParticleLayer(Immutable<Impl> impl_)
: Layer(std::move(impl_)) {}
RasterParticleLayer::~RasterParticleLayer() {
weakFactory.invalidateWeakPtrs();
}
const RasterParticleLayer::Impl& RasterParticleLayer::impl() const {
return static_cast<const Impl&>(*baseImpl);
}
Mutable<RasterParticleLayer::Impl> RasterParticleLayer::mutableImpl() const {
return makeMutable<Impl>(impl());
}
bool RasterParticleLayer::isActive() const {
return impl().active;
}
uint8_t RasterParticleLayer::getMaskMode() const {
return impl().maskMode;
}
void RasterParticleLayer::setActive(bool active) {
if (active != isActive()) {
auto next = mutableImpl();
next->active = active;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
void RasterParticleLayer::setMaskMode(uint8_t mode) {
if (mode <= 2 && mode != getMaskMode()) {
auto next = mutableImpl();
next->maskMode = mode;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getTimeMix() const {
return impl().timeMix;
}
void RasterParticleLayer::setTimeMix(float timeMix) {
const auto clamped = std::clamp(timeMix, 0.0f, 1.0f);
if (clamped != getTimeMix()) {
auto next = mutableImpl();
next->timeMix = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
uint32_t RasterParticleLayer::getParticleCount() const {
return impl().particleCount;
}
void RasterParticleLayer::setParticleCount(const uint32_t particleCount) {
if (isSupportedParticleCount(particleCount) && particleCount != getParticleCount()) {
auto next = mutableImpl();
next->particleCount = particleCount;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getLineWidth() const {
return impl().lineWidth;
}
void RasterParticleLayer::setLineWidth(float value) {
const auto clamped = std::clamp(value, 0.25f, 8.0f);
if (clamped != getLineWidth()) {
auto next = mutableImpl();
next->lineWidth = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getParticleLength() const {
return impl().particleLength;
}
void RasterParticleLayer::setParticleLength(float value) {
const auto clamped = std::clamp(value, 0.0005f, 0.080f);
if (clamped != getParticleLength()) {
auto next = mutableImpl();
next->particleLength = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getParticleOpacity() const {
return impl().particleOpacity;
}
void RasterParticleLayer::setParticleOpacity(float value) {
const auto clamped = std::clamp(value, 0.0f, 1.0f);
if (clamped != getParticleOpacity()) {
auto next = mutableImpl();
next->particleOpacity = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getTrailOpacity() const {
return impl().trailOpacity;
}
void RasterParticleLayer::setTrailOpacity(float value) {
const auto clamped = std::clamp(value, 0.0f, 1.0f);
if (clamped != getTrailOpacity()) {
auto next = mutableImpl();
next->trailOpacity = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getTrailFade() const {
return impl().trailFade;
}
void RasterParticleLayer::setTrailFade(float value) {
const auto clamped = std::clamp(value, 0.0f, 0.995f);
if (clamped != getTrailFade()) {
auto next = mutableImpl();
next->trailFade = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
float RasterParticleLayer::getAdvectionScale() const {
return impl().advectionScale;
}
void RasterParticleLayer::setAdvectionScale(float value) {
if (!std::isfinite(value) || value <= 0.0f || value == getAdvectionScale()) return;
auto next = mutableImpl();
next->advectionScale = value;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
bool RasterParticleLayer::isSpeedColorEnabled() const {
return impl().speedColorEnabled;
}
std::array<float, 4> RasterParticleLayer::getParticleColor() const { return impl().particleColor; }
void RasterParticleLayer::setParticleColor(std::array<float, 4> value) {
if (!validWeatherRGBA(value)) return;
if (value == getParticleColor()) return;
auto next = mutableImpl(); next->particleColor = value; baseImpl = std::move(next); observer->onLayerChanged(*this);
}
std::array<float, 4> RasterParticleLayer::getTrailColor() const { return impl().trailColor; }
void RasterParticleLayer::setTrailColor(std::array<float, 4> value) {
if (!validWeatherRGBA(value)) return;
if (value == getTrailColor()) return;
auto next = mutableImpl(); next->trailColor = value; baseImpl = std::move(next); observer->onLayerChanged(*this);
}
void RasterParticleLayer::setSpeedColorEnabled(bool value) {
if (value != isSpeedColorEnabled()) {
auto next = mutableImpl();
next->speedColorEnabled = value;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
}
std::optional<conversion::Error> RasterParticleLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) {
conversion::Error error;
if (name == ActiveProperty) {
auto converted = conversion::convert<bool>(value, error);
if (!converted) return error;
setActive(*converted);
return std::nullopt;
}
if (name == MaskModeProperty) {
auto converted = conversion::convert<float>(value, error);
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted)
return conversion::Error{"raster-particle-mask-mode must be 0, 1, or 2"};
setMaskMode(static_cast<uint8_t>(*converted));
return std::nullopt;
}
if (name == TimeMixProperty) {
auto converted = conversion::convert<float>(value, error);
if (!converted || !std::isfinite(*converted))
return conversion::Error{"raster-particle-time-mix must be finite"};
setTimeMix(*converted);
return std::nullopt;
}
if (name == ParticleCountProperty) {
auto converted = conversion::convert<float>(value, error);
if (!converted || !std::isfinite(*converted) || std::floor(*converted) != *converted ||
!isSupportedParticleCount(static_cast<uint32_t>(*converted))) {
return conversion::Error{
"raster-particle-particle-count must be a supported validation preset from 256 to 1048576"};
}
setParticleCount(static_cast<uint32_t>(*converted));
return std::nullopt;
}
const auto finiteFloat = [&](const char* message, auto setter) -> std::optional<conversion::Error> {
auto converted = conversion::convert<float>(value, error);
if (!converted || !std::isfinite(*converted)) return conversion::Error{message};
(this->*setter)(*converted);
return std::nullopt;
};
if (name == LineWidthProperty)
return finiteFloat("raster-particle-line-width must be finite", &RasterParticleLayer::setLineWidth);
if (name == ParticleLengthProperty)
return finiteFloat("raster-particle-particle-length must be finite", &RasterParticleLayer::setParticleLength);
if (name == ParticleOpacityProperty)
return finiteFloat("raster-particle-particle-opacity must be finite", &RasterParticleLayer::setParticleOpacity);
if (name == TrailOpacityProperty)
return finiteFloat("raster-particle-trail-opacity must be finite", &RasterParticleLayer::setTrailOpacity);
if (name == TrailFadeProperty)
return finiteFloat("raster-particle-trail-fade must be finite", &RasterParticleLayer::setTrailFade);
if (name == AdvectionScaleProperty) {
auto converted = conversion::convert<float>(value, error);
if (!converted || !std::isfinite(*converted) || *converted <= 0.0f)
return conversion::Error{"raster-particle-advection-scale must be finite and greater than zero"};
setAdvectionScale(*converted);
return std::nullopt;
}
if (name == SpeedColorEnabledProperty) {
auto converted = conversion::convert<bool>(value, error);
if (!converted) return error;
setSpeedColorEnabled(*converted);
return std::nullopt;
}
return conversion::Error{"layer doesn't support this property"};
}
StyleProperty RasterParticleLayer::getProperty(const std::string& name) const {
if (name == ActiveProperty) return StyleProperty(Value{isActive()}, StyleProperty::Kind::Constant);
if (name == MaskModeProperty)
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
if (name == TimeMixProperty) return StyleProperty(Value{getTimeMix()}, StyleProperty::Kind::Constant);
if (name == ParticleCountProperty)
return StyleProperty(Value{static_cast<uint64_t>(getParticleCount())}, StyleProperty::Kind::Constant);
if (name == LineWidthProperty) return StyleProperty(Value{getLineWidth()}, StyleProperty::Kind::Constant);
if (name == ParticleLengthProperty) return StyleProperty(Value{getParticleLength()}, StyleProperty::Kind::Constant);
if (name == ParticleOpacityProperty)
return StyleProperty(Value{getParticleOpacity()}, StyleProperty::Kind::Constant);
if (name == TrailOpacityProperty) return StyleProperty(Value{getTrailOpacity()}, StyleProperty::Kind::Constant);
if (name == TrailFadeProperty) return StyleProperty(Value{getTrailFade()}, StyleProperty::Kind::Constant);
if (name == AdvectionScaleProperty)
return StyleProperty(Value{getAdvectionScale()}, StyleProperty::Kind::Constant);
if (name == SpeedColorEnabledProperty)
return StyleProperty(Value{isSpeedColorEnabled()}, StyleProperty::Kind::Constant);
return {};
}
Value RasterParticleLayer::serialize() const {
auto result = Layer::serialize();
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
serializeProperty(result, getProperty(TimeMixProperty), TimeMixProperty, true);
serializeProperty(result, getProperty(ParticleCountProperty), ParticleCountProperty, true);
serializeProperty(result, getProperty(LineWidthProperty), LineWidthProperty, true);
serializeProperty(result, getProperty(ParticleLengthProperty), ParticleLengthProperty, true);
serializeProperty(result, getProperty(ParticleOpacityProperty), ParticleOpacityProperty, true);
serializeProperty(result, getProperty(TrailOpacityProperty), TrailOpacityProperty, true);
serializeProperty(result, getProperty(TrailFadeProperty), TrailFadeProperty, true);
serializeProperty(result, getProperty(AdvectionScaleProperty), AdvectionScaleProperty, true);
serializeProperty(result, getProperty(SpeedColorEnabledProperty), SpeedColorEnabledProperty, true);
return result;
}
std::unique_ptr<Layer> RasterParticleLayer::cloneRef(const std::string& id) const {
auto next = mutableImpl();
next->id = id;
return std::unique_ptr<Layer>(new RasterParticleLayer(std::move(next)));
}
Mutable<Layer::Impl> RasterParticleLayer::mutableBaseImpl() const {
return staticMutableCast<Layer::Impl>(mutableImpl());
}
} // namespace mln::style

View File

@@ -0,0 +1,39 @@
#pragma once
#include <mln/style/layer_impl.hpp>
#include <mln/style/layers/raster_particle_layer.hpp>
#include <cstdint>
#include <array>
namespace mln::style {
class RasterParticleLayer::Impl final : public Layer::Impl {
public:
Impl(const std::string& layerID, const std::string& sourceID)
: Layer::Impl(layerID, sourceID) {}
Impl(const Impl&) = default;
bool hasLayoutDifference(const Layer::Impl&) const override { return false; }
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override {}
bool active = true;
uint8_t maskMode = 0;
float timeMix = 0.0f;
uint32_t particleCount = 256;
float lineWidth = 2.0f;
float particleLength = 0.018f;
float particleOpacity = 1.0f;
float trailOpacity = 1.0f;
float trailFade = 0.92f;
// Direction-only vectors have no physical magnitude. This renderer-owned
// scale is deliberately independent from field metadata and only applies
// when VectorSemantics is Direction.
float advectionScale = 1.0f;
// Formal particles honor the explicit particleColor by default. Speed-based
// head coloring remains available as an explicit opt-in diagnostic mode.
bool speedColorEnabled = false;
std::array<float, 4> particleColor{{0.08f, 0.55f, 1.0f, 1.0f}};
std::array<float, 4> trailColor{{1.0f, 1.0f, 1.0f, 1.0f}};
DECLARE_LAYER_TYPE_INFO;
};
} // namespace mln::style

View File

@@ -0,0 +1,80 @@
#include <mln/style/layers/weather_color_layer.hpp>
#include <mln/style/conversion/constant.hpp>
#include <mln/style/conversion_impl.hpp>
#include <mln/style/layer_observer.hpp>
#include <mln/style/layers/weather_color_layer_impl.hpp>
#include <algorithm>
#include <cmath>
namespace mln::style {
namespace {
constexpr const char* OpacityProperty = "weather-color-opacity";
const LayerTypeInfo typeInfo{.type = "weather-color",
.source = LayerTypeInfo::Source::Required,
.pass3d = LayerTypeInfo::Pass3D::NotRequired,
.layout = LayerTypeInfo::Layout::NotRequired,
.fadingTiles = LayerTypeInfo::FadingTiles::NotRequired,
.crossTileIndex = LayerTypeInfo::CrossTileIndex::NotRequired,
.tileKind = LayerTypeInfo::TileKind::NotRequired};
} // namespace
const LayerTypeInfo* WeatherColorLayer::Impl::staticTypeInfo() noexcept { return &typeInfo; }
WeatherColorLayer::WeatherColorLayer(const std::string& id, const std::string& source)
: Layer(makeMutable<Impl>(id, source)) {}
WeatherColorLayer::WeatherColorLayer(Immutable<Impl> impl_) : Layer(std::move(impl_)) {}
WeatherColorLayer::~WeatherColorLayer() { weakFactory.invalidateWeakPtrs(); }
const WeatherColorLayer::Impl& WeatherColorLayer::impl() const { return static_cast<const Impl&>(*baseImpl); }
Mutable<WeatherColorLayer::Impl> WeatherColorLayer::mutableImpl() const { return makeMutable<Impl>(impl()); }
float WeatherColorLayer::getOpacity() const { return impl().opacity; }
void WeatherColorLayer::setOpacity(float value) {
if (!std::isfinite(value)) return;
const auto clamped = std::clamp(value, 0.0f, 1.0f);
if (clamped == getOpacity()) return;
auto next = mutableImpl();
next->opacity = clamped;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
const std::vector<WeatherColorStop>& WeatherColorLayer::getColorStops() const { return impl().colorStops; }
bool WeatherColorLayer::setColorStops(std::vector<WeatherColorStop> stops) {
if (!validWeatherColorStops(stops)) return false;
if (stops == getColorStops()) return true;
auto next = mutableImpl();
next->colorStops = std::move(stops);
baseImpl = std::move(next);
observer->onLayerChanged(*this);
return true;
}
FieldValueSource WeatherColorLayer::getFieldValueSource() const { return impl().valueSource; }
void WeatherColorLayer::setFieldValueSource(FieldValueSource valueSource) {
if (valueSource == getFieldValueSource()) return;
auto next = mutableImpl();
next->valueSource = valueSource;
baseImpl = std::move(next);
observer->onLayerChanged(*this);
}
std::optional<conversion::Error> WeatherColorLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) {
if (name != OpacityProperty) return conversion::Error{"unknown weather-color property: " + name};
conversion::Error error;
auto converted = conversion::convert<float>(value, error);
if (!converted || !std::isfinite(*converted)) return conversion::Error{"weather-color-opacity must be finite"};
setOpacity(*converted);
return std::nullopt;
}
StyleProperty WeatherColorLayer::getProperty(const std::string& name) const {
return name == OpacityProperty ? StyleProperty(Value{getOpacity()}, StyleProperty::Kind::Constant) : StyleProperty{};
}
Value WeatherColorLayer::serialize() const {
auto result = Layer::serialize();
serializeProperty(result, getProperty(OpacityProperty), OpacityProperty, true);
return result;
}
std::unique_ptr<Layer> WeatherColorLayer::cloneRef(const std::string& id) const {
auto next = mutableImpl(); next->id = id; return std::unique_ptr<Layer>(new WeatherColorLayer(std::move(next)));
}
Mutable<Layer::Impl> WeatherColorLayer::mutableBaseImpl() const { return staticMutableCast<Layer::Impl>(mutableImpl()); }
} // namespace mln::style

View File

@@ -0,0 +1,30 @@
#pragma once
#include <mln/style/layer_impl.hpp>
#include <mln/style/layer_properties.hpp>
#include <mln/style/layers/weather_color_layer.hpp>
namespace mln::style {
class WeatherColorLayer::Impl final : public Layer::Impl {
public:
Impl(const std::string& layerID, const std::string& sourceID) : Layer::Impl(layerID, sourceID) {}
Impl(const Impl&) = default;
bool hasLayoutDifference(const Layer::Impl&) const override { return false; }
void stringifyLayout(rapidjson::Writer<rapidjson::StringBuffer>&) const override {}
float opacity = 0.62f;
// Empty preserves the historical shader palette until a Host supplies stops.
std::vector<WeatherColorStop> colorStops;
FieldValueSource valueSource = FieldValueSource::VectorMagnitude;
DECLARE_LAYER_TYPE_INFO;
};
class WeatherColorLayerProperties final : public LayerProperties {
public:
WeatherColorLayerProperties(Immutable<Layer::Impl> impl, float opacity_)
: LayerProperties(std::move(impl)), opacity(opacity_) {}
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
float opacity;
};
} // namespace mln::style

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
#pragma once
#include <mln/particle/field/offline_fixture_particle_adapter.hpp>
#include <mln/particle/field/field_projection_materialization.hpp>
#include <mln/style/sources/raster_array_source.hpp>
#include <mln/style/source_impl.hpp>
namespace mln::style {
class RasterArraySource::Impl final : public Source::Impl {
public:
struct V4RendererState {
std::shared_ptr<const particle::MaterializedColorProjection> color;
std::shared_ptr<const particle::MaterializedParticleProjection> particle;
// Retain the exact immutable inputs used to build the renderer snapshots.
// Pointer identity is a conservative revision key and avoids hashing or
// comparing the decoded field contents on each forecast-clock tick.
std::optional<particle::ColorProjection> colorInput;
std::optional<particle::ParticleProjection> particleInput;
};
Impl(std::string sourceID,
std::string fixtureID_,
std::string metadataURL_,
std::string fieldURL_,
std::string nextFixtureID_ = {},
std::string nextMetadataURL_ = {},
std::string nextFieldURL_ = {},
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields_ = {})
: Source::Impl(SourceType::RasterArray, std::move(sourceID)),
fixtureID(std::move(fixtureID_)),
metadataURL(std::move(metadataURL_)),
fieldURL(std::move(fieldURL_)),
nextFixtureID(std::move(nextFixtureID_)),
nextMetadataURL(std::move(nextMetadataURL_)),
nextFieldURL(std::move(nextFieldURL_)),
runtimeFields(std::move(runtimeFields_)) {}
Impl(std::string sourceID, std::vector<RasterArrayLocalFieldDescriptor> localFields_)
: Source::Impl(SourceType::RasterArray, std::move(sourceID)),
localFields(std::move(localFields_)) {}
explicit Impl(std::string sourceID)
: Source::Impl(SourceType::RasterArray, std::move(sourceID)),
hostFieldSubmission(true) {}
Impl(const Impl&) = default;
Impl(const Impl& other, particle::ParticleFieldSnapshot field_)
: Source::Impl(other),
fixtureID(other.fixtureID),
metadataURL(other.metadataURL),
fieldURL(other.fieldURL),
nextFixtureID(other.nextFixtureID),
nextMetadataURL(other.nextMetadataURL),
nextFieldURL(other.nextFieldURL),
runtimeFields(other.runtimeFields),
localFields(other.localFields),
hostFieldSubmission(other.hostFieldSubmission),
timeline(other.timeline),
v4Cadence(other.v4Cadence),
v4RendererState(other.v4RendererState),
field(std::move(field_)) {}
Impl(const Impl& other, particle::ParticleFieldSnapshot field_, particle::ParticleFieldSnapshot nextField_)
: Impl(other, std::move(field_)) {
nextField = std::move(nextField_);
}
std::optional<std::string> getAttribution() const override { return {}; }
std::string fixtureID;
std::string metadataURL;
std::string fieldURL;
std::string nextFixtureID;
std::string nextMetadataURL;
std::string nextFieldURL;
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields;
std::vector<RasterArrayLocalFieldDescriptor> localFields;
bool hostFieldSubmission = false;
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
std::shared_ptr<RasterArrayRendererCadence> v4Cadence = std::make_shared<RasterArrayRendererCadence>();
// Immutable renderer-facing V4 state. It is intentionally separate from
// the legacy V3 timeline/field pair.
V4RendererState v4RendererState;
std::optional<particle::ParticleFieldSnapshot> field;
std::optional<particle::ParticleFieldSnapshot> nextField;
};
} // namespace mln::style

View File

@@ -12,7 +12,8 @@ MBGL_DEFINE_ENUM(SourceType,
{SourceType::Video, "video"},
{SourceType::Annotations, "annotations"},
{SourceType::Image, "image"},
{SourceType::CustomVector, "customvector"}});
{SourceType::CustomVector, "customvector"},
{SourceType::RasterArray, "raster-array"}});
MBGL_DEFINE_ENUM(VisibilityType,
{