From 40e283cdbbdb0e7819bbab7403433bb51e0b956b Mon Sep 17 00:00:00 2001 From: Stefan Karschti Date: Thu, 26 Oct 2023 21:36:50 +0300 Subject: [PATCH] Custom drawable layer v2 (#1754) --- include/mbgl/gfx/drawable_builder.hpp | 3 +- include/mbgl/gfx/polyline_generator.hpp | 7 +- .../style/layers/custom_drawable_layer.hpp | 91 +++++- include/mbgl/util/feature.hpp | 2 +- platform/BUILD.bazel | 40 ++- platform/darwin/BUILD.bazel | 10 + .../app/ExampleCustomDrawableStyleLayer.h | 12 + .../app/ExampleCustomDrawableStyleLayer.mm | 100 +++++++ platform/darwin/bazel/files.bzl | 5 + .../darwin/src/MLNCustomDrawableStyleLayer.h | 10 + .../darwin/src/MLNCustomDrawableStyleLayer.mm | 39 +++ .../src/MLNCustomDrawableStyleLayer_Private.h | 15 + platform/darwin/src/MLNStyleLayerManager.mm | 12 + platform/ios/app/MBXViewController.m | 27 ++ platform/ios/src/Mapbox.template.h | 3 + src/mbgl/gfx/drawable_builder_impl.cpp | 2 +- src/mbgl/gfx/drawable_builder_impl.hpp | 10 + src/mbgl/gfx/polyline_generator.cpp | 32 +-- src/mbgl/gl/drawable_gl_builder.cpp | 5 +- src/mbgl/mtl/drawable_builder.cpp | 5 +- src/mbgl/renderer/buckets/line_bucket.cpp | 2 +- .../layers/render_custom_drawable_layer.cpp | 9 +- .../layers/render_custom_drawable_layer.hpp | 2 - .../style/layers/custom_drawable_layer.cpp | 193 +++++++++++++ test/api/custom_drawable_layer.test.cpp | 259 ++++++------------ .../custom_drawable_layer/basic/expected.png | Bin 9867 -> 30223 bytes 26 files changed, 670 insertions(+), 225 deletions(-) create mode 100644 platform/darwin/app/ExampleCustomDrawableStyleLayer.h create mode 100644 platform/darwin/app/ExampleCustomDrawableStyleLayer.mm create mode 100644 platform/darwin/src/MLNCustomDrawableStyleLayer.h create mode 100644 platform/darwin/src/MLNCustomDrawableStyleLayer.mm create mode 100644 platform/darwin/src/MLNCustomDrawableStyleLayer_Private.h diff --git a/include/mbgl/gfx/drawable_builder.hpp b/include/mbgl/gfx/drawable_builder.hpp index e1ece18c5ee..d5cca13c424 100644 --- a/include/mbgl/gfx/drawable_builder.hpp +++ b/include/mbgl/gfx/drawable_builder.hpp @@ -201,9 +201,10 @@ class DrawableBuilder { /// Add a polyline. If the last point equals the first it will be closed, otherwise open void addPolyline(const GeometryCoordinates& coordinates, const gfx::PolylineGeneratorOptions&); -protected: + /// return the curent vertex count std::size_t curVertexCount() const; +protected: /// Create an instance of the appropriate drawable type virtual UniqueDrawable createDrawable() const = 0; diff --git a/include/mbgl/gfx/polyline_generator.hpp b/include/mbgl/gfx/polyline_generator.hpp index b54562c6777..f257523156f 100644 --- a/include/mbgl/gfx/polyline_generator.hpp +++ b/include/mbgl/gfx/polyline_generator.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -40,8 +41,8 @@ struct PolylineGeneratorOptions { style::LineCapType beginCap{style::LineCapType::Butt}; style::LineCapType endCap{style::LineCapType::Butt}; float roundLimit{1.f}; - uint32_t overscaling{1}; // TODO: what is this??? - std::optional lineDistances; + uint32_t overscaling{1}; + std::optional clipDistances; }; template @@ -100,4 +101,4 @@ class PolylineGenerator { }; } // namespace gfx -} // namespace mbgl \ No newline at end of file +} // namespace mbgl diff --git a/include/mbgl/style/layers/custom_drawable_layer.hpp b/include/mbgl/style/layers/custom_drawable_layer.hpp index 0856e919728..561a922d099 100644 --- a/include/mbgl/style/layers/custom_drawable_layer.hpp +++ b/include/mbgl/style/layers/custom_drawable_layer.hpp @@ -6,30 +6,107 @@ #include #include #include +#include #include #include +#include namespace mbgl { namespace style { class CustomDrawableLayerHost { +public: + class Interface; + public: virtual ~CustomDrawableLayerHost() = default; virtual void initialize() = 0; - virtual void update(RenderLayer& proxyLayer, - gfx::ShaderRegistry& shaders, - gfx::Context& context, - const TransformState& state, - const std::shared_ptr&, - const RenderTree& renderTree, - UniqueChangeRequestVec& changes) = 0; + virtual void update(Interface& interface) = 0; virtual void deinitialize() = 0; }; +class CustomDrawableLayerHost::Interface { +public: + struct LineOptions { + Color color; + float blur = 0.f; + float opacity = 1.f; + float gapWidth = 0.f; + float offset = 0.f; + float width = 1.f; + gfx::PolylineGeneratorOptions geometry; + }; + +public: + /// @brief Construct a new Interface object (internal core use only) + Interface(RenderLayer& layer, + LayerGroupBasePtr& layerGroup, + gfx::ShaderRegistry& shaders, + gfx::Context& context, + const TransformState& state, + const std::shared_ptr& updateParameters, + const RenderTree& renderTree, + UniqueChangeRequestVec& changes); + /** + * @brief Get the drawable count + * + * @return std::size_t + */ + std::size_t getDrawableCount() const; + + /** + * @brief Set the Tile ID + * + * @param tileID + */ + void setTileID(OverscaledTileID tileID); + + /** + * @brief Set the line options + * + * @param options + */ + void setLineOptions(const LineOptions& options); + + /** + * @brief Add a polyline + * + * @param coordinates + * @param options Polyline options + */ + void addPolyline(const GeometryCoordinates& coordinates); + + /** + * @brief Finishe the current drawable building session + * + */ + void finish(); + +protected: + gfx::ShaderPtr lineShaderDefault() const; + + std::unique_ptr createBuilder(const std::string& name, gfx::ShaderPtr shader) const; + +public: + RenderLayer& layer; + LayerGroupBasePtr& layerGroup; + gfx::ShaderRegistry& shaders; + gfx::Context& context; + const TransformState& state; + const std::shared_ptr& updateParameters; + const RenderTree& renderTree; + UniqueChangeRequestVec& changes; + +private: + std::unique_ptr builder; + std::optional tileID; + LineOptions lineOptions; +}; + class CustomDrawableLayer final : public Layer { public: CustomDrawableLayer(const std::string& id, std::unique_ptr host); diff --git a/include/mbgl/util/feature.hpp b/include/mbgl/util/feature.hpp index 4f295643851..85313865530 100644 --- a/include/mbgl/util/feature.hpp +++ b/include/mbgl/util/feature.hpp @@ -1,10 +1,10 @@ #pragma once #include - #include #include +#include namespace mbgl { diff --git a/platform/BUILD.bazel b/platform/BUILD.bazel index 7f77fff1c41..71ae8fe7976 100644 --- a/platform/BUILD.bazel +++ b/platform/BUILD.bazel @@ -15,6 +15,11 @@ objc_library( "//conditions:default": [ "//platform/darwin:darwin_objcpp_opengl_srcs", ], + }) + select({ + "//:legacy_renderer": [], + "//conditions:default": [ + "//platform/darwin:darwin_objcpp_custom_drawable_srcs", + ], }), copts = CPP_FLAGS + MAPLIBRE_FLAGS + [ "-fcxx-modules", @@ -165,6 +170,28 @@ objc_library( ], ) +objc_library( + name = "app_custom_drawable_layer_objcpp_srcs", + srcs = [ + "//platform/darwin:app/ExampleCustomDrawableStyleLayer.h", + "//platform/darwin:app/ExampleCustomDrawableStyleLayer.mm", + ], + copts = CPP_FLAGS + MAPLIBRE_FLAGS + WARNING_FLAGS["ios"] + [ + "-fcxx-modules", + "-fmodules", + "-Wno-c99-extensions", + "-Wno-gnu-zero-variadic-macro-arguments", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-gnu-statement-expression", + "-Wno-deprecated-declarations", + ], + visibility = ["//visibility:public"], + deps = [ + "//:mbgl-core", + ":ios-sdk", + ], +) + objc_library( name = "iosapp", srcs = [ @@ -183,7 +210,6 @@ objc_library( includes = [ "darwin/app", "darwin/src", - "ios/src", ], sdk_frameworks = [ ] + select({ @@ -196,7 +222,12 @@ objc_library( visibility = ["//visibility:public"], deps = [ ":ios-sdk", - ], + ] + select({ + "//:legacy_renderer": [], + "//conditions:default": [ + ":app_custom_drawable_layer_objcpp_srcs", + ], + }), ) objc_library( @@ -242,6 +273,11 @@ sh_binary( "//conditions:default": [ "//platform/darwin:darwin_objcpp_opengl_srcs", ], + }) + select({ + "//:legacy_renderer": [], + "//conditions:default": [ + "//platform/darwin:darwin_objcpp_custom_drawable_srcs", + ], }), deps = [ "//platform/darwin:generated_code", diff --git a/platform/darwin/BUILD.bazel b/platform/darwin/BUILD.bazel index 72eb895cd6f..79edfbee25b 100644 --- a/platform/darwin/BUILD.bazel +++ b/platform/darwin/BUILD.bazel @@ -6,6 +6,7 @@ load( "MLN_DARWIN_PRIVATE_HEADERS", "MLN_DARWIN_PUBLIC_OBJCPP_SOURCE", "MLN_DARWIN_PUBLIC_OBJCPP_OPENGL_SOURCE", + "MLN_DARWIN_PUBLIC_OBJCPP_CUSTOM_DRAWABLE_SOURCE", "MLN_DARWIN_PUBLIC_OBJC_SOURCE", "MLN_GENERATED_DARWIN_STYLE_HEADERS", "MLN_GENERATED_DARWIN_STYLE_PUBLIC_HEADERS", @@ -47,6 +48,12 @@ filegroup( visibility = ["//visibility:public"], ) +filegroup( + name = "darwin_objcpp_custom_drawable_srcs", + srcs = MLN_DARWIN_PUBLIC_OBJCPP_CUSTOM_DRAWABLE_SOURCE, + visibility = ["//visibility:public"], +) + filegroup( name = "darwin_objc_srcs", srcs = MLN_DARWIN_PUBLIC_OBJC_SOURCE, @@ -324,12 +331,15 @@ exports_files( "test/MLNSDKTestHelpers.swift", "app/LimeGreenStyleLayer.h", "app/LimeGreenStyleLayer.m", + "app/ExampleCustomDrawableStyleLayer.h", + "app/ExampleCustomDrawableStyleLayer.mm", "include/mbgl/util/image+MLNAdditions.hpp", "scripts/check-public-symbols.js", "scripts/check-public-symbols.sh", ] + MLN_DARWIN_PUBLIC_OBJC_SOURCE + MLN_DARWIN_PUBLIC_OBJCPP_SOURCE + MLN_DARWIN_PUBLIC_OBJCPP_OPENGL_SOURCE + + MLN_DARWIN_PUBLIC_OBJCPP_CUSTOM_DRAWABLE_SOURCE + MLN_DARWIN_PRIVATE_HEADERS + MLN_DARWIN_OBJC_HEADERS + MLN_DARWIN_OBJCPP_HEADERS, diff --git a/platform/darwin/app/ExampleCustomDrawableStyleLayer.h b/platform/darwin/app/ExampleCustomDrawableStyleLayer.h new file mode 100644 index 00000000000..2c5e558bc89 --- /dev/null +++ b/platform/darwin/app/ExampleCustomDrawableStyleLayer.h @@ -0,0 +1,12 @@ +#import "Mapbox.h" + +#import "MLNFoundation.h" +#import "MLNStyleValue.h" +#import "MLNStyleLayer.h" +#import "MLNGeometry.h" + +@interface ExampleCustomDrawableStyleLayer : MLNStyleLayer + +- (instancetype)initWithIdentifier:(NSString *)identifier; + +@end diff --git a/platform/darwin/app/ExampleCustomDrawableStyleLayer.mm b/platform/darwin/app/ExampleCustomDrawableStyleLayer.mm new file mode 100644 index 00000000000..2c4d8842901 --- /dev/null +++ b/platform/darwin/app/ExampleCustomDrawableStyleLayer.mm @@ -0,0 +1,100 @@ +#import "ExampleCustomDrawableStyleLayer.h" +#import "MLNStyleLayer.h" + +#import "MLNCustomDrawableStyleLayer_Private.h" +#import "MLNStyle_Private.h" +#import "MLNStyleLayer_Private.h" +#import "MLNGeometry_Private.h" + +#include +#include +#include +#include + +#include +#include + +class ExampleCustomDrawableStyleLayerHost; + +@implementation ExampleCustomDrawableStyleLayer + +- (instancetype)initWithIdentifier:(NSString *)identifier { + auto layer = std::make_unique(identifier.UTF8String, + std::make_unique(self)); + return self = [super initWithPendingLayer:std::move(layer)]; +} + + +@end + +class ExampleCustomDrawableStyleLayerHost : public mbgl::style::CustomDrawableLayerHost { +public: + ExampleCustomDrawableStyleLayerHost(ExampleCustomDrawableStyleLayer *styleLayer) { + layerRef = styleLayer; + layer = nil; + } + + void initialize() override { + if (layerRef == nil) return; + else if (layer == nil) layer = layerRef; + } + + void update(Interface& interface) override { + + // if we have built our drawable(s) already, either update or skip + if (interface.getDrawableCount()) + return; + + // set tile + interface.setTileID({11, 327, 791}); + + // add polylines + using namespace mbgl; + + constexpr auto numLines = 6; + Interface::LineOptions options[numLines] { + {/*color=*/Color::red(), /*blur=*/0.0f, /*opacity=*/1.0f, /*gapWidth=*/0.0f, /*offset=*/0.0f, /*width=*/8.0f, {} }, + {/*color=*/Color::blue(), /*blur=*/4.0f, /*opacity=*/1.0f, /*gapWidth=*/2.0f, /*offset=*/-1.0f, /*width=*/4.0f, {} }, + {/*color=*/Color(1.f, 0.5f, 0, 0.5f), /*blur=*/16.0f, /*opacity=*/1.0f, /*gapWidth=*/1.0f, /*offset=*/2.0f, /*width=*/16.0f, {} }, + {/*color=*/Color(1.f, 1.f, 0, 0.3f), /*blur=*/2.0f, /*opacity=*/1.0f, /*gapWidth=*/1.0f, /*offset=*/-2.0f, /*width=*/2.0f, {} }, + {/*color=*/Color::black(), /*blur=*/0.5f, /*opacity=*/0.5f, /*gapWidth=*/1.0f, /*offset=*/0.5f, /*width=*/0.5f, {} }, + {/*color=*/Color(1.f, 0, 1.f, 0.2f), /*blur=*/24.0f, /*opacity=*/0.5f, /*gapWidth=*/1.0f, /*offset=*/-5.0f, /*width=*/24.0f, {} }, + }; + for(auto& opt: options) { + opt.geometry.beginCap = style::LineCapType::Round; + opt.geometry.endCap = style::LineCapType::Round; + opt.geometry.joinType = style::LineJoinType::Round; + } + + constexpr auto numPoints = 100; + GeometryCoordinates polyline; + for (auto ipoint{0}; ipoint < numPoints; ++ipoint) { + polyline.emplace_back(ipoint * util::EXTENT / numPoints, std::sin(ipoint * 2 * M_PI / numPoints) * util::EXTENT / numLines / 2.f); + } + + for (auto index {0}; index < numLines; ++index) { + for(auto &p : polyline) { + p.y += util::EXTENT / numLines; + } + + // set property values + interface.setLineOptions(options[index]); + + // add polyline + interface.addPolyline(polyline); + } + + // finish + interface.finish(); + } + + void deinitialize() override { + if (layer == nil) return; + + layerRef = layer; + layer = nil; + } +private: + __weak ExampleCustomDrawableStyleLayer * layerRef; + ExampleCustomDrawableStyleLayer * layer = nil; +}; diff --git a/platform/darwin/bazel/files.bzl b/platform/darwin/bazel/files.bzl index 8ed70ea2dc8..df83c141247 100644 --- a/platform/darwin/bazel/files.bzl +++ b/platform/darwin/bazel/files.bzl @@ -208,6 +208,11 @@ MLN_DARWIN_PUBLIC_OBJCPP_OPENGL_SOURCE = [ "src/MLNOpenGLStyleLayer.h", "src/MLNOpenGLStyleLayer.mm", ] +MLN_DARWIN_PUBLIC_OBJCPP_CUSTOM_DRAWABLE_SOURCE = [ + "src/MLNCustomDrawableStyleLayer_Private.h", + "src/MLNCustomDrawableStyleLayer.h", + "src/MLNCustomDrawableStyleLayer.mm", +] MLN_DARWIN_PUBLIC_OBJC_SOURCE = [ "src/MLNAttributedExpression.m", "src/MLNClockDirectionFormatter.m", diff --git a/platform/darwin/src/MLNCustomDrawableStyleLayer.h b/platform/darwin/src/MLNCustomDrawableStyleLayer.h new file mode 100644 index 00000000000..6c581b3730a --- /dev/null +++ b/platform/darwin/src/MLNCustomDrawableStyleLayer.h @@ -0,0 +1,10 @@ +#import "MLNFoundation.h" +#import "MLNStyleValue.h" +#import "MLNStyleLayer.h" +#import "MLNGeometry.h" + +@interface MLNCustomDrawableStyleLayer : MLNStyleLayer + +- (instancetype)initWithIdentifier:(NSString *)identifier; + +@end diff --git a/platform/darwin/src/MLNCustomDrawableStyleLayer.mm b/platform/darwin/src/MLNCustomDrawableStyleLayer.mm new file mode 100644 index 00000000000..be3698b1316 --- /dev/null +++ b/platform/darwin/src/MLNCustomDrawableStyleLayer.mm @@ -0,0 +1,39 @@ +#import "MLNCustomDrawableStyleLayer.h" +#import "MLNStyleLayer.h" + +#import "MLNCustomDrawableStyleLayer_Private.h" +#import "MLNStyle_Private.h" +#import "MLNStyleLayer_Private.h" +#import "MLNGeometry_Private.h" + +#include +#include + +#include +#include + +@implementation MLNCustomDrawableStyleLayer + +/// @note +/// Inherit MLNCustomDrawableStyleLayer class and override initWithIdentifier method to create and attach a valid CustomDrawableLayerHost instance +/// Example: +/// - (instancetype)initWithIdentifier:(NSString *)identifier { +/// auto layer = std::make_unique(identifier.UTF8String, +/// std::make_unique(self)); +/// return self = [super initWithPendingLayer:std::move(layer)]; +/// } +/// +- (instancetype)initWithIdentifier:(NSString *)identifier { + auto layer = std::make_unique(identifier.UTF8String, nullptr); + return self = [super initWithPendingLayer:std::move(layer)]; +} + +@end + +namespace mbgl { + +MLNStyleLayer* CustomDrawableStyleLayerPeerFactory::createPeer(style::Layer* rawLayer) { + return [[MLNCustomDrawableStyleLayer alloc] initWithRawLayer:rawLayer]; +} + +} // namespace mbgl diff --git a/platform/darwin/src/MLNCustomDrawableStyleLayer_Private.h b/platform/darwin/src/MLNCustomDrawableStyleLayer_Private.h new file mode 100644 index 00000000000..9b03bbbd9e2 --- /dev/null +++ b/platform/darwin/src/MLNCustomDrawableStyleLayer_Private.h @@ -0,0 +1,15 @@ +#pragma once + +#include "MLNStyleLayer_Private.h" + +#include + +namespace mbgl { + +class CustomDrawableStyleLayerPeerFactory : public LayerPeerFactory, public mbgl::CustomDrawableLayerFactory { + // LayerPeerFactory overrides. + LayerFactory* getCoreLayerFactory() final { return this; } + virtual MLNStyleLayer* createPeer(style::Layer*) final; +}; + +} // namespace mbgl diff --git a/platform/darwin/src/MLNStyleLayerManager.mm b/platform/darwin/src/MLNStyleLayerManager.mm index 29a1a711d97..ff9ebd3a7ae 100644 --- a/platform/darwin/src/MLNStyleLayerManager.mm +++ b/platform/darwin/src/MLNStyleLayerManager.mm @@ -14,6 +14,10 @@ #import "MLNOpenGLStyleLayer_Private.h" #endif +#if MLN_DRAWABLE_RENDERER +#import "MLNCustomDrawableStyleLayer_Private.h" +#endif + #include namespace mbgl { @@ -69,6 +73,14 @@ #elif !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) && !MLN_RENDER_BACKEND_METAL addLayerType(std::make_unique()); #endif + +#if MLN_DRAWABLE_RENDERER +#if defined(MLN_LAYER_CUSTOM_DRAWABLE_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MLN_LAYER_CUSTOM_DRAWABLE_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#endif } LayerManagerDarwin::~LayerManagerDarwin() = default; diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 66b702c58a2..37247a23632 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -17,6 +17,10 @@ #import "LimeGreenStyleLayer.h" #endif +#if MLN_DRAWABLE_RENDERER +#import "ExampleCustomDrawableStyleLayer.h" +#endif + #import "MBXFrameTimeGraphView.h" #import "MLNMapView_Experimental.h" #import @@ -108,6 +112,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) { MBXSettingsRuntimeStylingDDSPolygon, MBXSettingsRuntimeStylingCustomLatLonGrid, MBXSettingsRuntimeStylingLineGradient, +#if MLN_DRAWABLE_RENDERER + MBXSettingsRuntimeStylingCustomDrawableLayer, +#endif }; typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { @@ -441,6 +448,9 @@ - (void)dismissSettings:(__unused id)sender @"Dynamically Style Polygon", @"Add Custom Lat/Lon Grid", @"Style Route line with gradient", +#if MLN_DRAWABLE_RENDERER + @"Add Custom Drawable Layer", +#endif ]]; break; case MBXSettingsMiscellaneous: @@ -666,6 +676,11 @@ - (void)performActionForSettingAtIndexPath:(NSIndexPath *)indexPath case MBXSettingsRuntimeStylingLineGradient: [self styleLineGradient]; break; +#if MLN_DRAWABLE_RENDERER + case MBXSettingsRuntimeStylingCustomDrawableLayer: + [self addCustomDrawableLayer]; + break; +#endif default: NSAssert(NO, @"All runtime styling setting rows should be implemented"); break; @@ -1564,6 +1579,18 @@ - (void)styleLineGradient [self.mapView.style addLayer:routeLayer]; } +#if MLN_DRAWABLE_RENDERER +- (void)addCustomDrawableLayer +{ + // Create a CustomLayer that uses the Drawable/Builder toolkit to generate and render geometry + ExampleCustomDrawableStyleLayer* layer = [[ExampleCustomDrawableStyleLayer alloc] initWithIdentifier:@"custom-drawable-layer"]; + + if (layer) { + [self.mapView.style addLayer:layer]; + } +} +#endif + - (void)styleRouteLine { CLLocationCoordinate2D coords[] = { diff --git a/platform/ios/src/Mapbox.template.h b/platform/ios/src/Mapbox.template.h index 6554eae0ec6..43ac29b78b1 100644 --- a/platform/ios/src/Mapbox.template.h +++ b/platform/ios/src/Mapbox.template.h @@ -77,3 +77,6 @@ FOUNDATION_EXPORT MLN_EXPORT const unsigned char MapboxVersionString[]; #import "NSPredicate+MLNAdditions.h" #import "NSValue+MLNAdditions.h" #import "MLNUserLocationAnnotationViewStyle.h" +#if MLN_DRAWABLE_RENDERER +#import "MLNCustomDrawableStyleLayer.h" +#endif diff --git a/src/mbgl/gfx/drawable_builder_impl.cpp b/src/mbgl/gfx/drawable_builder_impl.cpp index 9ed34d8eda9..46e7ffa43fd 100644 --- a/src/mbgl/gfx/drawable_builder_impl.cpp +++ b/src/mbgl/gfx/drawable_builder_impl.cpp @@ -70,7 +70,7 @@ void DrawableBuilder::Impl::setupForPolylines(gfx::DrawableBuilder& builder) { gfx::VertexAttributeArray attrs; using VertexVector = gfx::VertexVector; - std::shared_ptr verts = std::make_shared(polylineVertices); + std::shared_ptr verts = std::make_shared(std::move(polylineVertices)); if (const auto& attr = attrs.add(idVertexAttribName)) { attr->setSharedRawData(verts, diff --git a/src/mbgl/gfx/drawable_builder_impl.hpp b/src/mbgl/gfx/drawable_builder_impl.hpp index 292519aba08..da12d46d674 100644 --- a/src/mbgl/gfx/drawable_builder_impl.hpp +++ b/src/mbgl/gfx/drawable_builder_impl.hpp @@ -65,6 +65,16 @@ class DrawableBuilder::Impl { return std::max(rawVerticesCount, std::max(vertices.elements(), polylineVertices.elements())); } + void clear() { + vertices.clear(); + rawVertices.clear(); + rawVerticesCount = 0; + polylineVertices.clear(); + polylineIndexes.clear(); + buildIndexes.clear(); + segments.clear(); + } + private: LineLayoutVertex layoutVertex( Point p, Point e, bool round, bool up, int8_t dir, int32_t linesofar = 0); diff --git a/src/mbgl/gfx/polyline_generator.cpp b/src/mbgl/gfx/polyline_generator.cpp index 51ccdf52e3c..beac5d57765 100644 --- a/src/mbgl/gfx/polyline_generator.cpp +++ b/src/mbgl/gfx/polyline_generator.cpp @@ -227,7 +227,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); prevCoordinate = newPrevVertex; } } @@ -279,7 +279,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } else if (middleVertex && currentJoin == style::LineJoinType::FlipBevel) { // miter is too big, flip the direction to make a beveled join @@ -302,7 +302,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); addCurrentVertex(*currentCoordinate, distance, @@ -312,7 +312,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } else if (middleVertex && (currentJoin == style::LineJoinType::Bevel || currentJoin == style::LineJoinType::FakeRound)) { const bool lineTurnsLeft = (prevNormal->x * nextNormal->y - prevNormal->y * nextNormal->x) > 0; @@ -338,7 +338,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } if (currentJoin == style::LineJoinType::FakeRound) { @@ -369,7 +369,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates lineTurnsLeft, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } } @@ -383,7 +383,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } } else if (!middleVertex && currentCap == style::LineCapType::Butt) { @@ -397,7 +397,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } // Start next segment with a butt @@ -410,7 +410,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } } else if (!middleVertex && currentCap == style::LineCapType::Square) { @@ -424,7 +424,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); // The segment is done. Unset vertices to disconnect segments. e1 = e2 = -1; @@ -440,7 +440,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } } else if (middleVertex ? currentJoin == style::LineJoinType::Round : currentCap == style::LineCapType::Round) { @@ -454,7 +454,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); // Add round cap or linejoin at end of segment addCurrentVertex(*currentCoordinate, @@ -465,7 +465,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates true, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); // The segment is done. Unset vertices to disconnect segments. e1 = e2 = -1; @@ -482,7 +482,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates true, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); addCurrentVertex(*currentCoordinate, distance, @@ -492,7 +492,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); } } @@ -512,7 +512,7 @@ void PolylineGenerator::generate(const GeometryCoordinates& coordinates false, startVertex, triangleStore, - options.lineDistances); + options.clipDistances); currentCoordinate = newCurrentVertex; } } diff --git a/src/mbgl/gl/drawable_gl_builder.cpp b/src/mbgl/gl/drawable_gl_builder.cpp index 22bf7921427..f1269e9e363 100644 --- a/src/mbgl/gl/drawable_gl_builder.cpp +++ b/src/mbgl/gl/drawable_gl_builder.cpp @@ -39,11 +39,10 @@ void DrawableGLBuilder::init() { if (!impl->sharedIndexes && !impl->buildIndexes.empty()) { impl->sharedIndexes = std::make_shared(std::move(impl->buildIndexes)); } + assert(impl->sharedIndexes && impl->sharedIndexes->elements()); drawableGL.setIndexData(std::move(impl->sharedIndexes), std::move(impl->segments)); - impl->buildIndexes.clear(); - impl->segments.clear(); - impl->vertices.clear(); + impl->clear(); textures.clear(); } diff --git a/src/mbgl/mtl/drawable_builder.cpp b/src/mbgl/mtl/drawable_builder.cpp index 4144f802be9..741db0a78f7 100644 --- a/src/mbgl/mtl/drawable_builder.cpp +++ b/src/mbgl/mtl/drawable_builder.cpp @@ -27,6 +27,7 @@ void DrawableBuilder::init() { if (impl->rawVerticesCount) { auto raw = impl->rawVertices; drawable.setVertices(std::move(raw), impl->rawVerticesCount, impl->rawVerticesType); + impl->rawVerticesCount = 0; } else { const auto& verts = impl->vertices.vector(); constexpr auto vertSize = sizeof(std::remove_reference::type::value_type); @@ -41,9 +42,7 @@ void DrawableBuilder::init() { assert(impl->sharedIndexes && impl->sharedIndexes->elements()); drawable.setIndexData(std::move(impl->sharedIndexes), std::move(impl->segments)); - impl->buildIndexes.clear(); - impl->segments.clear(); - impl->vertices.clear(); + impl->clear(); textures.clear(); } diff --git a/src/mbgl/renderer/buckets/line_bucket.cpp b/src/mbgl/renderer/buckets/line_bucket.cpp index 5458074b834..5e2825b1b34 100644 --- a/src/mbgl/renderer/buckets/line_bucket.cpp +++ b/src/mbgl/renderer/buckets/line_bucket.cpp @@ -99,7 +99,7 @@ void LineBucket::addGeometry(const GeometryCoordinates& coordinates, total_length += util::dist(coordinates[i], coordinates[i + 1]); } - options.lineDistances = gfx::PolylineGeneratorDistances{ + options.clipDistances = gfx::PolylineGeneratorDistances{ *numericValue(clip_start_it->second), *numericValue(clip_end_it->second), total_length}; } diff --git a/src/mbgl/renderer/layers/render_custom_drawable_layer.cpp b/src/mbgl/renderer/layers/render_custom_drawable_layer.cpp index b05965a750c..c6f22075692 100644 --- a/src/mbgl/renderer/layers/render_custom_drawable_layer.cpp +++ b/src/mbgl/renderer/layers/render_custom_drawable_layer.cpp @@ -52,10 +52,6 @@ bool RenderCustomDrawableLayer::hasCrossfade() const { return false; } -void RenderCustomDrawableLayer::markContextDestroyed() { - contextDestroyed = true; -} - void RenderCustomDrawableLayer::prepare(const LayerPrepareParameters&) {} #if MLN_LEGACY_RENDERER @@ -84,9 +80,12 @@ void RenderCustomDrawableLayer::update(gfx::ShaderRegistry& shaders, // delegate the call to the custom layer if (host) { - host->update(*this, shaders, context, state, updateParameters, renderTree, changes); + CustomDrawableLayerHost::Interface interface( + *this, layerGroup, shaders, context, state, updateParameters, renderTree, changes); + host->update(interface); } } + #endif } // namespace mbgl diff --git a/src/mbgl/renderer/layers/render_custom_drawable_layer.hpp b/src/mbgl/renderer/layers/render_custom_drawable_layer.hpp index bd03b6e8ec8..9a4591a0616 100644 --- a/src/mbgl/renderer/layers/render_custom_drawable_layer.hpp +++ b/src/mbgl/renderer/layers/render_custom_drawable_layer.hpp @@ -25,14 +25,12 @@ class RenderCustomDrawableLayer final : public RenderLayer { void evaluate(const PropertyEvaluationParameters&) override; bool hasTransition() const override; bool hasCrossfade() const override; - void markContextDestroyed() override; void prepare(const LayerPrepareParameters&) override; #if MLN_LEGACY_RENDERER void render(PaintParameters&) override; #endif - bool contextDestroyed = false; std::shared_ptr host; }; diff --git a/src/mbgl/style/layers/custom_drawable_layer.cpp b/src/mbgl/style/layers/custom_drawable_layer.cpp index a70dd0d9b85..17e16921f8e 100644 --- a/src/mbgl/style/layers/custom_drawable_layer.cpp +++ b/src/mbgl/style/layers/custom_drawable_layer.cpp @@ -1,7 +1,20 @@ #include #include + #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace mbgl { namespace style { @@ -53,5 +66,185 @@ const LayerTypeInfo* CustomDrawableLayer::Impl::staticTypeInfo() noexcept { return &typeInfoCustomDrawable; } +// CustomDrawableLayerHost::Interface + +class LineDrawableTweaker : public gfx::DrawableTweaker { +public: + LineDrawableTweaker(const shaders::LinePropertiesUBO& properties) + : linePropertiesUBO(properties) {} + ~LineDrawableTweaker() override = default; + + void init(gfx::Drawable&) override{}; + + void execute(gfx::Drawable& drawable, const PaintParameters& parameters) override { + if (!drawable.getTileID().has_value()) { + return; + } + + const UnwrappedTileID tileID = drawable.getTileID()->toUnwrapped(); + const auto zoom = parameters.state.getZoom(); + mat4 tileMatrix; + parameters.state.matrixFor(/*out*/ tileMatrix, tileID); + + const auto matrix = LayerTweaker::getTileMatrix( + tileID, parameters, {{0, 0}}, style::TranslateAnchorType::Viewport, false, false, false); + + static const StringIdentity idLineUBOName = stringIndexer().get("LineUBO"); + const shaders::LineUBO lineUBO{ + /*matrix = */ util::cast(matrix), + /*units_to_pixels = */ {1.0f / parameters.pixelsToGLUnits[0], 1.0f / parameters.pixelsToGLUnits[1]}, + /*ratio = */ 1.0f / tileID.pixelsToTileUnits(1.0f, zoom), + /*device_pixel_ratio = */ parameters.pixelRatio}; + + static const StringIdentity idLinePropertiesUBOName = stringIndexer().get("LinePropertiesUBO"); + + static const StringIdentity idLineInterpolationUBOName = stringIndexer().get("LineInterpolationUBO"); + const shaders::LineInterpolationUBO lineInterpolationUBO{/*color_t =*/0.f, + /*blur_t =*/0.f, + /*opacity_t =*/0.f, + /*gapwidth_t =*/0.f, + /*offset_t =*/0.f, + /*width_t =*/0.f, + 0, + 0}; + auto& uniforms = drawable.mutableUniformBuffers(); + uniforms.createOrUpdate(idLineUBOName, &lineUBO, parameters.context); + uniforms.createOrUpdate(idLinePropertiesUBOName, &linePropertiesUBO, parameters.context); + uniforms.createOrUpdate(idLineInterpolationUBOName, &lineInterpolationUBO, parameters.context); + +#if MLN_RENDER_BACKEND_METAL + static const StringIdentity idExpressionInputsUBOName = stringIndexer().get("ExpressionInputsUBO"); + const auto expressionUBO = LayerTweaker::buildExpressionUBO(zoom, parameters.frameCount); + uniforms.createOrUpdate(idExpressionInputsUBOName, &expressionUBO, parameters.context); + + static const StringIdentity idLinePermutationUBOName = stringIndexer().get("LinePermutationUBO"); + const shaders::LinePermutationUBO permutationUBO = { + /* .color = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .blur = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .opacity = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .gapwidth = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .offset = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .width = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .floorwidth = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .pattern_from = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .pattern_to = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, + /* .overdrawInspector = */ false, + /* .pad = */ 0, + 0, + 0, + 0}; + uniforms.createOrUpdate(idLinePermutationUBOName, &permutationUBO, parameters.context); +#endif // MLN_RENDER_BACKEND_METAL + }; + +private: + shaders::LinePropertiesUBO linePropertiesUBO; +}; + +CustomDrawableLayerHost::Interface::Interface(RenderLayer& layer_, + LayerGroupBasePtr& layerGroup_, + gfx::ShaderRegistry& shaders_, + gfx::Context& context_, + const TransformState& state_, + const std::shared_ptr& updateParameters_, + const RenderTree& renderTree_, + UniqueChangeRequestVec& changes_) + : layer(layer_), + layerGroup(layerGroup_), + shaders(shaders_), + context(context_), + state(state_), + updateParameters(updateParameters_), + renderTree(renderTree_), + changes(changes_) { + // ensure we have a default layer group set up + if (!layerGroup) { + if (auto aLayerGroup = context.createTileLayerGroup( + /*layerIndex*/ layer.getLayerIndex(), /*initialCapacity=*/64, layer.getID())) { + changes.emplace_back(std::make_unique(aLayerGroup)); + layerGroup = std::move(aLayerGroup); + } + } +} + +std::size_t CustomDrawableLayerHost::Interface::getDrawableCount() const { + return layerGroup->getDrawableCount(); +} + +void CustomDrawableLayerHost::Interface::setTileID(OverscaledTileID tileID_) { + tileID = tileID_; +} + +void CustomDrawableLayerHost::Interface::setLineOptions(const LineOptions& options) { + finish(); + lineOptions = options; +} + +void CustomDrawableLayerHost::Interface::addPolyline(const GeometryCoordinates& coordinates) { + if (!builder) { + builder = createBuilder("thick-lines", lineShaderDefault()); + } else { + // TODO: check builder + } + builder->addPolyline(coordinates, lineOptions.geometry); +} + +void CustomDrawableLayerHost::Interface::finish() { + if (builder && builder->curVertexCount()) { + // create tweaker + const shaders::LinePropertiesUBO linePropertiesUBO{lineOptions.color, + lineOptions.blur, + lineOptions.opacity, + lineOptions.gapWidth, + lineOptions.offset, + lineOptions.width, + 0, + 0, + 0}; + + auto tweaker = std::make_shared(linePropertiesUBO); + + // finish + builder->flush(); + for (auto& drawable : builder->clearDrawables()) { + assert(tileID.has_value()); + drawable->setTileID(tileID.value()); + drawable->addTweaker(tweaker); + + TileLayerGroup* tileLayerGroup = static_cast(layerGroup.get()); + tileLayerGroup->addDrawable(RenderPass::Translucent, tileID.value(), std::move(drawable)); + } + } +} + +gfx::ShaderPtr CustomDrawableLayerHost::Interface::lineShaderDefault() const { + gfx::ShaderGroupPtr lineShaderGroup = shaders.getShaderGroup("LineShader"); + + const std::unordered_set propertiesAsUniforms{ + stringIndexer().get("a_color"), + stringIndexer().get("a_blur"), + stringIndexer().get("a_opacity"), + stringIndexer().get("a_gapwidth"), + stringIndexer().get("a_offset"), + stringIndexer().get("a_width"), + }; + + return lineShaderGroup->getOrCreateShader(context, propertiesAsUniforms); +} + +std::unique_ptr CustomDrawableLayerHost::Interface::createBuilder(const std::string& name, + gfx::ShaderPtr shader) const { + std::unique_ptr builder_ = context.createDrawableBuilder(name); + builder_->setShader(std::static_pointer_cast(shader)); + builder_->setSubLayerIndex(0); + builder_->setEnableDepth(false); + builder_->setColorMode(gfx::ColorMode::alphaBlended()); + builder_->setCullFaceMode(gfx::CullFaceMode::disabled()); + builder_->setEnableStencil(false); + builder_->setRenderPass(RenderPass::Translucent); + + return builder_; +} + } // namespace style } // namespace mbgl diff --git a/test/api/custom_drawable_layer.test.cpp b/test/api/custom_drawable_layer.test.cpp index 990d7bceb75..5673cbe1422 100644 --- a/test/api/custom_drawable_layer.test.cpp +++ b/test/api/custom_drawable_layer.test.cpp @@ -13,207 +13,106 @@ #if MLN_DRAWABLE_RENDERER #include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include +#include #include - -using namespace mbgl; -using namespace mbgl::style; - -class TestDrawableTweaker : public gfx::DrawableTweaker { -public: - TestDrawableTweaker() {} - ~TestDrawableTweaker() override = default; - - void init(gfx::Drawable&) override{}; - - void execute(gfx::Drawable& drawable, const PaintParameters& parameters) override { - if (!drawable.getTileID().has_value()) { - return; - } - - const UnwrappedTileID tileID = drawable.getTileID()->toUnwrapped(); - const auto zoom = parameters.state.getZoom(); - mat4 tileMatrix; - parameters.state.matrixFor(/*out*/ tileMatrix, tileID); - - const auto matrix = LayerTweaker::getTileMatrix( - tileID, parameters, {{0, 0}}, style::TranslateAnchorType::Viewport, false, false, false); - - static const StringIdentity idLineUBOName = stringIndexer().get("LineUBO"); - const shaders::LineUBO lineUBO{ - /*matrix = */ util::cast(matrix), - /*units_to_pixels = */ {1.0f / parameters.pixelsToGLUnits[0], 1.0f / parameters.pixelsToGLUnits[1]}, - /*ratio = */ 1.0f / tileID.pixelsToTileUnits(1.0f, zoom), - /*device_pixel_ratio = */ parameters.pixelRatio}; - - static const StringIdentity idLinePropertiesUBOName = stringIndexer().get("LinePropertiesUBO"); - const shaders::LinePropertiesUBO linePropertiesUBO{/*color =*/Color(1.f, 0.f, 1.f, 1.f), - /*blur =*/0.f, - /*opacity =*/1.f, - /*gapwidth =*/0.f, - /*offset =*/0.f, - /*width =*/8.f, - 0, - 0, - 0}; - - static const StringIdentity idLineInterpolationUBOName = stringIndexer().get("LineInterpolationUBO"); - const shaders::LineInterpolationUBO lineInterpolationUBO{/*color_t =*/0.f, - /*blur_t =*/0.f, - /*opacity_t =*/0.f, - /*gapwidth_t =*/0.f, - /*offset_t =*/0.f, - /*width_t =*/0.f, - 0, - 0}; - auto& uniforms = drawable.mutableUniformBuffers(); - uniforms.createOrUpdate(idLineUBOName, &lineUBO, parameters.context); - uniforms.createOrUpdate(idLinePropertiesUBOName, &linePropertiesUBO, parameters.context); - uniforms.createOrUpdate(idLineInterpolationUBOName, &lineInterpolationUBO, parameters.context); - -#if MLN_RENDER_BACKEND_METAL - static const StringIdentity idExpressionInputsUBOName = stringIndexer().get("ExpressionInputsUBO"); - const auto expressionUBO = LayerTweaker::buildExpressionUBO(zoom, parameters.frameCount); - uniforms.createOrUpdate(idExpressionInputsUBOName, &expressionUBO, parameters.context); - - static const StringIdentity idLinePermutationUBOName = stringIndexer().get("LinePermutationUBO"); - const shaders::LinePermutationUBO permutationUBO = { - /* .color = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .blur = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .opacity = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .gapwidth = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .offset = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .width = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .floorwidth = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .pattern_from = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .pattern_to = */ {/*.source=*/shaders::AttributeSource::Constant, /*.expression=*/{}}, - /* .overdrawInspector = */ false, - /* .pad = */ 0, - 0, - 0, - 0}; - uniforms.createOrUpdate(idLinePermutationUBOName, &permutationUBO, parameters.context); -#endif // MLN_RENDER_BACKEND_METAL - }; -}; +#include class TestDrawableLayer : public mbgl::style::CustomDrawableLayerHost { public: void initialize() override {} - /** - * @brief Create Drawables for the Custom Drawable Layer - * - * @param proxyLayer - * @param shaders - * @param context - * @param state - * @param updateParameters - * @param renderTree - * @param changes - */ - void update(RenderLayer& proxyLayer, - gfx::ShaderRegistry& shaders, - gfx::Context& context, - [[maybe_unused]] const TransformState& state, - [[maybe_unused]] const std::shared_ptr& updateParameters, - [[maybe_unused]] const RenderTree& renderTree, - UniqueChangeRequestVec& changes) override { - // Set up a layer group - if (!layerGroup) { - if (auto layerGroup_ = context.createTileLayerGroup( - /*layerIndex*/ proxyLayer.getLayerIndex(), /*initialCapacity=*/2, proxyLayer.getID())) { - changes.emplace_back(std::make_unique(layerGroup_)); - layerGroup = std::move(layerGroup_); - } - } - - if (!layerGroup) return; - - // if we have build our drawable(s) already, either update or skip - if (layerGroup->getDrawableCount()) return; - - // create drawable(s) - const OverscaledTileID tileID{11, 327, 791}; - - auto createLineBuilder = [&](const std::string& name, - gfx::ShaderPtr shader) -> std::unique_ptr { - std::unique_ptr builder = context.createDrawableBuilder(name); - builder->setShader(std::static_pointer_cast(shader)); - builder->setSubLayerIndex(0); - builder->setEnableDepth(false); - builder->setColorMode(gfx::ColorMode::alphaBlended()); - builder->setCullFaceMode(gfx::CullFaceMode::disabled()); - builder->setEnableStencil(false); - builder->setRenderPass(RenderPass::Translucent); + void update(Interface& interface) override { + // if we have built our drawable(s) already, either update or skip + if (interface.getDrawableCount()) return; - return builder; - }; - - gfx::ShaderGroupPtr lineShaderGroup = shaders.getShaderGroup("LineShader"); - - const std::unordered_set propertiesAsUniforms{ - stringIndexer().get("a_color"), - stringIndexer().get("a_blur"), - stringIndexer().get("a_opacity"), - stringIndexer().get("a_gapwidth"), - stringIndexer().get("a_offset"), - stringIndexer().get("a_width"), - }; - - auto shader = lineShaderGroup->getOrCreateShader(context, propertiesAsUniforms); - auto builder = createLineBuilder("thick-lines", shader); - - auto* tileLayerGroup = static_cast(layerGroup.get()); + // set tile + interface.setTileID({11, 327, 791}); // add polylines - const auto size{util::EXTENT}; - GeometryCoordinates geom{{0, 0}, {size, 0}, {0, size}, {size, size}, {size / 3, size / 3}}; + using namespace mbgl; + + constexpr auto numLines = 6; + Interface::LineOptions options[numLines]{ + {/*color=*/Color::red(), + /*blur=*/0.0f, + /*opacity=*/1.0f, + /*gapWidth=*/0.0f, + /*offset=*/0.0f, + /*width=*/8.0f, + {}}, + {/*color=*/Color::blue(), + /*blur=*/4.0f, + /*opacity=*/1.0f, + /*gapWidth=*/2.0f, + /*offset=*/-1.0f, + /*width=*/4.0f, + {}}, + {/*color=*/Color(1.f, 0.5f, 0, 0.5f), + /*blur=*/16.0f, + /*opacity=*/1.0f, + /*gapWidth=*/1.0f, + /*offset=*/2.0f, + /*width=*/16.0f, + {}}, + {/*color=*/Color(1.f, 1.f, 0, 0.3f), + /*blur=*/2.0f, + /*opacity=*/1.0f, + /*gapWidth=*/1.0f, + /*offset=*/-2.0f, + /*width=*/2.0f, + {}}, + {/*color=*/Color::black(), + /*blur=*/0.5f, + /*opacity=*/0.5f, + /*gapWidth=*/1.0f, + /*offset=*/0.5f, + /*width=*/0.5f, + {}}, + {/*color=*/Color(1.f, 0, 1.f, 0.2f), + /*blur=*/24.0f, + /*opacity=*/0.5f, + /*gapWidth=*/1.0f, + /*offset=*/-5.0f, + /*width=*/24.0f, + {}}, + }; + for (auto& opt : options) { + opt.geometry.beginCap = style::LineCapType::Round; + opt.geometry.endCap = style::LineCapType::Round; + opt.geometry.joinType = style::LineJoinType::Round; + } - gfx::PolylineGeneratorOptions options; - options.beginCap = style::LineCapType::Round; - options.endCap = style::LineCapType::Round; - options.joinType = style::LineJoinType::Round; - builder->addPolyline(geom, options); + constexpr auto numPoints = 100; + GeometryCoordinates polyline; + for (auto ipoint{0}; ipoint < numPoints; ++ipoint) { + polyline.emplace_back(ipoint * util::EXTENT / numPoints, + std::sin(ipoint * 2 * M_PI / numPoints) * util::EXTENT / numLines / 2.f); + } - // create tweaker - auto tweaker = std::make_shared(); + for (auto index{0}; index < numLines; ++index) { + for (auto& p : polyline) { + p.y += util::EXTENT / numLines; + } - // finish - builder->flush(); - for (auto& drawable : builder->clearDrawables()) { - drawable->setTileID(tileID); - drawable->addTweaker(tweaker); + // set property values + interface.setLineOptions(options[index]); - tileLayerGroup->addDrawable(RenderPass::Translucent, tileID, std::move(drawable)); + // add polyline + interface.addPolyline(polyline); } - } - void deinitialize() override { - // layerGroup->reset(); + // finish + interface.finish(); } -private: - std::shared_ptr layerGroup; + void deinitialize() override {} }; TEST(CustomDrawableLayer, Basic) { + using namespace mbgl; + using namespace mbgl::style; + util::RunLoop loop; HeadlessFrontend frontend{1}; diff --git a/test/fixtures/custom_drawable_layer/basic/expected.png b/test/fixtures/custom_drawable_layer/basic/expected.png index 11898f329ef72b4315a0e055cc5044d5b77c09ff..4aa9e67a43c171df99c382fcfea7b28f001aa984 100644 GIT binary patch literal 30223 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 literal 9867 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