[core] Convert GeometryTileWorker to "one-phase" loading
Modest simplification refactoring (issue #10457). Also, fixes issue #11538, which was caused in part by a hole in the vestigial two-phase loading.
This commit is contained in:
@@ -125,36 +125,17 @@ void GeometryTile::setShowCollisionBoxes(const bool showCollisionBoxes_) {
|
||||
}
|
||||
|
||||
void GeometryTile::onLayout(LayoutResult result, const uint64_t resultCorrelationID) {
|
||||
// Don't mark ourselves loaded or renderable until the first successful placement
|
||||
// TODO: Ideally we'd render this tile without symbols as long as this tile wasn't
|
||||
// replacing a tile at a different zoom that _did_ have symbols.
|
||||
(void)resultCorrelationID;
|
||||
nonSymbolBuckets = std::move(result.nonSymbolBuckets);
|
||||
// It is possible for multiple onLayouts to be called before an onPlacement is called, in which
|
||||
// case we will discard previous pending data
|
||||
pendingFeatureIndex = {{ false, std::move(result.featureIndex) }};
|
||||
pendingData = {{ false, std::move(result.tileData) }};
|
||||
observer->onTileChanged(*this);
|
||||
}
|
||||
|
||||
void GeometryTile::onPlacement(PlacementResult result, const uint64_t resultCorrelationID) {
|
||||
loaded = true;
|
||||
renderable = true;
|
||||
if (resultCorrelationID == correlationID) {
|
||||
pending = false;
|
||||
}
|
||||
|
||||
nonSymbolBuckets = std::move(result.nonSymbolBuckets);
|
||||
symbolBuckets = std::move(result.symbolBuckets);
|
||||
// When symbolBuckets arrive, mark pendingData/FeatureIndex as "ready for commit" at the
|
||||
// time of the next global placement. We are counting on these symbolBuckets being
|
||||
// in sync with the data/index in the last `onLayout` call, even though the correlation IDs
|
||||
// may not be the same (e.g. "setShowCollisionBoxes" could bump the correlation ID while
|
||||
// waiting for glyph dependencies)
|
||||
if (pendingData) {
|
||||
pendingData->first = true;
|
||||
}
|
||||
if (pendingFeatureIndex) {
|
||||
pendingFeatureIndex->first = true;
|
||||
}
|
||||
|
||||
dataPendingCommit = {{ std::move(result.tileData), std::move(result.featureIndex) }};
|
||||
|
||||
if (result.glyphAtlasImage) {
|
||||
glyphAtlasImage = std::move(*result.glyphAtlasImage);
|
||||
}
|
||||
@@ -227,17 +208,12 @@ Bucket* GeometryTile::getBucket(const Layer::Impl& layer) const {
|
||||
}
|
||||
|
||||
void GeometryTile::commitFeatureIndex() {
|
||||
// We commit our pending FeatureIndex and GeometryTileData when:
|
||||
// 1) An `onPlacement` result has delivered us updated symbolBuckets since we received the pending data
|
||||
// 2) A global placement has run, synchronizing the global CollisionIndex with the latest
|
||||
// symbolBuckets (and thus with the latest FeatureIndex/GeometryTileData)
|
||||
if (pendingFeatureIndex && pendingFeatureIndex->first) {
|
||||
featureIndex = std::move(pendingFeatureIndex->second);
|
||||
pendingFeatureIndex = nullopt;
|
||||
}
|
||||
if (pendingData && pendingData->first) {
|
||||
data = std::move(pendingData->second);
|
||||
pendingData = nullopt;
|
||||
// We commit our pending FeatureIndex and GeometryTileData when a global placement has run,
|
||||
// synchronizing the global CollisionIndex with the latest symbolBuckets/FeatureIndex/GeometryTileData
|
||||
if (dataPendingCommit) {
|
||||
data = std::move(dataPendingCommit->first);
|
||||
featureIndex = std::move(dataPendingCommit->second);
|
||||
dataPendingCommit = nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,30 +68,24 @@ public:
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> nonSymbolBuckets;
|
||||
std::unique_ptr<FeatureIndex> featureIndex;
|
||||
std::unique_ptr<GeometryTileData> tileData;
|
||||
|
||||
LayoutResult(std::unordered_map<std::string, std::shared_ptr<Bucket>> nonSymbolBuckets_,
|
||||
std::unique_ptr<FeatureIndex> featureIndex_,
|
||||
std::unique_ptr<GeometryTileData> tileData_)
|
||||
: nonSymbolBuckets(std::move(nonSymbolBuckets_)),
|
||||
featureIndex(std::move(featureIndex_)),
|
||||
tileData(std::move(tileData_)) {}
|
||||
};
|
||||
void onLayout(LayoutResult, uint64_t correlationID);
|
||||
|
||||
class PlacementResult {
|
||||
public:
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> symbolBuckets;
|
||||
optional<AlphaImage> glyphAtlasImage;
|
||||
optional<PremultipliedImage> iconAtlasImage;
|
||||
|
||||
PlacementResult(std::unordered_map<std::string, std::shared_ptr<Bucket>> symbolBuckets_,
|
||||
optional<AlphaImage> glyphAtlasImage_,
|
||||
optional<PremultipliedImage> iconAtlasImage_)
|
||||
: symbolBuckets(std::move(symbolBuckets_)),
|
||||
LayoutResult(std::unordered_map<std::string, std::shared_ptr<Bucket>> nonSymbolBuckets_,
|
||||
std::unique_ptr<FeatureIndex> featureIndex_,
|
||||
std::unique_ptr<GeometryTileData> tileData_,
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> symbolBuckets_,
|
||||
optional<AlphaImage> glyphAtlasImage_,
|
||||
optional<PremultipliedImage> iconAtlasImage_)
|
||||
: nonSymbolBuckets(std::move(nonSymbolBuckets_)),
|
||||
featureIndex(std::move(featureIndex_)),
|
||||
tileData(std::move(tileData_)),
|
||||
symbolBuckets(std::move(symbolBuckets_)),
|
||||
glyphAtlasImage(std::move(glyphAtlasImage_)),
|
||||
iconAtlasImage(std::move(iconAtlasImage_)) {}
|
||||
};
|
||||
void onPlacement(PlacementResult, uint64_t correlationID);
|
||||
void onLayout(LayoutResult, uint64_t correlationID);
|
||||
|
||||
void onError(std::exception_ptr, uint64_t correlationID);
|
||||
|
||||
@@ -124,16 +118,15 @@ private:
|
||||
uint64_t correlationID = 0;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> nonSymbolBuckets;
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> symbolBuckets;
|
||||
|
||||
optional<std::pair<std::unique_ptr<const GeometryTileData>, std::unique_ptr<FeatureIndex>>> dataPendingCommit;
|
||||
std::unique_ptr<FeatureIndex> featureIndex;
|
||||
optional<std::pair<bool,std::unique_ptr<FeatureIndex>>> pendingFeatureIndex;
|
||||
std::unique_ptr<const GeometryTileData> data;
|
||||
optional<std::pair<bool, std::unique_ptr<const GeometryTileData>>> pendingData;
|
||||
|
||||
optional<AlphaImage> glyphAtlasImage;
|
||||
optional<PremultipliedImage> iconAtlasImage;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> symbolBuckets;
|
||||
|
||||
const MapMode mode;
|
||||
|
||||
bool showCollisionBoxes;
|
||||
|
||||
@@ -41,48 +41,73 @@ GeometryTileWorker::GeometryTileWorker(ActorRef<GeometryTileWorker> self_,
|
||||
GeometryTileWorker::~GeometryTileWorker() = default;
|
||||
|
||||
/*
|
||||
NOTE: The comments below are technically correct, but currently
|
||||
conceptually misleading. The change to foreground label placement
|
||||
means that:
|
||||
(1) "placement" here is a misnomer: the remaining role of
|
||||
"attemptPlacement" is symbol buffer generation
|
||||
(2) Once a tile has completed layout, we will only run
|
||||
"attemptPlacement" once
|
||||
(3) Tiles won't be rendered until "attemptPlacement" has run once
|
||||
|
||||
TODO: Simplify GeometryTileWorker to fit its new role
|
||||
https://github.com/mapbox/mapbox-gl-native/issues/10457
|
||||
|
||||
GeometryTileWorker is a state machine. This is its transition diagram.
|
||||
States are indicated by [state], lines are transitions triggered by
|
||||
messages, (parentheses) are actions taken on transition.
|
||||
|
||||
[idle] <----------------------------.
|
||||
| |
|
||||
set{Data,Layers,Placement}, symbolDependenciesChanged |
|
||||
| |
|
||||
(do layout/placement; self-send "coalesced") |
|
||||
v |
|
||||
[coalescing] --- coalesced ------------.
|
||||
| |
|
||||
.-----------------. .---------------.
|
||||
[Idle] <-------------------------.
|
||||
| |
|
||||
set{Data,Layers}, symbolDependenciesChanged, |
|
||||
setShowCollisionBoxes |
|
||||
| |
|
||||
(do parse and/or symbol layout; self-send "coalesced") |
|
||||
v |
|
||||
[Coalescing] --- coalesced ---------.
|
||||
| |
|
||||
.-----------. .---------------------.
|
||||
| |
|
||||
.--- set{Data,Layers} setPlacement -----.
|
||||
| | | |
|
||||
| v v |
|
||||
.-- [need layout] <-- set{Data,Layers} -- [need placement] --.
|
||||
.--- set{Data,Layers} setShowCollisionBoxes,
|
||||
| | symbolDependenciesChanged --.
|
||||
| | | |
|
||||
| v v |
|
||||
.-- [NeedsParse] <-- set{Data,Layers} -- [NeedsSymbolLayout] ---.
|
||||
| |
|
||||
coalesced coalesced
|
||||
| |
|
||||
v v
|
||||
(do layout or placement; self-send "coalesced"; goto [coalescing])
|
||||
(do parse or symbol layout; self-send "coalesced"; goto [coalescing])
|
||||
|
||||
The idea is that in the [idle] state, layout or placement happens immediately
|
||||
in response to a "set" message. During this processing, multiple "set" messages
|
||||
might get queued in the mailbox. At the end of processing, we self-send "coalesced",
|
||||
read all the queued messages until we get to "coalesced", and then redo either
|
||||
layout or placement if there were one or more "set"s (with layout taking priority,
|
||||
since it will trigger placement when complete), or return to the [idle] state if not.
|
||||
The idea is that in the [idle] state, parsing happens immediately in response to
|
||||
a "set" message, and symbol layout happens once all symbol dependencies are met.
|
||||
During this processing, multiple "set" messages might get queued in the mailbox.
|
||||
At the end of processing, we self-send "coalesced", read all the queued messages
|
||||
until we get to "coalesced", and then re-parse if there were one or more "set"s or
|
||||
return to the [idle] state if not.
|
||||
|
||||
One important goal of the design is to prevent starvation. Under heavy load new
|
||||
requests for tiles should not prevent in progress request from completing.
|
||||
It is nevertheless possible to restart an in-progress request:
|
||||
|
||||
- [Idle] setData -> parse()
|
||||
sends getGlyphs, hasPendingSymbolDependencies() is true
|
||||
enters [Coalescing], sends coalesced
|
||||
- [Coalescing] coalesced -> [Idle]
|
||||
- [Idle] setData -> new parse(), interrupts old parse()
|
||||
sends getGlyphs, hasPendingSymbolDependencies() is true
|
||||
enters [Coalescing], sends coalesced
|
||||
- [Coalescing] onGlyphsAvailable -> [NeedsSymbolLayout]
|
||||
hasPendingSymbolDependencies() may or may not be true
|
||||
- [NeedsSymbolLayout] coalesced -> performSymbolLayout()
|
||||
Generates result depending on whether dependencies are met
|
||||
-> [Idle]
|
||||
|
||||
In this situation, we are counting on the idea that even with rapid changes to
|
||||
the tile's data, the set of glyphs/images it requires will not keep growing without
|
||||
limit.
|
||||
|
||||
Although parsing (which populates all non-symbol buckets and requests dependencies
|
||||
for symbol buckets) is internally separate from symbol layout, we only return
|
||||
results to the foreground when we have completed both steps. Because we _move_
|
||||
the result buckets to the foreground, it is necessary to re-generate all buckets from
|
||||
scratch for `setShowCollisionBoxes`, even though it only affects symbol layers.
|
||||
|
||||
The GL JS equivalent (in worker_tile.js and vector_tile_worker_source.js)
|
||||
is somewhat simpler because it relies on getGlyphs/getImages calls that transfer
|
||||
an entire set of glyphs/images on every tile load, while the native logic
|
||||
maintains a local state that can be incrementally updated. Because each tile load
|
||||
call becomes self-contained, the equivalent of the coalescing logic is handled by
|
||||
'reloadTile' queueing a single extra 'reloadTile' callback to run after the next
|
||||
completed parse.
|
||||
*/
|
||||
|
||||
void GeometryTileWorker::setData(std::unique_ptr<const GeometryTileData> data_, uint64_t correlationID_) {
|
||||
@@ -92,14 +117,14 @@ void GeometryTileWorker::setData(std::unique_ptr<const GeometryTileData> data_,
|
||||
|
||||
switch (state) {
|
||||
case Idle:
|
||||
redoLayout();
|
||||
parse();
|
||||
coalesce();
|
||||
break;
|
||||
|
||||
case Coalescing:
|
||||
case NeedLayout:
|
||||
case NeedPlacement:
|
||||
state = NeedLayout;
|
||||
case NeedsParse:
|
||||
case NeedsSymbolLayout:
|
||||
state = NeedsParse;
|
||||
break;
|
||||
}
|
||||
} catch (...) {
|
||||
@@ -114,16 +139,16 @@ void GeometryTileWorker::setLayers(std::vector<Immutable<Layer::Impl>> layers_,
|
||||
|
||||
switch (state) {
|
||||
case Idle:
|
||||
redoLayout();
|
||||
parse();
|
||||
coalesce();
|
||||
break;
|
||||
|
||||
case Coalescing:
|
||||
case NeedPlacement:
|
||||
state = NeedLayout;
|
||||
case NeedsSymbolLayout:
|
||||
state = NeedsParse;
|
||||
break;
|
||||
|
||||
case NeedLayout:
|
||||
case NeedsParse:
|
||||
break;
|
||||
}
|
||||
} catch (...) {
|
||||
@@ -138,16 +163,20 @@ void GeometryTileWorker::setShowCollisionBoxes(bool showCollisionBoxes_, uint64_
|
||||
|
||||
switch (state) {
|
||||
case Idle:
|
||||
attemptPlacement();
|
||||
coalesce();
|
||||
if (!hasPendingParseResult()) {
|
||||
// Trigger parse if nothing is in flight, otherwise symbol layout will automatically
|
||||
// pick up the change
|
||||
parse();
|
||||
coalesce();
|
||||
}
|
||||
break;
|
||||
|
||||
case Coalescing:
|
||||
state = NeedPlacement;
|
||||
state = NeedsSymbolLayout;
|
||||
break;
|
||||
|
||||
case NeedPlacement:
|
||||
case NeedLayout:
|
||||
case NeedsSymbolLayout:
|
||||
case NeedsParse:
|
||||
break;
|
||||
}
|
||||
} catch (...) {
|
||||
@@ -160,19 +189,23 @@ void GeometryTileWorker::symbolDependenciesChanged() {
|
||||
switch (state) {
|
||||
case Idle:
|
||||
if (symbolLayoutsNeedPreparation) {
|
||||
attemptPlacement();
|
||||
// symbolLayoutsNeedPreparation can only be set true by parsing
|
||||
// and the parse result can only be cleared by performSymbolLayout
|
||||
// which also clears symbolLayoutsNeedPreparation
|
||||
assert(hasPendingParseResult());
|
||||
performSymbolLayout();
|
||||
coalesce();
|
||||
}
|
||||
break;
|
||||
|
||||
case Coalescing:
|
||||
if (symbolLayoutsNeedPreparation) {
|
||||
state = NeedPlacement;
|
||||
state = NeedsSymbolLayout;
|
||||
}
|
||||
break;
|
||||
|
||||
case NeedPlacement:
|
||||
case NeedLayout:
|
||||
case NeedsSymbolLayout:
|
||||
case NeedsParse:
|
||||
break;
|
||||
}
|
||||
} catch (...) {
|
||||
@@ -191,13 +224,16 @@ void GeometryTileWorker::coalesced() {
|
||||
state = Idle;
|
||||
break;
|
||||
|
||||
case NeedLayout:
|
||||
redoLayout();
|
||||
case NeedsParse:
|
||||
parse();
|
||||
coalesce();
|
||||
break;
|
||||
|
||||
case NeedPlacement:
|
||||
attemptPlacement();
|
||||
case NeedsSymbolLayout:
|
||||
// We may have entered NeedsSymbolLayout while coalescing
|
||||
// after a performSymbolLayout. In that case, we need to
|
||||
// start over with parsing in order to do another layout.
|
||||
hasPendingParseResult() ? performSymbolLayout() : parse();
|
||||
coalesce();
|
||||
break;
|
||||
}
|
||||
@@ -279,7 +315,7 @@ static std::vector<std::unique_ptr<RenderLayer>> toRenderLayers(const std::vecto
|
||||
return renderLayers;
|
||||
}
|
||||
|
||||
void GeometryTileWorker::redoLayout() {
|
||||
void GeometryTileWorker::parse() {
|
||||
if (!data || !layers) {
|
||||
return;
|
||||
}
|
||||
@@ -292,8 +328,8 @@ void GeometryTileWorker::redoLayout() {
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::unique_ptr<SymbolLayout>> symbolLayoutMap;
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> buckets;
|
||||
auto featureIndex = std::make_unique<FeatureIndex>();
|
||||
nonSymbolBuckets.clear();
|
||||
featureIndex = std::make_unique<FeatureIndex>();
|
||||
BucketParameters parameters { id, mode, pixelRatio };
|
||||
|
||||
GlyphDependencies glyphDependencies;
|
||||
@@ -352,7 +388,7 @@ void GeometryTileWorker::redoLayout() {
|
||||
}
|
||||
|
||||
for (const auto& layer : group) {
|
||||
buckets.emplace(layer->getID(), bucket);
|
||||
nonSymbolBuckets.emplace(layer->getID(), bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,13 +404,7 @@ void GeometryTileWorker::redoLayout() {
|
||||
requestNewGlyphs(glyphDependencies);
|
||||
requestNewImages(imageDependencies);
|
||||
|
||||
parent.invoke(&GeometryTile::onLayout, GeometryTile::LayoutResult {
|
||||
std::move(buckets),
|
||||
std::move(featureIndex),
|
||||
*data ? (*data)->clone() : nullptr,
|
||||
}, correlationID);
|
||||
|
||||
attemptPlacement();
|
||||
performSymbolLayout();
|
||||
}
|
||||
|
||||
bool GeometryTileWorker::hasPendingSymbolDependencies() const {
|
||||
@@ -385,9 +415,13 @@ bool GeometryTileWorker::hasPendingSymbolDependencies() const {
|
||||
}
|
||||
return !pendingImageDependencies.empty();
|
||||
}
|
||||
|
||||
bool GeometryTileWorker::hasPendingParseResult() const {
|
||||
return bool(featureIndex);
|
||||
}
|
||||
|
||||
void GeometryTileWorker::attemptPlacement() {
|
||||
if (!data || !layers || hasPendingSymbolDependencies()) {
|
||||
void GeometryTileWorker::performSymbolLayout() {
|
||||
if (!data || !layers || !hasPendingParseResult() || hasPendingSymbolDependencies()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -435,11 +469,14 @@ void GeometryTileWorker::attemptPlacement() {
|
||||
}
|
||||
|
||||
firstLoad = false;
|
||||
|
||||
parent.invoke(&GeometryTile::onPlacement, GeometryTile::PlacementResult {
|
||||
|
||||
parent.invoke(&GeometryTile::onLayout, GeometryTile::LayoutResult {
|
||||
std::move(nonSymbolBuckets),
|
||||
std::move(featureIndex),
|
||||
*data ? (*data)->clone() : nullptr,
|
||||
std::move(buckets),
|
||||
std::move(glyphAtlasImage),
|
||||
std::move(iconAtlasImage),
|
||||
std::move(iconAtlasImage)
|
||||
}, correlationID);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <mbgl/util/optional.hpp>
|
||||
#include <mbgl/util/immutable.hpp>
|
||||
#include <mbgl/style/layer_impl.hpp>
|
||||
#include <mbgl/geometry/feature_index.hpp>
|
||||
#include <mbgl/renderer/bucket.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
@@ -43,8 +45,8 @@ public:
|
||||
|
||||
private:
|
||||
void coalesced();
|
||||
void redoLayout();
|
||||
void attemptPlacement();
|
||||
void parse();
|
||||
void performSymbolLayout();
|
||||
|
||||
void coalesce();
|
||||
|
||||
@@ -53,6 +55,7 @@ private:
|
||||
|
||||
void symbolDependenciesChanged();
|
||||
bool hasPendingSymbolDependencies() const;
|
||||
bool hasPendingParseResult() const;
|
||||
|
||||
ActorRef<GeometryTileWorker> self;
|
||||
ActorRef<GeometryTile> parent;
|
||||
@@ -62,12 +65,15 @@ private:
|
||||
const std::atomic<bool>& obsolete;
|
||||
const MapMode mode;
|
||||
const float pixelRatio;
|
||||
|
||||
std::unique_ptr<FeatureIndex> featureIndex;
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>> nonSymbolBuckets;
|
||||
|
||||
enum State {
|
||||
Idle,
|
||||
Coalescing,
|
||||
NeedLayout,
|
||||
NeedPlacement
|
||||
NeedsParse,
|
||||
NeedsSymbolLayout
|
||||
};
|
||||
|
||||
State state = Idle;
|
||||
|
||||
@@ -65,39 +65,6 @@ TEST(VectorTile, onError) {
|
||||
EXPECT_TRUE(tile.isComplete());
|
||||
}
|
||||
|
||||
TEST(VectorTile, Issue7615) {
|
||||
VectorTileTest test;
|
||||
VectorTile tile(OverscaledTileID(0, 0, 0), "source", test.tileParameters, test.tileset);
|
||||
|
||||
style::SymbolLayer symbolLayer("symbol", "source");
|
||||
std::vector<SymbolInstance> symbolInstances;
|
||||
auto symbolBucket = std::make_shared<SymbolBucket>(
|
||||
style::SymbolLayoutProperties::PossiblyEvaluated(),
|
||||
std::map<
|
||||
std::string,
|
||||
std::pair<style::IconPaintProperties::PossiblyEvaluated, style::TextPaintProperties::PossiblyEvaluated>>(),
|
||||
16.0f, 1.0f, 0.0f, false, false, false, std::move(symbolInstances));
|
||||
|
||||
// Simulate placement of a symbol layer.
|
||||
tile.onPlacement(GeometryTile::PlacementResult {
|
||||
{{
|
||||
symbolLayer.getID(),
|
||||
symbolBucket
|
||||
}},
|
||||
{},
|
||||
{},
|
||||
}, 0);
|
||||
|
||||
// Subsequent onLayout should not cause the existing symbol bucket to be discarded.
|
||||
tile.onLayout(GeometryTile::LayoutResult {
|
||||
std::unordered_map<std::string, std::shared_ptr<Bucket>>(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
}, 0);
|
||||
|
||||
EXPECT_EQ(symbolBucket.get(), tile.getBucket(*symbolLayer.baseImpl));
|
||||
}
|
||||
|
||||
TEST(VectorTile, Issue8542) {
|
||||
VectorTileTest test;
|
||||
VectorTile tile(OverscaledTileID(0, 0, 0), "source", test.tileParameters, test.tileset);
|
||||
|
||||
Reference in New Issue
Block a user