merge(upgrade): port particle extensions onto official native 13.6.1 (unverified)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
void onStyleLoaded();
|
||||
|
||||
void updateData();
|
||||
bool hasPendingChanges();
|
||||
|
||||
void addTile(AnnotationTile&);
|
||||
void removeTile(AnnotationTile&);
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
24
src/mln/layermanager/particle_validation_layer_factory.cpp
Normal file
24
src/mln/layermanager/particle_validation_layer_factory.cpp
Normal 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
|
||||
25
src/mln/layermanager/raster_particle_layer_factory.cpp
Normal file
25
src/mln/layermanager/raster_particle_layer_factory.cpp
Normal 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
|
||||
13
src/mln/layermanager/weather_color_layer_factory.cpp
Normal file
13
src/mln/layermanager/weather_color_layer_factory.cpp
Normal 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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
151
src/mln/particle/field/field_binding.cpp
Normal file
151
src/mln/particle/field/field_binding.cpp
Normal 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
|
||||
117
src/mln/particle/field/field_data_adapter.cpp
Normal file
117
src/mln/particle/field/field_data_adapter.cpp
Normal 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
|
||||
97
src/mln/particle/field/field_profile_contract_json.cpp
Normal file
97
src/mln/particle/field/field_profile_contract_json.cpp
Normal 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
|
||||
252
src/mln/particle/field/field_profile_validator.cpp
Normal file
252
src/mln/particle/field/field_profile_validator.cpp
Normal 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
|
||||
241
src/mln/particle/field/field_projection_materialization.cpp
Normal file
241
src/mln/particle/field/field_projection_materialization.cpp
Normal 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
|
||||
308
src/mln/particle/field/field_runtime.cpp
Normal file
308
src/mln/particle/field/field_runtime.cpp
Normal 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
|
||||
71
src/mln/particle/field/local_particle_field_feed.cpp
Normal file
71
src/mln/particle/field/local_particle_field_feed.cpp
Normal 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
|
||||
199
src/mln/particle/field/offline_fixture_particle_adapter.cpp
Normal file
199
src/mln/particle/field/offline_fixture_particle_adapter.cpp
Normal 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
|
||||
22
src/mln/particle/field/offline_fixture_particle_adapter.hpp
Normal file
22
src/mln/particle/field/offline_fixture_particle_adapter.hpp
Normal 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
|
||||
127
src/mln/particle/ingress/qbin_v2_decoder.cpp
Normal file
127
src/mln/particle/ingress/qbin_v2_decoder.cpp
Normal 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
|
||||
674
src/mln/particle/ingress/qbin_v3_decoder.cpp
Normal file
674
src/mln/particle/ingress/qbin_v3_decoder.cpp
Normal 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
|
||||
80
src/mln/renderer/continuous_layer_range.hpp
Normal file
80
src/mln/renderer/continuous_layer_range.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
2137
src/mln/renderer/layers/render_particle_validation_layer.cpp
Normal file
2137
src/mln/renderer/layers/render_particle_validation_layer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
11
src/mln/renderer/layers/render_particle_validation_layer.hpp
Normal file
11
src/mln/renderer/layers/render_particle_validation_layer.hpp
Normal 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
|
||||
52
src/mln/renderer/layers/render_raster_particle_layer.hpp
Normal file
52
src/mln/renderer/layers/render_raster_particle_layer.hpp
Normal 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
|
||||
393
src/mln/renderer/layers/render_weather_color_layer.cpp
Normal file
393
src/mln/renderer/layers/render_weather_color_layer.cpp
Normal 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
|
||||
42
src/mln/renderer/layers/render_weather_color_layer.hpp
Normal file
42
src/mln/renderer/layers/render_weather_color_layer.hpp
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
141
src/mln/renderer/particle_fast_path.hpp
Normal file
141
src/mln/renderer/particle_fast_path.hpp
Normal 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
|
||||
59
src/mln/renderer/particle_viewport_reseed.hpp
Normal file
59
src/mln/renderer/particle_viewport_reseed.hpp
Normal 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
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
230
src/mln/renderer/render_segment_json.hpp
Normal file
230
src/mln/renderer/render_segment_json.hpp
Normal 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
|
||||
172
src/mln/renderer/render_segment_plan.hpp
Normal file
172
src/mln/renderer/render_segment_plan.hpp
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
383
src/mln/renderer/retained_underlay_cache.cpp
Normal file
383
src/mln/renderer/retained_underlay_cache.cpp
Normal 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
|
||||
173
src/mln/renderer/retained_underlay_cache.hpp
Normal file
173
src/mln/renderer/retained_underlay_cache.hpp
Normal 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
|
||||
538
src/mln/renderer/retained_underlay_runtime_state.hpp
Normal file
538
src/mln/renderer/retained_underlay_runtime_state.hpp
Normal 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
|
||||
69
src/mln/renderer/sources/render_raster_array_source.hpp
Normal file
69
src/mln/renderer/sources/render_raster_array_source.hpp
Normal 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
|
||||
@@ -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>>>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
14
src/mln/shaders/mtl/particle_gpu_validation.cpp
Normal file
14
src/mln/shaders/mtl/particle_gpu_validation.cpp
Normal 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
|
||||
12
src/mln/shaders/mtl/retained_underlay_composite.cpp
Normal file
12
src/mln/shaders/mtl/retained_underlay_composite.cpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
58
src/mln/style/layers/particle_render_layer_properties.hpp
Normal file
58
src/mln/style/layers/particle_render_layer_properties.hpp
Normal 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
|
||||
168
src/mln/style/layers/particle_validation_layer.cpp
Normal file
168
src/mln/style/layers/particle_validation_layer.cpp
Normal 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
|
||||
47
src/mln/style/layers/particle_validation_layer_impl.hpp
Normal file
47
src/mln/style/layers/particle_validation_layer_impl.hpp
Normal 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
|
||||
317
src/mln/style/layers/raster_particle_layer.cpp
Normal file
317
src/mln/style/layers/raster_particle_layer.cpp
Normal 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
|
||||
39
src/mln/style/layers/raster_particle_layer_impl.hpp
Normal file
39
src/mln/style/layers/raster_particle_layer_impl.hpp
Normal 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
|
||||
80
src/mln/style/layers/weather_color_layer.cpp
Normal file
80
src/mln/style/layers/weather_color_layer.cpp
Normal 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
|
||||
30
src/mln/style/layers/weather_color_layer_impl.hpp
Normal file
30
src/mln/style/layers/weather_color_layer_impl.hpp
Normal 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
|
||||
1276
src/mln/style/sources/raster_array_source.cpp
Normal file
1276
src/mln/style/sources/raster_array_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
83
src/mln/style/sources/raster_array_source_impl.hpp
Normal file
83
src/mln/style/sources/raster_array_source_impl.hpp
Normal 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
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user