diff options
-rw-r--r-- | cmake/core-files.cmake | 1 | ||||
-rw-r--r-- | src/mbgl/layout/symbol_layout.cpp | 90 | ||||
-rw-r--r-- | src/mbgl/layout/symbol_layout.hpp | 9 | ||||
-rw-r--r-- | src/mbgl/style/layers/symbol_layer_impl.cpp | 6 | ||||
-rw-r--r-- | src/mbgl/style/layers/symbol_layer_impl.hpp | 3 | ||||
-rw-r--r-- | src/mbgl/text/glyph.cpp | 2 | ||||
-rw-r--r-- | src/mbgl/text/glyph.hpp | 19 | ||||
-rw-r--r-- | src/mbgl/text/glyph_atlas.cpp | 243 | ||||
-rw-r--r-- | src/mbgl/text/glyph_atlas.hpp | 68 | ||||
-rw-r--r-- | src/mbgl/text/glyph_pbf.cpp | 3 | ||||
-rw-r--r-- | src/mbgl/text/glyph_pbf.hpp | 6 | ||||
-rw-r--r-- | src/mbgl/text/glyph_set.cpp | 275 | ||||
-rw-r--r-- | src/mbgl/text/glyph_set.hpp | 31 | ||||
-rw-r--r-- | src/mbgl/text/shaping.cpp | 280 | ||||
-rw-r--r-- | src/mbgl/text/shaping.hpp | 14 | ||||
-rw-r--r-- | src/mbgl/tile/geometry_tile.cpp | 13 | ||||
-rw-r--r-- | src/mbgl/tile/geometry_tile.hpp | 9 | ||||
-rw-r--r-- | src/mbgl/tile/geometry_tile_worker.cpp | 22 | ||||
-rw-r--r-- | src/mbgl/tile/geometry_tile_worker.hpp | 6 | ||||
-rw-r--r-- | src/mbgl/util/exclusive.hpp | 27 | ||||
-rw-r--r-- | test/text/glyph_atlas.test.cpp | 44 | ||||
-rw-r--r-- | test/text/glyph_pbf.test.cpp | 2 |
22 files changed, 631 insertions, 542 deletions
diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake index 45a3ed25b3..519c52a7fb 100644 --- a/cmake/core-files.cmake +++ b/cmake/core-files.cmake @@ -480,7 +480,6 @@ set(MBGL_CORE_FILES src/mbgl/util/dtoa.cpp src/mbgl/util/dtoa.hpp src/mbgl/util/event.cpp - src/mbgl/util/exclusive.hpp src/mbgl/util/font_stack.cpp src/mbgl/util/geo.cpp src/mbgl/util/geojson.cpp diff --git a/src/mbgl/layout/symbol_layout.cpp b/src/mbgl/layout/symbol_layout.cpp index 3a2c082ad8..513a5e071e 100644 --- a/src/mbgl/layout/symbol_layout.cpp +++ b/src/mbgl/layout/symbol_layout.cpp @@ -7,10 +7,9 @@ #include <mbgl/style/layers/symbol_layer.hpp> #include <mbgl/style/layers/symbol_layer_impl.hpp> #include <mbgl/sprite/sprite_atlas.hpp> -#include <mbgl/text/glyph_atlas.hpp> #include <mbgl/text/get_anchors.hpp> -#include <mbgl/text/glyph_atlas.hpp> #include <mbgl/text/collision_tile.hpp> +#include <mbgl/text/shaping.hpp> #include <mbgl/util/constants.hpp> #include <mbgl/util/utf.hpp> #include <mbgl/util/token.hpp> @@ -33,7 +32,8 @@ using namespace style; SymbolLayout::SymbolLayout(const BucketParameters& parameters, const std::vector<const Layer*>& layers, const GeometryTileLayer& sourceLayer, - SpriteAtlas& spriteAtlas_) + SpriteAtlas& spriteAtlas_, + GlyphDependencies& glyphDependencies) : sourceLayerName(sourceLayer.getName()), bucketName(layers.at(0)->getID()), overscaling(parameters.tileID.overscaleFactor()), @@ -93,7 +93,7 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, )); } - // Determine and load glyph ranges + // Determine glyph dependencies const size_t featureCount = sourceLayer.featureCount(); for (size_t i = 0; i < featureCount; ++i) { auto feature = sourceLayer.getFeature(i); @@ -139,9 +139,9 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, // Loop through all characters of this text and collect unique codepoints. for (char16_t chr : *ft.text) { - ranges.insert(getGlyphRange(chr)); + glyphDependencies[layout.get<TextFont>()].insert(chr); if (char16_t verticalChr = util::i18n::verticalizePunctuation(chr)) { - ranges.insert(getGlyphRange(verticalChr)); + glyphDependencies[layout.get<TextFont>()].insert(verticalChr); } } } @@ -164,13 +164,13 @@ bool SymbolLayout::hasSymbolInstances() const { return !symbolInstances.empty(); } -bool SymbolLayout::canPrepare(GlyphAtlas& glyphAtlas) { - const bool hasTextField = layout.get<TextField>().match( - [&] (const std::string& s) { return !s.empty(); }, - [&] (const auto&) { return true; } - ); - - if (hasTextField && !layout.get<TextFont>().empty() && !glyphAtlas.hasGlyphRanges(layout.get<TextFont>(), ranges)) { +bool SymbolLayout::canPrepare(const GlyphPositionMap& glyphPositions) { + // TODO: This is a needlessly complex way to check if we can move to the next step, we really just want to wait until + // we've gotten a reply from 'getGlyphs'. I'm just keeping this in place here to reduce the number of moving parts in the refactor + const bool hasTextField = layout.get<TextField>().match([&] (const std::string& s) { return !s.empty(); }, + [&] (const auto&) { return true; } ); + + if (hasTextField && glyphPositions.empty()) { return false; } @@ -181,8 +181,7 @@ bool SymbolLayout::canPrepare(GlyphAtlas& glyphAtlas) { return true; } -void SymbolLayout::prepare(uintptr_t tileUID, - GlyphAtlas& glyphAtlas) { +void SymbolLayout::prepare(const GlyphPositionMap& glyphs) { float horizontalAlign = 0.5; float verticalAlign = 0.5; @@ -224,7 +223,6 @@ void SymbolLayout::prepare(uintptr_t tileUID, layout.get<TextJustify>() == TextJustifyType::Left ? 0 : 0.5; - auto glyphSet = glyphAtlas.getGlyphSet(layout.get<TextFont>()); const bool textAlongLine = layout.get<TextRotationAlignment>() == AlignmentType::Map && layout.get<SymbolPlacement>() == SymbolPlacementType::Line; @@ -239,34 +237,33 @@ void SymbolLayout::prepare(uintptr_t tileUID, // if feature has text, shape the text if (feature.text) { - auto getShaping = [&] (const std::u16string& text, WritingModeType writingMode) { - const float oneEm = 24.0f; - const Shaping result = glyphSet->getShaping( - /* string */ text, - /* maxWidth: ems */ layout.get<SymbolPlacement>() != SymbolPlacementType::Line ? - layout.get<TextMaxWidth>() * oneEm : 0, - /* lineHeight: ems */ layout.get<TextLineHeight>() * oneEm, - /* horizontalAlign */ horizontalAlign, - /* verticalAlign */ verticalAlign, - /* justify */ justify, - /* spacing: ems */ layout.get<TextLetterSpacing>() * oneEm, - /* translate */ Point<float>(layout.get<TextOffset>()[0], layout.get<TextOffset>()[1]), - /* verticalHeight */ oneEm, - /* writingMode */ writingMode, - /* bidirectional algorithm object */ bidi); - - // Add the glyphs we need for this label to the glyph atlas. - if (result) { - glyphAtlas.addGlyphs(tileUID, text, layout.get<TextFont>(), glyphSet, face); + auto glyphPositions = glyphs.find(layout.get<TextFont>()); + if (glyphPositions != glyphs.end()) { // If there are no glyphs available for this feature, skip shaping + auto applyShaping = [&] (const std::u16string& text, WritingModeType writingMode) { + const float oneEm = 24.0f; + const Shaping result = getShaping( + /* string */ text, + /* maxWidth: ems */ layout.get<SymbolPlacement>() != SymbolPlacementType::Line ? + layout.get<TextMaxWidth>() * oneEm : 0, + /* lineHeight: ems */ layout.get<TextLineHeight>() * oneEm, + /* horizontalAlign */ horizontalAlign, + /* verticalAlign */ verticalAlign, + /* justify */ justify, + /* spacing: ems */ layout.get<TextLetterSpacing>() * oneEm, + /* translate */ Point<float>(layout.get<TextOffset>()[0], layout.get<TextOffset>()[1]), + /* verticalHeight */ oneEm, + /* writingMode */ writingMode, + /* bidirectional algorithm object */ bidi, + /* glyphs */ glyphPositions->second); + + return result; + }; + + shapedTextOrientations.first = applyShaping(*feature.text, WritingModeType::Horizontal); + + if (util::i18n::allowsVerticalWritingMode(*feature.text) && textAlongLine) { + shapedTextOrientations.second = applyShaping(util::i18n::verticalizePunctuation(*feature.text), WritingModeType::Vertical); } - - return result; - }; - - shapedTextOrientations.first = getShaping(*feature.text, WritingModeType::Horizontal); - - if (util::i18n::allowsVerticalWritingMode(*feature.text) && textAlongLine) { - shapedTextOrientations.second = getShaping(util::i18n::verticalizePunctuation(*feature.text), WritingModeType::Vertical); } } @@ -291,7 +288,8 @@ void SymbolLayout::prepare(uintptr_t tileUID, // if either shapedText or icon position is present, add the feature if (shapedTextOrientations.first || shapedIcon) { - addFeature(std::distance(features.begin(), it), feature, shapedTextOrientations, shapedIcon, face); + auto glyphPositionsIt = glyphs.find(layout.get<TextFont>()); + addFeature(std::distance(features.begin(), it), feature, shapedTextOrientations, shapedIcon, glyphPositionsIt == glyphs.end() ? GlyphPositions() : glyphPositionsIt->second); } feature.geometry.clear(); @@ -304,7 +302,7 @@ void SymbolLayout::addFeature(const std::size_t index, const SymbolFeature& feature, const std::pair<Shaping, Shaping>& shapedTextOrientations, const PositionedIcon& shapedIcon, - const GlyphPositions& face) { + const GlyphPositions& glyphs) { const float minScale = 0.5f; const float glyphSize = 24.0f; @@ -350,7 +348,7 @@ void SymbolLayout::addFeature(const std::size_t index, symbolInstances.emplace_back(anchor, line, shapedTextOrientations, shapedIcon, layout, addToBuffers, symbolInstances.size(), textBoxScale, textPadding, textPlacement, iconBoxScale, iconPadding, iconPlacement, - face, indexedFeature, index); + glyphs, indexedFeature, index); }; const auto& type = feature.getType(); diff --git a/src/mbgl/layout/symbol_layout.hpp b/src/mbgl/layout/symbol_layout.hpp index 5b14090d97..fe5be1e4a9 100644 --- a/src/mbgl/layout/symbol_layout.hpp +++ b/src/mbgl/layout/symbol_layout.hpp @@ -32,12 +32,12 @@ public: SymbolLayout(const style::BucketParameters&, const std::vector<const style::Layer*>&, const GeometryTileLayer&, - SpriteAtlas&); + SpriteAtlas&, + GlyphDependencies&); - bool canPrepare(GlyphAtlas&); + bool canPrepare(const GlyphPositionMap& glyphs); - void prepare(uintptr_t tileUID, - GlyphAtlas&); + void prepare(const GlyphPositionMap& glyphs); std::unique_ptr<SymbolBucket> place(CollisionTile&); @@ -89,7 +89,6 @@ private: bool sdfIcons = false; bool iconsNeedLinear = false; - GlyphRangeSet ranges; std::vector<SymbolInstance> symbolInstances; std::vector<SymbolFeature> features; diff --git a/src/mbgl/style/layers/symbol_layer_impl.cpp b/src/mbgl/style/layers/symbol_layer_impl.cpp index ff59b14d65..47d802a118 100644 --- a/src/mbgl/style/layers/symbol_layer_impl.cpp +++ b/src/mbgl/style/layers/symbol_layer_impl.cpp @@ -36,11 +36,13 @@ std::unique_ptr<Bucket> SymbolLayer::Impl::createBucket(const BucketParameters&, std::unique_ptr<SymbolLayout> SymbolLayer::Impl::createLayout(const BucketParameters& parameters, const std::vector<const Layer*>& group, - const GeometryTileLayer& layer) const { + const GeometryTileLayer& layer, + GlyphDependencies& glyphDependencies) const { return std::make_unique<SymbolLayout>(parameters, group, layer, - *spriteAtlas); + *spriteAtlas, + glyphDependencies); } IconPaintProperties::Evaluated SymbolLayer::Impl::iconPaintProperties() const { diff --git a/src/mbgl/style/layers/symbol_layer_impl.hpp b/src/mbgl/style/layers/symbol_layer_impl.hpp index 1e9f05e4c7..16bd67a4d4 100644 --- a/src/mbgl/style/layers/symbol_layer_impl.hpp +++ b/src/mbgl/style/layers/symbol_layer_impl.hpp @@ -1,5 +1,6 @@ #pragma once +#include <mbgl/text/glyph.hpp> #include <mbgl/util/variant.hpp> #include <mbgl/style/layer_impl.hpp> #include <mbgl/style/layers/symbol_layer.hpp> @@ -67,7 +68,7 @@ public: std::unique_ptr<Bucket> createBucket(const BucketParameters&, const std::vector<const Layer*>&) const override; std::unique_ptr<SymbolLayout> createLayout(const BucketParameters&, const std::vector<const Layer*>&, - const GeometryTileLayer&) const; + const GeometryTileLayer&, GlyphDependencies&) const; IconPaintProperties::Evaluated iconPaintProperties() const; TextPaintProperties::Evaluated textPaintProperties() const; diff --git a/src/mbgl/text/glyph.cpp b/src/mbgl/text/glyph.cpp index 29929b73e6..74863d7435 100644 --- a/src/mbgl/text/glyph.cpp +++ b/src/mbgl/text/glyph.cpp @@ -3,7 +3,7 @@ namespace mbgl { // Note: this only works for the BMP -GlyphRange getGlyphRange(char16_t glyph) { +GlyphRange getGlyphRange(GlyphID glyph) { unsigned start = (glyph/256) * 256; unsigned end = (start + 255); if (start > 65280) start = 65280; diff --git a/src/mbgl/text/glyph.hpp b/src/mbgl/text/glyph.hpp index 175a36f0fa..6aa4e11b1c 100644 --- a/src/mbgl/text/glyph.hpp +++ b/src/mbgl/text/glyph.hpp @@ -1,6 +1,7 @@ #pragma once #include <mbgl/text/glyph_range.hpp> +#include <mbgl/util/font_stack.hpp> #include <mbgl/util/rect.hpp> #include <mbgl/util/traits.hpp> #include <mbgl/util/image.hpp> @@ -12,8 +13,11 @@ namespace mbgl { +typedef char16_t GlyphID; +typedef std::set<GlyphID> GlyphIDs; + // Note: this only works for the BMP -GlyphRange getGlyphRange(char16_t glyph); +GlyphRange getGlyphRange(GlyphID glyph); struct GlyphMetrics { explicit operator bool() const { @@ -50,14 +54,14 @@ struct Glyph { const GlyphMetrics metrics; }; -typedef std::map<uint32_t, Glyph> GlyphPositions; +typedef std::map<GlyphID, Glyph> GlyphPositions; class PositionedGlyph { public: - explicit PositionedGlyph(uint32_t glyph_, float x_, float y_, float angle_) + explicit PositionedGlyph(GlyphID glyph_, float x_, float y_, float angle_) : glyph(glyph_), x(x_), y(y_), angle(angle_) {} - uint32_t glyph = 0; + GlyphID glyph = 0; float x = 0; float y = 0; float angle = 0; @@ -86,7 +90,7 @@ public: // also need to be reencoded. static constexpr const uint8_t borderSize = 3; - uint32_t id = 0; + GlyphID id = 0; // A signed distance field of the glyph with a border (see above). AlphaImage bitmap; @@ -121,4 +125,9 @@ constexpr WritingModeType operator~(WritingModeType value) { return WritingModeType(~mbgl::underlying_type(value)); } +typedef std::map<FontStack,GlyphIDs> GlyphDependencies; +typedef std::map<FontStack,GlyphRangeSet> GlyphRangeDependencies; +typedef std::map<FontStack,GlyphPositions> GlyphPositionMap; + + } // end namespace mbgl diff --git a/src/mbgl/text/glyph_atlas.cpp b/src/mbgl/text/glyph_atlas.cpp index 1b3f7518b5..b242fb490a 100644 --- a/src/mbgl/text/glyph_atlas.cpp +++ b/src/mbgl/text/glyph_atlas.cpp @@ -22,81 +22,138 @@ GlyphAtlas::GlyphAtlas(const Size size, FileSource& fileSource_) GlyphAtlas::~GlyphAtlas() = default; -void GlyphAtlas::requestGlyphRange(const FontStack& fontStack, const GlyphRange& range) { - std::lock_guard<std::mutex> lock(mutex); - auto& rangeSets = entries[fontStack].ranges; - - const auto& rangeSetsIt = rangeSets.find(range); - if (rangeSetsIt != rangeSets.end()) { - return; +GlyphSet& GlyphAtlas::getGlyphSet(const FontStack& fontStack) { + return entries[fontStack].glyphSet; +} + +bool GlyphAtlas::hasGlyphRanges(const FontStack& fontStack, const GlyphRangeSet& ranges) const { + for (const auto& range : ranges) { + if (!hasGlyphRange(fontStack,range)) { + return false; + } } - - rangeSets.emplace(std::piecewise_construct, - std::forward_as_tuple(range), - std::forward_as_tuple(this, fontStack, range, observer, fileSource)); + return true; +} + +bool rangeIsParsed(const std::map<GlyphRange, GlyphPBF>& ranges, const GlyphRange& range) { + auto rangeIt = ranges.find(range); + if (rangeIt == ranges.end()) + return false; + + return rangeIt->second.isParsed(); } + +bool GlyphAtlas::hasGlyphRange(const FontStack& fontStack, const GlyphRange& range) const { + auto entry = entries.find(fontStack); + if (entry == entries.end()) + return false; + + return rangeIsParsed(entry->second.ranges, range); +} + +void GlyphAtlas::getGlyphs(GlyphRequestor& requestor, GlyphDependencies glyphDependencies) { + auto dependencies = std::make_shared<GlyphDependencies>(std::move(glyphDependencies)); + + // Figure out which glyph ranges need to be fetched. For each range that does need to + // be fetched, record an entry mapping the requestor to a shared pointer containing the + // dependencies. When the shared pointer becomes unique, we know that all the dependencies + // for that requestor have been fetched, and can notify it of completion. + for (const auto& dependency : *dependencies) { + const FontStack& fontStack = dependency.first; + const GlyphIDs& glyphIDs = dependency.second; + + Entry& entry = entries[fontStack]; + + GlyphRangeSet processedRanges; + for (const auto& glyphID : glyphIDs) { + GlyphRange range = getGlyphRange(glyphID); + if (processedRanges.find(range) == processedRanges.end() && !rangeIsParsed(entry.ranges, range)) { + if (entry.ranges.find(range) == entry.ranges.end()) { + entry.ranges.emplace(std::piecewise_construct, + std::forward_as_tuple(range), + std::forward_as_tuple(this, fontStack, range, this, fileSource)); + } -bool GlyphAtlas::hasGlyphRanges(const FontStack& fontStack, const GlyphRangeSet& glyphRanges) { - if (glyphRanges.empty()) { - return true; + entry.ranges.find(range)->second.requestors[&requestor] = dependencies; + } + processedRanges.insert(range); + } } - std::lock_guard<std::mutex> lock(mutex); - const auto& rangeSets = entries[fontStack].ranges; + // If the shared dependencies pointer is already unique, then all dependent glyph ranges + // have already been loaded. Send a notification immediately. + if (dependencies.unique()) { + addGlyphs(requestor, *dependencies); + } +} - bool hasRanges = true; - for (const auto& range : glyphRanges) { - const auto& rangeSetsIt = rangeSets.find(range); - if (rangeSetsIt == rangeSets.end()) { - // Push the request to the MapThread, so we can easly cancel - // if it is still pending when we destroy this object. - workQueue.push(std::bind(&GlyphAtlas::requestGlyphRange, this, fontStack, range)); +void GlyphAtlas::setObserver(GlyphAtlasObserver* observer_) { + observer = observer_; +} + +void GlyphAtlas::onGlyphsError(const FontStack& fontStack, const GlyphRange& range, std::exception_ptr err) { + if (observer) { + observer->onGlyphsError(fontStack, range, err); + } +} - hasRanges = false; - continue; - } +void GlyphAtlas::onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& range) { + Entry& entry = entries[fontStack]; - if (!rangeSetsIt->second.isParsed()) { - hasRanges = false; + auto it = entry.ranges.find(range); + if (it != entry.ranges.end()) { + for (auto& pair : it->second.requestors) { + GlyphRequestor& requestor = *pair.first; + const std::shared_ptr<GlyphDependencies>& dependencies = pair.second; + if (dependencies.unique()) { + addGlyphs(requestor, *dependencies); + } } + it->second.requestors.clear(); } - return hasRanges; + if (observer) { + observer->onGlyphsLoaded(fontStack, range); + } } -util::exclusive<GlyphSet> GlyphAtlas::getGlyphSet(const FontStack& fontStack) { - auto lock = std::make_unique<std::lock_guard<std::mutex>>(mutex); - return { &entries[fontStack].glyphSet, std::move(lock) }; -} +void GlyphAtlas::addGlyphs(GlyphRequestor& requestor, const GlyphDependencies& glyphDependencies) { + GlyphPositionMap glyphPositions; -void GlyphAtlas::setObserver(GlyphAtlasObserver* observer_) { - observer = observer_; -} + for (const auto& dependency : glyphDependencies) { + const FontStack& fontStack = dependency.first; + const GlyphIDs& glyphIDs = dependency.second; -void GlyphAtlas::addGlyphs(uintptr_t tileUID, - const std::u16string& text, - const FontStack& fontStack, - const util::exclusive<GlyphSet>& glyphSet, - GlyphPositions& face) -{ - const std::map<uint32_t, SDFGlyph>& sdfs = glyphSet->getSDFs(); + GlyphPositions& positions = glyphPositions[fontStack]; + Entry& entry = entries[fontStack]; + const auto& sdfs = entry.glyphSet.getSDFs(); - for (char16_t chr : text) - { - auto sdf_it = sdfs.find(chr); - if (sdf_it == sdfs.end()) { - continue; - } + for (const auto& glyphID : glyphIDs) { + auto it = sdfs.find(glyphID); + if (it == sdfs.end()) + continue; + + addGlyph(requestor, fontStack, it->second); - const SDFGlyph& sdf = sdf_it->second; - Rect<uint16_t> rect = addGlyph(tileUID, fontStack, sdf); - face.emplace(chr, Glyph{rect, sdf.metrics}); + // It's possible to have an SDF without a valid position (if the SDF was malformed). + // We indicate this case with Rect<uint16_t>(0,0,0,0). + auto glyphRect = entry.glyphValues.find(glyphID); + const Rect<uint16_t> rect = glyphRect == entry.glyphValues.end() + ? Rect<uint16_t>(0,0,0,0) + : glyphRect->second.rect; + + positions.emplace(std::piecewise_construct, + std::forward_as_tuple(glyphID), + std::forward_as_tuple(rect, it->second.metrics)); + } } + + requestor.onGlyphsAvailable(glyphPositions); } -Rect<uint16_t> GlyphAtlas::addGlyph(uintptr_t tileUID, - const FontStack& fontStack, - const SDFGlyph& glyph) +void GlyphAtlas::addGlyph(GlyphRequestor& requestor, + const FontStack& fontStack, + const SDFGlyph& glyph) { std::map<uint32_t, GlyphValue>& face = entries[fontStack].glyphValues; auto it = face.find(glyph.id); @@ -104,15 +161,15 @@ Rect<uint16_t> GlyphAtlas::addGlyph(uintptr_t tileUID, // The glyph is already in this texture. if (it != face.end()) { GlyphValue& value = it->second; - value.ids.insert(tileUID); - return value.rect; + value.ids.insert(&requestor); + return; } // Guard against glyphs that are too large, or that we don't need to place into the atlas since // they don't have any pixels. if (glyph.metrics.width == 0 || glyph.metrics.width >= 256 || glyph.metrics.height == 0 || glyph.metrics.height >= 256) { - return Rect<uint16_t>{ 0, 0, 0, 0 }; + return; } // Add a 1px border around every image. @@ -129,53 +186,59 @@ Rect<uint16_t> GlyphAtlas::addGlyph(uintptr_t tileUID, Rect<uint16_t> rect = bin.allocate(width, height); if (rect.w == 0) { Log::Error(Event::OpenGL, "glyph bitmap overflow"); - return rect; + return; } - face.emplace(glyph.id, GlyphValue { rect, tileUID }); + face.emplace(glyph.id, GlyphValue { rect, &requestor }); AlphaImage::copy(glyph.bitmap, image, { 0, 0 }, { rect.x + padding, rect.y + padding }, glyph.bitmap.size); dirty = true; - - return rect; } -void GlyphAtlas::removeGlyphs(uintptr_t tileUID) { - std::lock_guard<std::mutex> lock(mutex); +void GlyphAtlas::removeGlyphValues(GlyphRequestor& requestor, std::map<uint32_t, GlyphValue>& face) { + for (auto it = face.begin(); it != face.end(); /* we advance in the body */) { + GlyphValue& value = it->second; + value.ids.erase(&requestor); - for (auto& entry : entries) { - std::map<uint32_t, GlyphValue>& face = entry.second.glyphValues; - for (auto it = face.begin(); it != face.end(); /* we advance in the body */) { - GlyphValue& value = it->second; - value.ids.erase(tileUID); - - if (value.ids.empty()) { - const Rect<uint16_t>& rect = value.rect; - - // Clear out the bitmap. - uint8_t *target = image.data.get(); - for (uint32_t y = 0; y < rect.h; y++) { - uint32_t y1 = image.size.width * (rect.y + y) + rect.x; - for (uint32_t x = 0; x < rect.w; x++) { - target[y1 + x] = 0; - } + if (value.ids.empty()) { + const Rect<uint16_t>& rect = value.rect; + + // Clear out the bitmap. + uint8_t *target = image.data.get(); + for (uint32_t y = 0; y < rect.h; y++) { + uint32_t y1 = image.size.width * (rect.y + y) + rect.x; + for (uint32_t x = 0; x < rect.w; x++) { + target[y1 + x] = 0; } + } - bin.release(rect); + bin.release(rect); - // Make sure to post-increment the iterator: This will return the - // current iterator, but will go to the next position before we - // erase the element from the map. That way, the iterator stays - // valid. - face.erase(it++); - } else { - ++it; - } + // Make sure to post-increment the iterator: This will return the + // current iterator, but will go to the next position before we + // erase the element from the map. That way, the iterator stays + // valid. + face.erase(it++); + } else { + ++it; } } } +void GlyphAtlas::removePendingRanges(mbgl::GlyphRequestor &requestor, std::map<GlyphRange, GlyphPBF> &ranges) { + for (auto it = ranges.begin(); it != ranges.end(); it++) { + it->second.requestors.erase(&requestor); + } +} + +void GlyphAtlas::removeGlyphs(GlyphRequestor& requestor) { + for (auto& entry : entries) { + removeGlyphValues(requestor, entry.second.glyphValues); + removePendingRanges(requestor, entry.second.ranges); + } +} + Size GlyphAtlas::getSize() const { return image.size; } @@ -186,7 +249,7 @@ void GlyphAtlas::upload(gl::Context& context, gl::TextureUnit unit) { } else if (dirty) { context.updateTexture(*texture, image, unit); } - + dirty = false; } diff --git a/src/mbgl/text/glyph_atlas.hpp b/src/mbgl/text/glyph_atlas.hpp index 8267630096..e40f1b5147 100644 --- a/src/mbgl/text/glyph_atlas.hpp +++ b/src/mbgl/text/glyph_atlas.hpp @@ -1,46 +1,55 @@ #pragma once #include <mbgl/text/glyph.hpp> +#include <mbgl/text/glyph_atlas_observer.hpp> +#include <mbgl/text/glyph_range.hpp> #include <mbgl/text/glyph_set.hpp> #include <mbgl/geometry/binpack.hpp> #include <mbgl/util/noncopyable.hpp> #include <mbgl/util/optional.hpp> #include <mbgl/util/font_stack.hpp> -#include <mbgl/util/exclusive.hpp> #include <mbgl/util/work_queue.hpp> #include <mbgl/util/image.hpp> #include <mbgl/gl/texture.hpp> #include <mbgl/gl/object.hpp> -#include <atomic> #include <string> #include <unordered_set> #include <unordered_map> -#include <mutex> + +class GlyphAtlasTest; namespace mbgl { class FileSource; class GlyphPBF; -class GlyphAtlasObserver; namespace gl { class Context; } // namespace gl -class GlyphAtlas : public util::noncopyable { +class GlyphRequestor { +public: + virtual void onGlyphsAvailable(GlyphPositionMap) = 0; +}; + +class GlyphAtlas : public util::noncopyable, public GlyphAtlasObserver { public: GlyphAtlas(Size, FileSource&); ~GlyphAtlas(); - util::exclusive<GlyphSet> getGlyphSet(const FontStack&); - - // Returns true if the set of GlyphRanges are available and parsed or false - // if they are not. For the missing ranges, a request on the FileSource is - // made and when the glyph if finally parsed, it gets added to the respective - // GlyphSet and a signal is emitted to notify the observers. This method - // can be called from any thread. - bool hasGlyphRanges(const FontStack&, const GlyphRangeSet&); + GlyphSet& getGlyphSet(const FontStack&); + + // Workers send a `getGlyphs` message to the main thread once they have determined + // which glyphs they will need. Invoking this method will increment reference + // counts for all the glyphs in `GlyphDependencies`. If all glyphs are already + // locally available, the observer will be notified that the glyphs are available + // immediately. Otherwise, a request on the FileSource is made, and when all glyphs + // are parsed and added to the atlas, the observer will be notified. + // Workers are given a copied 'GlyphPositions' map to use for placing their glyphs. + // The positions specified in this object are guaranteed to be + // valid for the lifetime of the tile. + void getGlyphs(GlyphRequestor& requestor, GlyphDependencies glyphs); void setURL(const std::string &url) { glyphURL = url; @@ -52,12 +61,7 @@ public: void setObserver(GlyphAtlasObserver* observer); - void addGlyphs(uintptr_t tileUID, - const std::u16string& text, - const FontStack&, - const util::exclusive<GlyphSet>&, - GlyphPositions&); - void removeGlyphs(uintptr_t tileUID); + void removeGlyphs(GlyphRequestor&); // Binds the atlas texture to the GPU, and uploads data if it is out of date. void bind(gl::Context&, gl::TextureUnit unit); @@ -67,22 +71,29 @@ public: void upload(gl::Context&, gl::TextureUnit unit); Size getSize() const; + + virtual void onGlyphsLoaded(const FontStack&, const GlyphRange&); + virtual void onGlyphsError(const FontStack&, const GlyphRange&, std::exception_ptr); + + friend class ::GlyphAtlasTest; private: - void requestGlyphRange(const FontStack&, const GlyphRange&); + void addGlyphs(GlyphRequestor& requestor, const GlyphDependencies& glyphDependencies); - Rect<uint16_t> addGlyph(uintptr_t tileID, - const FontStack&, - const SDFGlyph&); + // Only used by GlyphAtlasTest + bool hasGlyphRanges(const FontStack&, const GlyphRangeSet& ranges) const; + bool hasGlyphRange(const FontStack&, const GlyphRange& range) const; + void addGlyph(GlyphRequestor& requestor, const FontStack&, const SDFGlyph&); + FileSource& fileSource; std::string glyphURL; struct GlyphValue { - GlyphValue(Rect<uint16_t> rect_, uintptr_t id) + GlyphValue(Rect<uint16_t> rect_, GlyphRequestor* id) : rect(std::move(rect_)), ids({ id }) {} Rect<uint16_t> rect; - std::unordered_set<uintptr_t> ids; + std::unordered_set<GlyphRequestor*> ids; }; struct Entry { @@ -92,14 +103,15 @@ private: }; std::unordered_map<FontStack, Entry, FontStackHash> entries; - std::mutex mutex; + + void removeGlyphValues(GlyphRequestor& requestor, std::map<uint32_t, GlyphValue>& face); + void removePendingRanges(GlyphRequestor& requestor, std::map<GlyphRange, GlyphPBF>& ranges); - util::WorkQueue workQueue; GlyphAtlasObserver* observer = nullptr; BinPack<uint16_t> bin; AlphaImage image; - std::atomic<bool> dirty; + bool dirty; mbgl::optional<gl::Texture> texture; }; diff --git a/src/mbgl/text/glyph_pbf.cpp b/src/mbgl/text/glyph_pbf.cpp index 5c57d278db..26eff812b7 100644 --- a/src/mbgl/text/glyph_pbf.cpp +++ b/src/mbgl/text/glyph_pbf.cpp @@ -16,6 +16,7 @@ namespace mbgl { namespace { +// Parses a Glyph Protobuf and inserts it into the GlyphAtlas. Must be called from main thread. void parseGlyphPBF(GlyphSet& glyphSet, const GlyphRange& glyphRange, const std::string& data) { protozero::pbf_reader glyphs_pbf(data); @@ -117,7 +118,7 @@ GlyphPBF::GlyphPBF(GlyphAtlas* atlas, observer->onGlyphsLoaded(fontStack, glyphRange); } else { try { - parseGlyphPBF(**atlas->getGlyphSet(fontStack), glyphRange, *res.data); + parseGlyphPBF(atlas->getGlyphSet(fontStack), glyphRange, *res.data); } catch (...) { observer->onGlyphsError(fontStack, glyphRange, std::current_exception()); return; diff --git a/src/mbgl/text/glyph_pbf.hpp b/src/mbgl/text/glyph_pbf.hpp index 7412ebe411..d5b89cd107 100644 --- a/src/mbgl/text/glyph_pbf.hpp +++ b/src/mbgl/text/glyph_pbf.hpp @@ -8,10 +8,12 @@ #include <functional> #include <string> #include <memory> +#include <unordered_map> namespace mbgl { class GlyphAtlas; +class GlyphRequestor; class GlyphAtlasObserver; class AsyncRequest; class FileSource; @@ -29,10 +31,10 @@ public: return parsed; } -private: - std::atomic<bool> parsed; + bool parsed; std::unique_ptr<AsyncRequest> req; GlyphAtlasObserver* observer = nullptr; + std::unordered_map<GlyphRequestor*, std::shared_ptr<GlyphDependencies>> requestors; }; } // namespace mbgl diff --git a/src/mbgl/text/glyph_set.cpp b/src/mbgl/text/glyph_set.cpp index 19a6e2cddd..b8e155502e 100644 --- a/src/mbgl/text/glyph_set.cpp +++ b/src/mbgl/text/glyph_set.cpp @@ -1,13 +1,6 @@ -#include <mbgl/math/minmax.hpp> #include <mbgl/text/glyph_set.hpp> -#include <mbgl/util/i18n.hpp> #include <mbgl/util/logging.hpp> -#include <boost/algorithm/string.hpp> - -#include <algorithm> -#include <cassert> - namespace mbgl { void GlyphSet::insert(uint32_t id, SDFGlyph&& glyph) { @@ -34,272 +27,4 @@ const std::map<uint32_t, SDFGlyph>& GlyphSet::getSDFs() const { return sdfs; } -const Shaping GlyphSet::getShaping(const std::u16string& logicalInput, - const float maxWidth, - const float lineHeight, - const float horizontalAlign, - const float verticalAlign, - const float justify, - const float spacing, - const Point<float>& translate, - const float verticalHeight, - const WritingModeType writingMode, - BiDi& bidi) const { - Shaping shaping(translate.x * 24, translate.y * 24, writingMode); - - std::vector<std::u16string> reorderedLines = - bidi.processText(logicalInput, - determineLineBreaks(logicalInput, spacing, maxWidth, writingMode)); - - shapeLines(shaping, reorderedLines, spacing, lineHeight, horizontalAlign, verticalAlign, - justify, translate, verticalHeight, writingMode); - - return shaping; -} - -void align(Shaping& shaping, - const float justify, - const float horizontalAlign, - const float verticalAlign, - const float maxLineLength, - const float lineHeight, - const std::size_t lineCount, - const Point<float>& translate) { - const float shiftX = - (justify - horizontalAlign) * maxLineLength + ::round(translate.x * 24 /* one em */); - const float shiftY = - (-verticalAlign * lineCount + 0.5) * lineHeight + ::round(translate.y * 24 /* one em */); - - for (auto& glyph : shaping.positionedGlyphs) { - glyph.x += shiftX; - glyph.y += shiftY; - } -} - -// justify left = 0, right = 1, center = .5 -void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs, - const std::map<uint32_t, SDFGlyph>& sdfs, - std::size_t start, - std::size_t end, - float justify) { - if (!justify) { - return; - } - - PositionedGlyph& glyph = positionedGlyphs[end]; - auto it = sdfs.find(glyph.glyph); - if (it != sdfs.end()) { - const uint32_t lastAdvance = it->second.metrics.advance; - const float lineIndent = float(glyph.x + lastAdvance) * justify; - - for (std::size_t j = start; j <= end; j++) { - positionedGlyphs[j].x -= lineIndent; - } - } -} - -float GlyphSet::determineAverageLineWidth(const std::u16string& logicalInput, - const float spacing, - float maxWidth) const { - float totalWidth = 0; - - for (char16_t chr : logicalInput) { - auto it = sdfs.find(chr); - if (it != sdfs.end()) { - totalWidth += it->second.metrics.advance + spacing; - } - } - - int32_t targetLineCount = std::fmax(1, std::ceil(totalWidth / maxWidth)); - return totalWidth / targetLineCount; -} - -float calculateBadness(const float lineWidth, const float targetWidth, const float penalty, const bool isLastBreak) { - const float raggedness = std::pow(lineWidth - targetWidth, 2); - if (isLastBreak) { - // Favor finals lines shorter than average over longer than average - if (lineWidth < targetWidth) { - return raggedness / 2; - } else { - return raggedness * 2; - } - } - if (penalty < 0) { - return raggedness - std::pow(penalty, 2); - } - return raggedness + std::pow(penalty, 2); -} - -float calculatePenalty(char16_t codePoint, char16_t nextCodePoint) { - float penalty = 0; - // Force break on newline - if (codePoint == 0x0a) { - penalty -= 10000; - } - // Penalize open parenthesis at end of line - if (codePoint == 0x28 || codePoint == 0xff08) { - penalty += 50; - } - - // Penalize close parenthesis at beginning of line - if (nextCodePoint == 0x29 || nextCodePoint == 0xff09) { - penalty += 50; - } - - return penalty; -} - -struct PotentialBreak { - PotentialBreak(const std::size_t p_index, const float p_x, const PotentialBreak* p_priorBreak, const float p_badness) - : index(p_index), x(p_x), priorBreak(p_priorBreak), badness(p_badness) - {} - - const std::size_t index; - const float x; - const PotentialBreak* priorBreak; - const float badness; -}; - - -PotentialBreak evaluateBreak(const std::size_t breakIndex, const float breakX, const float targetWidth, const std::list<PotentialBreak>& potentialBreaks, const float penalty, const bool isLastBreak) { - // We could skip evaluating breaks where the line length (breakX - priorBreak.x) > maxWidth - // ...but in fact we allow lines longer than maxWidth (if there's no break points) - // ...and when targetWidth and maxWidth are close, strictly enforcing maxWidth can give - // more lopsided results. - - const PotentialBreak* bestPriorBreak = nullptr; - float bestBreakBadness = calculateBadness(breakX, targetWidth, penalty, isLastBreak); - for (const auto& potentialBreak : potentialBreaks) { - const float lineWidth = breakX - potentialBreak.x; - float breakBadness = - calculateBadness(lineWidth, targetWidth, penalty, isLastBreak) + potentialBreak.badness; - if (breakBadness <= bestBreakBadness) { - bestPriorBreak = &potentialBreak; - bestBreakBadness = breakBadness; - } - } - - return PotentialBreak(breakIndex, breakX, bestPriorBreak, bestBreakBadness); -} - -std::set<std::size_t> leastBadBreaks(const PotentialBreak& lastLineBreak) { - std::set<std::size_t> leastBadBreaks = { lastLineBreak.index }; - const PotentialBreak* priorBreak = lastLineBreak.priorBreak; - while (priorBreak) { - leastBadBreaks.insert(priorBreak->index); - priorBreak = priorBreak->priorBreak; - } - return leastBadBreaks; -} - - -// We determine line breaks based on shaped text in logical order. Working in visual order would be -// more intuitive, but we can't do that because the visual order may be changed by line breaks! -std::set<std::size_t> GlyphSet::determineLineBreaks(const std::u16string& logicalInput, - const float spacing, - float maxWidth, - const WritingModeType writingMode) const { - if (!maxWidth || writingMode != WritingModeType::Horizontal) { - return {}; - } - - if (logicalInput.empty()) { - return {}; - } - - const float targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth); - - std::list<PotentialBreak> potentialBreaks; - float currentX = 0; - - for (std::size_t i = 0; i < logicalInput.size(); i++) { - const char16_t codePoint = logicalInput[i]; - auto it = sdfs.find(codePoint); - if (it != sdfs.end() && !boost::algorithm::is_any_of(u" \t\n\v\f\r")(codePoint)) { - currentX += it->second.metrics.advance + spacing; - } - - // Ideographic characters, spaces, and word-breaking punctuation that often appear without - // surrounding spaces. - if ((i < logicalInput.size() - 1) && - (util::i18n::allowsWordBreaking(codePoint) || util::i18n::allowsIdeographicBreaking(codePoint))) { - potentialBreaks.push_back(evaluateBreak(i+1, currentX, targetWidth, potentialBreaks, - calculatePenalty(codePoint, logicalInput[i+1]), - false)); - } - } - - return leastBadBreaks(evaluateBreak(logicalInput.size(), currentX, targetWidth, potentialBreaks, 0, true)); -} - -void GlyphSet::shapeLines(Shaping& shaping, - const std::vector<std::u16string>& lines, - const float spacing, - const float lineHeight, - const float horizontalAlign, - const float verticalAlign, - const float justify, - const Point<float>& translate, - const float verticalHeight, - const WritingModeType writingMode) const { - - // the y offset *should* be part of the font metadata - const int32_t yOffset = -17; - - float x = 0; - float y = yOffset; - - float maxLineLength = 0; - - for (std::u16string line : lines) { - // Collapse whitespace so it doesn't throw off justification - boost::algorithm::trim_if(line, boost::algorithm::is_any_of(u" \t\n\v\f\r")); - - if (line.empty()) { - y += lineHeight; // Still need a line feed after empty line - continue; - } - - std::size_t lineStartIndex = shaping.positionedGlyphs.size(); - for (char16_t chr : line) { - auto it = sdfs.find(chr); - if (it == sdfs.end()) { - continue; - } - - const SDFGlyph& glyph = it->second; - - if (writingMode == WritingModeType::Horizontal || !util::i18n::hasUprightVerticalOrientation(chr)) { - shaping.positionedGlyphs.emplace_back(chr, x, y, 0); - x += glyph.metrics.advance + spacing; - } else { - shaping.positionedGlyphs.emplace_back(chr, x, 0, -M_PI_2); - x += verticalHeight + spacing; - } - } - - // Only justify if we placed at least one glyph - if (shaping.positionedGlyphs.size() != lineStartIndex) { - float lineLength = x - spacing; // Don't count trailing spacing - maxLineLength = util::max(lineLength, maxLineLength); - - justifyLine(shaping.positionedGlyphs, sdfs, lineStartIndex, - shaping.positionedGlyphs.size() - 1, justify); - } - - x = 0; - y += lineHeight; - } - - align(shaping, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, - lines.size(), translate); - const uint32_t height = lines.size() * lineHeight; - - // Calculate the bounding box - shaping.top += -verticalAlign * height; - shaping.bottom = shaping.top + height; - shaping.left += -horizontalAlign * maxLineLength; - shaping.right = shaping.left + maxLineLength; -} - } // end namespace mbgl diff --git a/src/mbgl/text/glyph_set.hpp b/src/mbgl/text/glyph_set.hpp index 0342c82eb5..9f4bef94d2 100644 --- a/src/mbgl/text/glyph_set.hpp +++ b/src/mbgl/text/glyph_set.hpp @@ -1,6 +1,5 @@ #pragma once -#include <mbgl/text/bidi.hpp> #include <mbgl/text/glyph.hpp> #include <mbgl/util/geometry.hpp> @@ -10,38 +9,8 @@ class GlyphSet { public: void insert(uint32_t id, SDFGlyph&&); const std::map<uint32_t, SDFGlyph>& getSDFs() const; - const Shaping getShaping(const std::u16string& string, - float maxWidth, - float lineHeight, - float horizontalAlign, - float verticalAlign, - float justify, - float spacing, - const Point<float>& translate, - float verticalHeight, - const WritingModeType, - BiDi& bidi) const; private: - float determineAverageLineWidth(const std::u16string& logicalInput, - const float spacing, - float maxWidth) const; - std::set<std::size_t> determineLineBreaks(const std::u16string& logicalInput, - const float spacing, - float maxWidth, - const WritingModeType) const; - - void shapeLines(Shaping& shaping, - const std::vector<std::u16string>& lines, - const float spacing, - float lineHeight, - float horizontalAlign, - float verticalAlign, - float justify, - const Point<float>& translate, - float verticalHeight, - const WritingModeType) const; - std::map<uint32_t, SDFGlyph> sdfs; }; diff --git a/src/mbgl/text/shaping.cpp b/src/mbgl/text/shaping.cpp index e68566d419..7285d9c812 100644 --- a/src/mbgl/text/shaping.cpp +++ b/src/mbgl/text/shaping.cpp @@ -1,5 +1,12 @@ #include <mbgl/text/shaping.hpp> +#include <mbgl/util/i18n.hpp> #include <mbgl/layout/symbol_feature.hpp> +#include <mbgl/math/minmax.hpp> +#include <mbgl/text/bidi.hpp> + +#include <boost/algorithm/string.hpp> + +#include <algorithm> namespace mbgl { @@ -14,4 +21,277 @@ PositionedIcon shapeIcon(const SpriteAtlasElement& image, const std::array<float return PositionedIcon(image, y1, y2, x1, x2, iconRotation); } +void align(Shaping& shaping, + const float justify, + const float horizontalAlign, + const float verticalAlign, + const float maxLineLength, + const float lineHeight, + const std::size_t lineCount, + const Point<float>& translate) { + const float shiftX = + (justify - horizontalAlign) * maxLineLength + ::round(translate.x * 24 /* one em */); + const float shiftY = + (-verticalAlign * lineCount + 0.5) * lineHeight + ::round(translate.y * 24 /* one em */); + + for (auto& glyph : shaping.positionedGlyphs) { + glyph.x += shiftX; + glyph.y += shiftY; + } +} + +// justify left = 0, right = 1, center = .5 +void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs, + const GlyphPositions& glyphs, + std::size_t start, + std::size_t end, + float justify) { + if (!justify) { + return; + } + + PositionedGlyph& glyph = positionedGlyphs[end]; + auto it = glyphs.find(glyph.glyph); + if (it != glyphs.end()) { + const uint32_t lastAdvance = it->second.metrics.advance; + const float lineIndent = float(glyph.x + lastAdvance) * justify; + + for (std::size_t j = start; j <= end; j++) { + positionedGlyphs[j].x -= lineIndent; + } + } +} + +float determineAverageLineWidth(const std::u16string& logicalInput, + const float spacing, + float maxWidth, + const GlyphPositions& glyphs) { + float totalWidth = 0; + + for (char16_t chr : logicalInput) { + auto it = glyphs.find(chr); + if (it != glyphs.end()) { + totalWidth += it->second.metrics.advance + spacing; + } + } + + int32_t targetLineCount = std::fmax(1, std::ceil(totalWidth / maxWidth)); + return totalWidth / targetLineCount; +} + +float calculateBadness(const float lineWidth, const float targetWidth, const float penalty, const bool isLastBreak) { + const float raggedness = std::pow(lineWidth - targetWidth, 2); + if (isLastBreak) { + // Favor finals lines shorter than average over longer than average + if (lineWidth < targetWidth) { + return raggedness / 2; + } else { + return raggedness * 2; + } + } + if (penalty < 0) { + return raggedness - std::pow(penalty, 2); + } + return raggedness + std::pow(penalty, 2); +} + +float calculatePenalty(char16_t codePoint, char16_t nextCodePoint) { + float penalty = 0; + // Force break on newline + if (codePoint == 0x0a) { + penalty -= 10000; + } + // Penalize open parenthesis at end of line + if (codePoint == 0x28 || codePoint == 0xff08) { + penalty += 50; + } + + // Penalize close parenthesis at beginning of line + if (nextCodePoint == 0x29 || nextCodePoint == 0xff09) { + penalty += 50; + } + + return penalty; +} + +struct PotentialBreak { + PotentialBreak(const std::size_t p_index, const float p_x, const PotentialBreak* p_priorBreak, const float p_badness) + : index(p_index), x(p_x), priorBreak(p_priorBreak), badness(p_badness) + {} + + const std::size_t index; + const float x; + const PotentialBreak* priorBreak; + const float badness; +}; + + +PotentialBreak evaluateBreak(const std::size_t breakIndex, const float breakX, const float targetWidth, const std::list<PotentialBreak>& potentialBreaks, const float penalty, const bool isLastBreak) { + // We could skip evaluating breaks where the line length (breakX - priorBreak.x) > maxWidth + // ...but in fact we allow lines longer than maxWidth (if there's no break points) + // ...and when targetWidth and maxWidth are close, strictly enforcing maxWidth can give + // more lopsided results. + + const PotentialBreak* bestPriorBreak = nullptr; + float bestBreakBadness = calculateBadness(breakX, targetWidth, penalty, isLastBreak); + for (const auto& potentialBreak : potentialBreaks) { + const float lineWidth = breakX - potentialBreak.x; + float breakBadness = + calculateBadness(lineWidth, targetWidth, penalty, isLastBreak) + potentialBreak.badness; + if (breakBadness <= bestBreakBadness) { + bestPriorBreak = &potentialBreak; + bestBreakBadness = breakBadness; + } + } + + return PotentialBreak(breakIndex, breakX, bestPriorBreak, bestBreakBadness); +} + +std::set<std::size_t> leastBadBreaks(const PotentialBreak& lastLineBreak) { + std::set<std::size_t> leastBadBreaks = { lastLineBreak.index }; + const PotentialBreak* priorBreak = lastLineBreak.priorBreak; + while (priorBreak) { + leastBadBreaks.insert(priorBreak->index); + priorBreak = priorBreak->priorBreak; + } + return leastBadBreaks; +} + + +// We determine line breaks based on shaped text in logical order. Working in visual order would be +// more intuitive, but we can't do that because the visual order may be changed by line breaks! +std::set<std::size_t> determineLineBreaks(const std::u16string& logicalInput, + const float spacing, + float maxWidth, + const WritingModeType writingMode, + const GlyphPositions& glyphs) { + if (!maxWidth || writingMode != WritingModeType::Horizontal) { + return {}; + } + + if (logicalInput.empty()) { + return {}; + } + + const float targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphs); + + std::list<PotentialBreak> potentialBreaks; + float currentX = 0; + + for (std::size_t i = 0; i < logicalInput.size(); i++) { + const char16_t codePoint = logicalInput[i]; + auto it = glyphs.find(codePoint); + if (it != glyphs.end() && !boost::algorithm::is_any_of(u" \t\n\v\f\r")(codePoint)) { + currentX += it->second.metrics.advance + spacing; + } + + // Ideographic characters, spaces, and word-breaking punctuation that often appear without + // surrounding spaces. + if ((i < logicalInput.size() - 1) && + (util::i18n::allowsWordBreaking(codePoint) || util::i18n::allowsIdeographicBreaking(codePoint))) { + potentialBreaks.push_back(evaluateBreak(i+1, currentX, targetWidth, potentialBreaks, + calculatePenalty(codePoint, logicalInput[i+1]), + false)); + } + } + + return leastBadBreaks(evaluateBreak(logicalInput.size(), currentX, targetWidth, potentialBreaks, 0, true)); +} + +void shapeLines(Shaping& shaping, + const std::vector<std::u16string>& lines, + const float spacing, + const float lineHeight, + const float horizontalAlign, + const float verticalAlign, + const float justify, + const Point<float>& translate, + const float verticalHeight, + const WritingModeType writingMode, + const GlyphPositions& glyphs) { + + // the y offset *should* be part of the font metadata + const int32_t yOffset = -17; + + float x = 0; + float y = yOffset; + + float maxLineLength = 0; + + for (std::u16string line : lines) { + // Collapse whitespace so it doesn't throw off justification + boost::algorithm::trim_if(line, boost::algorithm::is_any_of(u" \t\n\v\f\r")); + + if (line.empty()) { + y += lineHeight; // Still need a line feed after empty line + continue; + } + + std::size_t lineStartIndex = shaping.positionedGlyphs.size(); + for (char16_t chr : line) { + auto it = glyphs.find(chr); + if (it == glyphs.end()) { + continue; + } + + const Glyph& glyph = it->second; + + if (writingMode == WritingModeType::Horizontal || !util::i18n::hasUprightVerticalOrientation(chr)) { + shaping.positionedGlyphs.emplace_back(chr, x, y, 0); + x += glyph.metrics.advance + spacing; + } else { + shaping.positionedGlyphs.emplace_back(chr, x, 0, -M_PI_2); + x += verticalHeight + spacing; + } + } + + // Only justify if we placed at least one glyph + if (shaping.positionedGlyphs.size() != lineStartIndex) { + float lineLength = x - spacing; // Don't count trailing spacing + maxLineLength = util::max(lineLength, maxLineLength); + + justifyLine(shaping.positionedGlyphs, glyphs, lineStartIndex, + shaping.positionedGlyphs.size() - 1, justify); + } + + x = 0; + y += lineHeight; + } + + align(shaping, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, + lines.size(), translate); + const uint32_t height = lines.size() * lineHeight; + + // Calculate the bounding box + shaping.top += -verticalAlign * height; + shaping.bottom = shaping.top + height; + shaping.left += -horizontalAlign * maxLineLength; + shaping.right = shaping.left + maxLineLength; +} + +const Shaping getShaping(const std::u16string& logicalInput, + const float maxWidth, + const float lineHeight, + const float horizontalAlign, + const float verticalAlign, + const float justify, + const float spacing, + const Point<float>& translate, + const float verticalHeight, + const WritingModeType writingMode, + BiDi& bidi, + const GlyphPositions& glyphs) { + Shaping shaping(translate.x * 24, translate.y * 24, writingMode); + + std::vector<std::u16string> reorderedLines = + bidi.processText(logicalInput, + determineLineBreaks(logicalInput, spacing, maxWidth, writingMode, glyphs)); + + shapeLines(shaping, reorderedLines, spacing, lineHeight, horizontalAlign, verticalAlign, + justify, translate, verticalHeight, writingMode, glyphs); + + return shaping; +} + + } // namespace mbgl diff --git a/src/mbgl/text/shaping.hpp b/src/mbgl/text/shaping.hpp index 1b7b8b2733..fc404e3a97 100644 --- a/src/mbgl/text/shaping.hpp +++ b/src/mbgl/text/shaping.hpp @@ -9,6 +9,7 @@ namespace mbgl { class SpriteAtlasElement; class SymbolFeature; +class BiDi; class PositionedIcon { public: @@ -37,5 +38,18 @@ public: }; PositionedIcon shapeIcon(const SpriteAtlasElement&, const std::array<float, 2>& iconOffset, const float iconRotation); + +const Shaping getShaping(const std::u16string& string, + float maxWidth, + float lineHeight, + float horizontalAlign, + float verticalAlign, + float justify, + float spacing, + const Point<float>& translate, + float verticalHeight, + const WritingModeType, + BiDi& bidi, + const GlyphPositions& glyphs); } // namespace mbgl diff --git a/src/mbgl/tile/geometry_tile.cpp b/src/mbgl/tile/geometry_tile.cpp index 5f1fc5de66..602c43148a 100644 --- a/src/mbgl/tile/geometry_tile.cpp +++ b/src/mbgl/tile/geometry_tile.cpp @@ -32,12 +32,13 @@ GeometryTile::GeometryTile(const OverscaledTileID& id_, worker(parameters.workerScheduler, ActorRef<GeometryTile>(*this, mailbox), id_, - *parameters.style.glyphAtlas, obsolete, - parameters.mode) { + parameters.mode), + glyphAtlas(*parameters.style.glyphAtlas) { } GeometryTile::~GeometryTile() { + glyphAtlas.removeGlyphs(*this); cancel(); } @@ -130,6 +131,14 @@ void GeometryTile::onError(std::exception_ptr err) { availableData = DataAvailability::All; observer->onTileError(*this, err); } + +void GeometryTile::onGlyphsAvailable(GlyphPositionMap glyphPositions) { + worker.invoke(&GeometryTileWorker::onGlyphsAvailable, std::move(glyphPositions)); +} + +void GeometryTile::getGlyphs(GlyphDependencies glyphDependencies) { + glyphAtlas.getGlyphs(*this, std::move(glyphDependencies)); +} Bucket* GeometryTile::getBucket(const Layer& layer) { const auto& buckets = layer.is<SymbolLayer>() ? symbolBuckets : nonSymbolBuckets; diff --git a/src/mbgl/tile/geometry_tile.hpp b/src/mbgl/tile/geometry_tile.hpp index cabe193467..c7b0264c5e 100644 --- a/src/mbgl/tile/geometry_tile.hpp +++ b/src/mbgl/tile/geometry_tile.hpp @@ -2,6 +2,7 @@ #include <mbgl/tile/tile.hpp> #include <mbgl/tile/geometry_tile_worker.hpp> +#include <mbgl/text/glyph_atlas.hpp> #include <mbgl/text/placement_config.hpp> #include <mbgl/util/feature.hpp> #include <mbgl/actor/actor.hpp> @@ -24,7 +25,7 @@ class UpdateParameters; class SourceQueryOptions; } // namespace style -class GeometryTile : public Tile { +class GeometryTile : public Tile, public GlyphRequestor { public: GeometryTile(const OverscaledTileID&, std::string sourceID, @@ -38,6 +39,8 @@ public: void setPlacementConfig(const PlacementConfig&) override; void symbolDependenciesChanged() override; void redoLayout() override; + + void getGlyphs(GlyphDependencies); Bucket* getBucket(const style::Layer&) override; @@ -71,6 +74,8 @@ public: void onPlacement(PlacementResult); void onError(std::exception_ptr); + + virtual void onGlyphsAvailable(GlyphPositionMap) override; protected: const GeometryTileData* getData() { @@ -87,6 +92,8 @@ private: std::shared_ptr<Mailbox> mailbox; Actor<GeometryTileWorker> worker; + GlyphAtlas& glyphAtlas; + uint64_t correlationID = 0; optional<PlacementConfig> requestedConfig; diff --git a/src/mbgl/tile/geometry_tile_worker.cpp b/src/mbgl/tile/geometry_tile_worker.cpp index b1fd7a852e..0d17526cb1 100644 --- a/src/mbgl/tile/geometry_tile_worker.cpp +++ b/src/mbgl/tile/geometry_tile_worker.cpp @@ -2,7 +2,6 @@ #include <mbgl/tile/geometry_tile_data.hpp> #include <mbgl/tile/geometry_tile.hpp> #include <mbgl/text/collision_tile.hpp> -#include <mbgl/text/glyph_atlas.hpp> #include <mbgl/layout/symbol_layout.hpp> #include <mbgl/style/bucket_parameters.hpp> #include <mbgl/style/group_by_layout.hpp> @@ -25,19 +24,16 @@ using namespace style; GeometryTileWorker::GeometryTileWorker(ActorRef<GeometryTileWorker> self_, ActorRef<GeometryTile> parent_, OverscaledTileID id_, - GlyphAtlas& glyphAtlas_, const std::atomic<bool>& obsolete_, const MapMode mode_) : self(std::move(self_)), parent(std::move(parent_)), id(std::move(id_)), - glyphAtlas(glyphAtlas_), obsolete(obsolete_), mode(mode_) { } GeometryTileWorker::~GeometryTileWorker() { - glyphAtlas.removeGlyphs(reinterpret_cast<uintptr_t>(this)); } /* @@ -168,6 +164,11 @@ void GeometryTileWorker::symbolDependenciesChanged() { } } +void GeometryTileWorker::onGlyphsAvailable(GlyphPositionMap glyphs) { + glyphPositions = std::move(glyphs); + symbolDependenciesChanged(); // TODO: This is a clumsy way to join the "glyphs loaded" and "symbol dependencies changed" signals +} + void GeometryTileWorker::coalesced() { try { switch (state) { @@ -215,6 +216,8 @@ void GeometryTileWorker::redoLayout() { std::unordered_map<std::string, std::shared_ptr<Bucket>> buckets; auto featureIndex = std::make_unique<FeatureIndex>(); BucketParameters parameters { id, mode }; + GlyphDependencies glyphDependencies; + glyphPositions.clear(); std::vector<std::vector<const Layer*>> groups = groupByLayout(*layers); for (auto& group : groups) { @@ -242,7 +245,7 @@ void GeometryTileWorker::redoLayout() { if (leader.is<SymbolLayer>()) { symbolLayoutMap.emplace(leader.getID(), - leader.as<SymbolLayer>()->impl->createLayout(parameters, group, *geometryLayer)); + leader.as<SymbolLayer>()->impl->createLayout(parameters, group, *geometryLayer, glyphDependencies)); } else { const Filter& filter = leader.baseImpl->filter; const std::string& sourceLayerID = leader.baseImpl->sourceLayer; @@ -276,6 +279,10 @@ void GeometryTileWorker::redoLayout() { symbolLayouts.push_back(std::move(it->second)); } } + + if (!glyphDependencies.empty()) { + parent.invoke(&GeometryTile::getGlyphs, std::move(glyphDependencies)); + } parent.invoke(&GeometryTile::onLayout, GeometryTile::LayoutResult { std::move(buckets), @@ -313,10 +320,9 @@ void GeometryTileWorker::attemptPlacement() { } if (symbolLayout->state == SymbolLayout::Pending) { - if (symbolLayout->canPrepare(glyphAtlas)) { + if (symbolLayout->canPrepare(glyphPositions)) { symbolLayout->state = SymbolLayout::Prepared; - symbolLayout->prepare(reinterpret_cast<uintptr_t>(this), - glyphAtlas); + symbolLayout->prepare(glyphPositions); } else { canPlace = false; } diff --git a/src/mbgl/tile/geometry_tile_worker.hpp b/src/mbgl/tile/geometry_tile_worker.hpp index 91bf81a697..3a62203b10 100644 --- a/src/mbgl/tile/geometry_tile_worker.hpp +++ b/src/mbgl/tile/geometry_tile_worker.hpp @@ -2,6 +2,7 @@ #include <mbgl/map/mode.hpp> #include <mbgl/tile/tile_id.hpp> +#include <mbgl/text/glyph.hpp> #include <mbgl/text/placement_config.hpp> #include <mbgl/actor/actor_ref.hpp> #include <mbgl/util/optional.hpp> @@ -25,7 +26,6 @@ public: GeometryTileWorker(ActorRef<GeometryTileWorker> self, ActorRef<GeometryTile> parent, OverscaledTileID, - GlyphAtlas&, const std::atomic<bool>&, const MapMode); ~GeometryTileWorker(); @@ -34,6 +34,8 @@ public: void setData(std::unique_ptr<const GeometryTileData>, uint64_t correlationID); void setPlacementConfig(PlacementConfig, uint64_t correlationID); void symbolDependenciesChanged(); + + void onGlyphsAvailable(GlyphPositionMap glyphs); private: void coalesce(); @@ -46,7 +48,6 @@ private: ActorRef<GeometryTile> parent; const OverscaledTileID id; - GlyphAtlas& glyphAtlas; const std::atomic<bool>& obsolete; const MapMode mode; @@ -66,6 +67,7 @@ private: optional<PlacementConfig> placementConfig; std::vector<std::unique_ptr<SymbolLayout>> symbolLayouts; + GlyphPositionMap glyphPositions; }; } // namespace mbgl diff --git a/src/mbgl/util/exclusive.hpp b/src/mbgl/util/exclusive.hpp deleted file mode 100644 index 844588dc90..0000000000 --- a/src/mbgl/util/exclusive.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include <memory> -#include <mutex> - - -namespace mbgl { -namespace util { - -template <class T> -class exclusive { -public: - exclusive(T* val, std::unique_ptr<std::lock_guard<std::mutex>> mtx) : ptr(val), lock(std::move(mtx)) {} - - T* operator->() { return ptr; } - const T* operator->() const { return ptr; } - T* operator*() { return ptr; } - const T* operator*() const { return ptr; } - -private: - T *ptr; - std::unique_ptr<std::lock_guard<std::mutex>> lock; -}; - - -} // end namespace util -} // end namespace mbgl diff --git a/test/text/glyph_atlas.test.cpp b/test/text/glyph_atlas.test.cpp index 3679cabc1b..314773b215 100644 --- a/test/text/glyph_atlas.test.cpp +++ b/test/text/glyph_atlas.test.cpp @@ -11,12 +11,13 @@ using namespace mbgl; -class GlyphAtlasTest { +class GlyphAtlasTest : public GlyphRequestor { public: util::RunLoop loop; StubFileSource fileSource; StubStyleObserver observer; GlyphAtlas glyphAtlas{ { 32, 32 }, fileSource }; + GlyphPositionMap glyphPositions; void run(const std::string& url, const FontStack& fontStack, const GlyphRangeSet& glyphRanges) { // Squelch logging. @@ -24,14 +25,30 @@ public: glyphAtlas.setObserver(&observer); glyphAtlas.setURL(url); + GlyphDependencies glyphDependencies; + for (const auto& range : glyphRanges) { + glyphDependencies[fontStack].insert(range.first); + } + glyphAtlas.getGlyphs(*this, glyphDependencies); glyphAtlas.hasGlyphRanges(fontStack, glyphRanges); loop.run(); } + void addGlyphs(GlyphRequestor& requestor, const GlyphDependencies& glyphDependencies) { + glyphAtlas.addGlyphs(requestor, glyphDependencies); + } + + bool hasGlyphRanges(const FontStack& fontStack, const GlyphRangeSet& ranges) const { + return glyphAtlas.hasGlyphRanges(fontStack, ranges); + } void end() { loop.stop(); } + + virtual void onGlyphsAvailable(GlyphPositionMap positions) { + glyphPositions = std::move(positions); + } }; TEST(GlyphAtlas, LoadingSuccess) { @@ -50,11 +67,11 @@ TEST(GlyphAtlas, LoadingSuccess) { }; test.observer.glyphsLoaded = [&] (const FontStack&, const GlyphRange&) { - if (!test.glyphAtlas.hasGlyphRanges({{"Test Stack"}}, {{0, 255}, {256, 511}})) + if (!test.hasGlyphRanges({{"Test Stack"}}, {{0, 255}, {256, 511}})) return; - auto glyphSet = test.glyphAtlas.getGlyphSet({{"Test Stack"}}); - ASSERT_FALSE(glyphSet->getSDFs().empty()); + auto& glyphSet = test.glyphAtlas.getGlyphSet({{"Test Stack"}}); + ASSERT_FALSE(glyphSet.getSDFs().empty()); test.end(); }; @@ -83,8 +100,8 @@ TEST(GlyphAtlas, LoadingFail) { EXPECT_TRUE(error != nullptr); EXPECT_EQ(util::toString(error), "Failed by the test case"); - ASSERT_TRUE(test.glyphAtlas.getGlyphSet({{"Test Stack"}})->getSDFs().empty()); - ASSERT_FALSE(test.glyphAtlas.hasGlyphRanges({{"Test Stack"}}, {{0, 255}})); + ASSERT_TRUE(test.glyphAtlas.getGlyphSet({{"Test Stack"}}).getSDFs().empty()); + ASSERT_FALSE(test.hasGlyphRanges({{"Test Stack"}}, {{0, 255}})); test.end(); }; @@ -111,8 +128,8 @@ TEST(GlyphAtlas, LoadingCorrupted) { EXPECT_TRUE(error != nullptr); EXPECT_EQ(util::toString(error), "unknown pbf field type exception"); - ASSERT_TRUE(test.glyphAtlas.getGlyphSet({{"Test Stack"}})->getSDFs().empty()); - ASSERT_FALSE(test.glyphAtlas.hasGlyphRanges({{"Test Stack"}}, {{0, 255}})); + ASSERT_TRUE(test.glyphAtlas.getGlyphSet({{"Test Stack"}}).getSDFs().empty()); + ASSERT_FALSE(test.hasGlyphRanges({{"Test Stack"}}, {{0, 255}})); test.end(); }; @@ -145,19 +162,20 @@ TEST(GlyphAtlas, InvalidSDFGlyph) { const FontStack fontStack{ "Mock Font" }; GlyphAtlasTest test; - GlyphPositions positions; - auto glyphSet = test.glyphAtlas.getGlyphSet(fontStack); - glyphSet->insert(66, SDFGlyph{ 66 /* ASCII 'B' */, + auto& glyphSet = test.glyphAtlas.getGlyphSet(fontStack); + glyphSet.insert(66, SDFGlyph{ 66 /* ASCII 'B' */, AlphaImage({7, 7}), /* correct */ { 1 /* width */, 1 /* height */, 0 /* left */, 0 /* top */, 0 /* advance */ } }); - glyphSet->insert(67, SDFGlyph{ 67 /* ASCII 'C' */, + glyphSet.insert(67, SDFGlyph{ 67 /* ASCII 'C' */, AlphaImage({518, 8}), /* correct */ { 512 /* width */, 2 /* height */, 0 /* left */, 0 /* top */, 0 /* advance */ } }); - test.glyphAtlas.addGlyphs(1, std::u16string{u"ABC"}, fontStack, glyphSet, positions); + GlyphDependencies glyphDependencies = {{fontStack, {'A','B','C'}}}; + test.addGlyphs(test, glyphDependencies); + GlyphPositions positions = test.glyphPositions[fontStack]; ASSERT_EQ(2u, positions.size()); diff --git a/test/text/glyph_pbf.test.cpp b/test/text/glyph_pbf.test.cpp index be3ca3359b..48b2d7a07c 100644 --- a/test/text/glyph_pbf.test.cpp +++ b/test/text/glyph_pbf.test.cpp @@ -44,7 +44,7 @@ TEST(GlyphPBF, Parsing) { glyphAtlasObserver.glyphsLoaded = [&](const FontStack&, const GlyphRange&) { loop.stop(); - const auto& sdfs = glyphAtlas.getGlyphSet(fontStack)->getSDFs(); + const auto& sdfs = glyphAtlas.getGlyphSet(fontStack).getSDFs(); // The fake glyphs don't contain a glyph that has the ID 0; it only contains glyphs with // undefined IDs, but the parser should remove them. |