Implement distance-based tile LOD (#4057)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
Nathan Olson
2026-02-21 03:38:10 -08:00
committed by GitHub
parent 8e0dc86956
commit 7fe58506c4
45 changed files with 650 additions and 86 deletions

View File

@@ -25,8 +25,9 @@ static void TileCoverPitchedViewport(benchmark::State& state) {
transform.jumpTo(CameraOptions().withCenter(LatLng{0.1, -0.1}).withZoom(8.0).withBearing(5.0).withPitch(40.0));
std::size_t length = 0;
const Range<uint8_t> zoomRange(0, 14);
while (state.KeepRunning()) {
auto tiles = util::tileCover({transform.getState()}, 8);
auto tiles = util::tileCover({transform.getState()}, 8, zoomRange);
length += tiles.size();
}
benchmark::DoNotOptimize(length);

View File

@@ -69,6 +69,17 @@ Now download the `actual.png` in `metrics/integration/render-tests/background-co
Of we don't want to commit this change. But know you can add and debug (Android) render tests.
### Adding Alternate Expected Images
You can add alternate expected images by coping the `actual.png` to a file that starts with `expected`. You can use the following command:
```
render_test_name=tile-lod/zoom-shift
adb exec-out run-as org.maplibre.render_test_runner cat files/metrics/integration/render-tests/$render_test_name/actual.png > metrics/integration/render-tests/$render_test_name/expected-android.png
```
Which will pull `tile-lod/zoom-shift/actual.png` and save it as `tile-lod/zoom-shift/expected-android.png`.
## Instrumentation Tests
To run the instrumentation tests, choose the "Instrumentation Tests" run configuration.

View File

@@ -186,6 +186,14 @@ public:
/// A negative values typically improves performance but reduces quality.
/// For instance, a value of -1 reduces the zoom level by 1 and this
/// reduces the number of tiles by a factor of 4 for the same camera view.
/// - `TileLodMode` selects the tile cover algorithm. The possible algorithms
/// are "default" and "distance". When "distance" is selected, the tile LOD is
/// based on the distance from camera to tile. This algorithm causes tiles
/// closer to the camera to be rendered at higher LOD, in contrast with the
/// default algorithm, which renders the highest LOD at the center of the
/// screen. The distance based algorithm observes `TileLodScale` and
/// `TileLodPitchThreshold` and ignores `TileLodMinRadius` and
/// `TileLodZoomShift`.
void setTileLodMinRadius(double radius);
double getTileLodMinRadius() const;
void setTileLodScale(double scale);
@@ -194,6 +202,8 @@ public:
double getTileLodPitchThreshold() const;
void setTileLodZoomShift(double shift);
double getTileLodZoomShift() const;
void setTileLodMode(TileLodMode mode);
TileLodMode getTileLodMode() const;
ClientOptions getClientOptions() const;

View File

@@ -35,6 +35,11 @@ enum class ViewportMode : EnumType {
FlippedY,
};
enum class TileLodMode : uint8_t {
Default, ///< Default TileLOD algorithm
Distance ///< Distance-based TileLOD algorithm
};
enum class MapDebugOptions : EnumType {
NoDebug = 0,
TileBorders = 1 << 1,

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,38 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 72,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,40 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodMode", "distance"],
["setTileLodPitchThreshold", 62],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 61,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,40 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodMode", "distance"],
["setTileLodScale", 3],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 72,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodMode", "distance"],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 72,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodMinRadius", 1],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 72,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodPitchThreshold", 62],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 61,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodScale", 3],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 72,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,39 @@
{
"version": 8,
"metadata": {
"test": {
"maxPitch": 85,
"operations": [
["setTileLodZoomShift", 2],
["wait"]
]
}
},
"center": [
13.418056,
52.499167
],
"bearing": 30,
"pitch": 72,
"zoom": 16,
"sources": {
"satellite": {
"type": "raster",
"tiles": [
"https://demotiles.maplibre.org/debug-tiles/number/{z}.png"
],
"maxzoom": 22,
"tileSize": 256
}
},
"layers": [
{
"id": "raster",
"type": "raster",
"source": "satellite",
"paint": {
"raster-fade-duration": 0
}
}
]
}

View File

@@ -100,60 +100,71 @@ public:
namespace {
enum class TileLodMode {
Default, // Default Tile LOD parameters
NoLod, // Disable LOD
Reduced, // Reduce LOD away from camera
Aggressive, // Aggressively reduce LOD away from camera at the detriment of quality
enum class TileLodProfile {
Default, // Default Tile LOD parameters
NoLod, // Disable LOD
Reduced, // Reduce LOD away from camera
Aggressive, // Aggressively reduce LOD away from camera at the detriment of quality
DistanceBased, // Use distance-based algorithm
};
constexpr TileLodMode nextTileLodMode(TileLodMode current) {
constexpr TileLodProfile nextTileLodProfile(TileLodProfile current) {
switch (current) {
case TileLodMode::Default:
return TileLodMode::NoLod;
case TileLodMode::NoLod:
return TileLodMode::Reduced;
case TileLodMode::Reduced:
return TileLodMode::Aggressive;
case TileLodMode::Aggressive:
return TileLodMode::Default;
case TileLodProfile::Default:
return TileLodProfile::NoLod;
case TileLodProfile::NoLod:
return TileLodProfile::Reduced;
case TileLodProfile::Reduced:
return TileLodProfile::Aggressive;
case TileLodProfile::Aggressive:
return TileLodProfile::DistanceBased;
case TileLodProfile::DistanceBased:
return TileLodProfile::Default;
default:
return TileLodMode::Default;
return TileLodProfile::Default;
}
}
void cycleTileLodMode(mbgl::Map &map) {
// TileLodMode::Default parameters
void cycleTileLodProfile(mbgl::Map &map) {
// TileLodProfile::Default parameters
static const auto defaultRadius = map.getTileLodMinRadius();
static const auto defaultScale = map.getTileLodScale();
static const auto defaultTilePitchThreshold = map.getTileLodPitchThreshold();
static const auto defaultTileLodMode = map.getTileLodMode();
static TileLodMode mode = TileLodMode::Default;
mode = nextTileLodMode(mode);
static TileLodProfile profile = TileLodProfile::Default;
profile = nextTileLodProfile(profile);
switch (mode) {
case TileLodMode::Default:
switch (profile) {
case TileLodProfile::Default:
map.setTileLodMinRadius(defaultRadius);
map.setTileLodScale(defaultScale);
map.setTileLodPitchThreshold(defaultTilePitchThreshold);
mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: default");
map.setTileLodMode(defaultTileLodMode);
mbgl::Log::Info(mbgl::Event::General, "Tile LOD profile: default");
break;
case TileLodMode::NoLod:
case TileLodProfile::NoLod:
// When LOD is off we set a maximum PitchThreshold
map.setTileLodPitchThreshold(std::numbers::pi);
mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: disabled");
mbgl::Log::Info(mbgl::Event::General, "Tile LOD profile: disabled");
break;
case TileLodMode::Reduced:
case TileLodProfile::Reduced:
map.setTileLodMinRadius(2);
map.setTileLodScale(1.5);
map.setTileLodPitchThreshold(std::numbers::pi / 4);
mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: reduced");
mbgl::Log::Info(mbgl::Event::General, "Tile LOD profile: reduced");
break;
case TileLodMode::Aggressive:
case TileLodProfile::Aggressive:
map.setTileLodMinRadius(1);
map.setTileLodScale(2);
map.setTileLodPitchThreshold(0);
mbgl::Log::Info(mbgl::Event::General, "Tile LOD mode: aggressive");
mbgl::Log::Info(mbgl::Event::General, "Tile LOD profile: aggressive");
break;
case TileLodProfile::DistanceBased:
map.setTileLodScale(1);
map.setTileLodPitchThreshold(0);
map.setTileLodMode(mbgl::TileLodMode::Distance);
mbgl::Log::Info(mbgl::Event::General, "Tile LOD profile: distance-based");
break;
}
map.triggerRepaint();
@@ -651,7 +662,7 @@ void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action,
view->invalidate();
} break;
case GLFW_KEY_F6: {
cycleTileLodMode(*view->map);
cycleTileLodProfile(*view->map);
} break;
case GLFW_KEY_F7: {
tileLodZoomShift(*view->map, false);

View File

@@ -25,6 +25,7 @@
#include <mbgl/style/style.hpp>
#include <mbgl/util/compression.hpp>
#include <mbgl/util/io.hpp>
#include <mbgl/util/enum.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/rapidjson.hpp>
#include <mbgl/util/run_loop.hpp>
@@ -612,6 +613,10 @@ TestMetadata parseTestMetadata(const TestPaths& paths) {
return metadata;
}
namespace mbgl {
MBGL_DEFINE_ENUM(TileLodMode, {{TileLodMode::Default, "default"}, {TileLodMode::Distance, "distance"}});
}
namespace TestOperationNames {
const std::string waitOp("wait");
const std::string sleepOp("sleep");
@@ -648,6 +653,11 @@ const std::string panGestureOp("panGesture");
const std::string gfxProbeOp("probeGFX");
const std::string gfxProbeStartOp("probeGFXStart");
const std::string gfxProbeEndOp("probeGFXEnd");
const std::string setTileLodMinRadiusOp("setTileLodMinRadius");
const std::string setTileLodScaleOp("setTileLodScale");
const std::string setTileLodPitchThresholdOp("setTileLodPitchThreshold");
const std::string setTileLodZoomShiftOp("setTileLodZoomShift");
const std::string setTileLodModeOp("setTileLodMode");
} // namespace TestOperationNames
using namespace TestOperationNames;
@@ -1321,6 +1331,53 @@ TestOperations parseTestOperations(TestMetadata& metadata) {
ctx.getMetadata().metrics.gfx.insert({mark, metricProbe});
return true;
});
} else if (operationArray[0].GetString() == setTileLodMinRadiusOp) {
// setTileLodMinRadius
assert(operationArray.Size() == 2u);
assert(operationArray[1].IsNumber());
double minRadius = operationArray[1].GetDouble();
result.emplace_back([minRadius](TestContext& ctx) {
ctx.getMap().setTileLodMinRadius(minRadius);
return true;
});
} else if (operationArray[0].GetString() == setTileLodScaleOp) {
// setTileLodScale
assert(operationArray.Size() == 2u);
assert(operationArray[1].IsNumber());
double scale = operationArray[1].GetDouble();
result.emplace_back([scale](TestContext& ctx) {
ctx.getMap().setTileLodScale(scale);
return true;
});
} else if (operationArray[0].GetString() == setTileLodPitchThresholdOp) {
// setTileLodPitchThreshold
assert(operationArray.Size() == 2u);
assert(operationArray[1].IsNumber());
double pitchThreshold = operationArray[1].GetDouble();
result.emplace_back([pitchThreshold](TestContext& ctx) {
ctx.getMap().setTileLodPitchThreshold(pitchThreshold);
return true;
});
} else if (operationArray[0].GetString() == setTileLodZoomShiftOp) {
// setTileLodZoomShift
assert(operationArray.Size() == 2u);
assert(operationArray[1].IsNumber());
double zoomShift = operationArray[1].GetDouble();
result.emplace_back([zoomShift](TestContext& ctx) {
ctx.getMap().setTileLodZoomShift(zoomShift);
return true;
});
} else if (operationArray[0].GetString() == setTileLodModeOp) {
// setTileLodMode
assert(operationArray.Size() == 2u);
assert(operationArray[1].IsString());
mbgl::TileLodMode mode = mbgl::Enum<mbgl::TileLodMode>::toEnum(operationArray[1].GetString()).value();
result.emplace_back([mode](TestContext& ctx) {
ctx.getMap().setTileLodMode(mode);
return true;
});
} else {
metadata.errorMessage = std::string("Unsupported operation: ") + operationArray[0].GetString();
return {};

View File

@@ -67,4 +67,9 @@ extern const std::string panGestureOp;
extern const std::string gfxProbeOp;
extern const std::string gfxProbeStartOp;
extern const std::string gfxProbeEndOp;
extern const std::string setTileLodMinRadiusOp;
extern const std::string setTileLodScaleOp;
extern const std::string setTileLodPitchThresholdOp;
extern const std::string setTileLodZoomShiftOp;
extern const std::string setTileLodModeOp;
} // namespace TestOperationNames

View File

@@ -588,6 +588,14 @@ double Map::getTileLodZoomShift() const {
return impl->tileLodZoomShift;
}
void Map::setTileLodMode(TileLodMode mode) {
impl->tileLodMode = mode;
}
TileLodMode Map::getTileLodMode() const {
return impl->tileLodMode;
}
ClientOptions Map::getClientOptions() const {
return impl->fileSource ? impl->fileSource->getClientOptions() : ClientOptions();
}

View File

@@ -135,7 +135,8 @@ void Map::Impl::onUpdate() {
.tileLodMinRadius = tileLodMinRadius,
.tileLodScale = tileLodScale,
.tileLodPitchThreshold = tileLodPitchThreshold,
.tileLodZoomShift = tileLodZoomShift};
.tileLodZoomShift = tileLodZoomShift,
.tileLodMode = tileLodMode};
rendererFrontend.update(std::make_shared<UpdateParameters>(std::move(params)));
}

View File

@@ -111,6 +111,7 @@ public:
double tileLodScale = 1;
double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi;
double tileLodZoomShift = 0;
TileLodMode tileLodMode = TileLodMode::Default;
};
// Forward declaration of this method is required for the MapProjection class

View File

@@ -120,11 +120,14 @@ void RenderBackgroundLayer::update(gfx::ShaderRegistry& shaders,
[[maybe_unused]] UniqueChangeRequestVec& changes) {
assert(updateParameters);
const auto zoom = state.getIntegerZoom();
Range<uint8_t> zoomRange(0, zoom);
const auto tileCover = util::tileCover({.transformState = state,
.tileLodMinRadius = updateParameters->tileLodMinRadius,
.tileLodScale = updateParameters->tileLodScale,
.tileLodPitchThreshold = updateParameters->tileLodPitchThreshold},
zoom);
.tileLodPitchThreshold = updateParameters->tileLodPitchThreshold,
.tileLodMode = updateParameters->tileLodMode},
zoom,
zoomRange);
// renderTiles is always empty, we use tileCover instead
if (tileCover.empty()) {

View File

@@ -64,6 +64,7 @@ PaintParameters::PaintParameters(gfx::Context& context_,
double tileLodMinRadius_,
double tileLodScale_,
double tileLodPitchThreshold_,
TileLodMode tileLodMode_,
const gfx::ScissorRect& scissorRect_)
: context(context_),
backend(backend_),
@@ -83,6 +84,7 @@ PaintParameters::PaintParameters(gfx::Context& context_,
tileLodMinRadius(tileLodMinRadius_),
tileLodScale(tileLodScale_),
tileLodPitchThreshold(tileLodPitchThreshold_),
tileLodMode(tileLodMode_),
scissorRect(scissorRect_) {
pixelsToGLUnits = {{2.0f / state.getSize().width, -2.0f / state.getSize().height}};

View File

@@ -62,6 +62,7 @@ public:
double tileLodMinRadius,
double tileLodScale,
double tileLodPitchThreshold,
TileLodMode tileLodMode,
const gfx::ScissorRect&);
~PaintParameters();
@@ -139,6 +140,7 @@ public:
double tileLodMinRadius;
double tileLodScale;
double tileLodPitchThreshold;
TileLodMode tileLodMode;
gfx::ScissorRect scissorRect;
};

View File

@@ -200,6 +200,7 @@ std::unique_ptr<RenderTree> RenderOrchestrator::createRenderTree(
.tileLodScale = updateParameters->tileLodScale,
.tileLodPitchThreshold = updateParameters->tileLodPitchThreshold,
.tileLodZoomShift = updateParameters->tileLodZoomShift,
.tileLodMode = updateParameters->tileLodMode,
.dynamicTextureAtlas = dynamicTextureAtlas};
glyphManager->setURL(updateParameters->glyphURL);

View File

@@ -215,6 +215,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const std::shared_ptr<
updateParameters->tileLodMinRadius,
updateParameters->tileLodScale,
updateParameters->tileLodPitchThreshold,
updateParameters->tileLodMode,
scissorRect,
};

View File

@@ -133,11 +133,14 @@ void RenderImageSource::update(Immutable<style::Source::Impl> baseImpl_,
bool hasVisibleTile = false;
// Add additional wrapped tile ids if neccessary
Range<uint8_t> zoomRange(0, zoom);
auto idealTiles = util::tileCover({.transformState = transformState,
.tileLodMinRadius = parameters.tileLodMinRadius,
.tileLodScale = parameters.tileLodScale,
.tileLodPitchThreshold = parameters.tileLodPitchThreshold},
static_cast<uint8_t>(transformState.getZoom()));
.tileLodPitchThreshold = parameters.tileLodPitchThreshold,
.tileLodMode = parameters.tileLodMode},
static_cast<uint8_t>(transformState.getZoom()),
zoomRange);
for (auto tile : idealTiles) {
if (tile.wrap != 0 && tileCover[0].canonical.isChildOf(tile.canonical)) {
tileIds.emplace_back(tile.wrap, tileCover[0].canonical);

View File

@@ -37,6 +37,7 @@ public:
double tileLodScale = 1;
double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi;
double tileLodZoomShift = 0;
TileLodMode tileLodMode = TileLodMode::Default;
gfx::DynamicTextureAtlasPtr dynamicTextureAtlas;
bool isUpdateSynchronous = false;
};

View File

@@ -113,7 +113,8 @@ void TilePyramid::update(const std::vector<Immutable<style::LayerProperties>>& l
util::TileCoverParameters tileCoverParameters = {.transformState = parameters.transformState,
.tileLodMinRadius = parameters.tileLodMinRadius,
.tileLodScale = parameters.tileLodScale,
.tileLodPitchThreshold = parameters.tileLodPitchThreshold};
.tileLodPitchThreshold = parameters.tileLodPitchThreshold,
.tileLodMode = parameters.tileLodMode};
if (std::cmp_greater_equal(overscaledZoom, zoomRange.min)) {
int32_t idealZoom = std::min<int32_t>(zoomRange.max, overscaledZoom);
@@ -135,11 +136,11 @@ void TilePyramid::update(const std::vector<Immutable<style::LayerProperties>>& l
}
if (panZoom < idealZoom) {
panTiles = util::tileCover(tileCoverParameters, panZoom);
panTiles = util::tileCover(tileCoverParameters, panZoom, zoomRange);
}
}
idealTiles = util::tileCover(tileCoverParameters, idealZoom, tileZoom);
idealTiles = util::tileCover(tileCoverParameters, idealZoom, zoomRange, tileZoom);
if (parameters.mode == MapMode::Tile && type != SourceType::Raster && type != SourceType::RasterDEM &&
idealTiles.size() > 1) {
mbgl::Log::Warning(mbgl::Event::General,
@@ -176,8 +177,10 @@ void TilePyramid::update(const std::vector<Immutable<style::LayerProperties>>& l
// levels. Child tiles are used from the cache, but not created.
std::optional<util::TileRange> tileRange = std::nullopt;
if (bounds) {
tileRange = util::TileRange::fromLatLngBounds(
*bounds, zoomRange.min, std::min(tileZoom, static_cast<int32_t>(zoomRange.max)));
int32_t maxZoom = (parameters.tileLodMode == TileLodMode::Distance)
? zoomRange.max
: std::min(tileZoom, static_cast<int32_t>(zoomRange.max));
tileRange = util::TileRange::fromLatLngBounds(*bounds, zoomRange.min, maxZoom);
}
auto createTileFn = [&](const OverscaledTileID& tileID) -> Tile* {
if (tileRange && !tileRange->contains(tileID.canonical)) {

View File

@@ -52,6 +52,7 @@ public:
double tileLodScale = 1;
double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi;
double tileLodZoomShift = 0;
TileLodMode tileLodMode = TileLodMode::Default;
};
} // namespace mbgl

View File

@@ -157,6 +157,7 @@ int32_t coveringZoomLevel(double zoom, style::SourceType type, uint16_t size) no
std::vector<OverscaledTileID> tileCover(const TileCoverParameters& state,
uint8_t z,
const Range<uint8_t> zoomRange,
const std::optional<uint8_t>& overscaledZ) {
struct Node {
AABB aabb;
@@ -171,12 +172,28 @@ std::vector<OverscaledTileID> tileCover(const TileCoverParameters& state,
double sqrDist;
};
auto childrenOf = [](const Node& node) -> std::vector<Node> {
std::vector<Node> children(4);
for (int i = 0; i < 4; i++) {
const uint32_t childX = (node.x << 1) + (i % 2);
const uint32_t childY = (node.y << 1) + (i >> 1);
children[i] = node;
children[i].aabb = node.aabb.quadrant(i);
children[i].zoom = node.zoom + 1;
children[i].x = childX;
children[i].y = childY;
}
return children;
};
const auto& transform = state.transformState;
const double numTiles = std::pow(2.0, z);
const double worldSize = Projection::worldSize(transform.getScale());
const uint8_t minZoom = transform.getPitch() <= state.tileLodPitchThreshold ? z : 0;
const uint8_t maxZoom = z;
const uint8_t overscaledZoom = std::max(overscaledZ.value_or(z), z);
const bool allowVariableZoom = transform.getPitch() > state.tileLodPitchThreshold;
const uint8_t minZoom = allowVariableZoom ? zoomRange.min : z;
const uint8_t maxZoom = ((state.tileLodMode == TileLodMode::Distance) && allowVariableZoom) ? zoomRange.max : z;
const uint8_t overscaledZoom = std::max(overscaledZ.value_or(z), maxZoom);
const bool flippedY = transform.getViewportMode() == ViewportMode::FlippedY;
const auto centerPoint = TileCoordinate::fromScreenCoordinate(
@@ -185,6 +202,12 @@ std::vector<OverscaledTileID> tileCover(const TileCoverParameters& state,
const vec3 centerCoord = {{centerPoint.x, centerPoint.y, 0.0}};
assert(transform.getFreeCameraOptions().position);
const vec3 cameraPositionMercator = *transform.getFreeCameraOptions().position;
const double nominalScale = std::pow(2.0, z);
const vec3 cameraCoord = vec3Scale(cameraPositionMercator, nominalScale);
const double cameraToCenterDistanceMercator = vec3Length(vec3Sub(cameraCoord, centerCoord)) / worldSize;
const Frustum frustum = Frustum::fromInvProjMatrix(transform.getInvProjectionMatrix(), worldSize, z, flippedY);
// There should always be a certain number of maximum zoom level tiles
@@ -227,48 +250,51 @@ std::vector<OverscaledTileID> tileCover(const TileCoverParameters& state,
node.fullyVisible = intersection == IntersectionResult::Contains;
}
const vec3 distanceXyz = node.aabb.distanceXYZ(centerCoord);
const double* longestDim = std::max_element(distanceXyz.data(), distanceXyz.data() + distanceXyz.size());
assert(longestDim);
bool shouldSplitTile;
if (state.tileLodMode == TileLodMode::Distance) {
const vec3 camToTileMercator = vec3Scale(node.aabb.distanceXYZ(cameraCoord), 1.0 / worldSize);
const double distanceToTileMercator = vec3Length(camToTileMercator);
const double cosPitchToTile = std::max(0.0, camToTileMercator[2] / distanceToTileMercator);
const double pitchExponent =
0.5; // 0: constant screen width, 1/2: constant screen area, 1: constant screen height
double tileScale = std::pow(2.0, node.zoom);
shouldSplitTile = distanceToTileMercator * tileScale < std::pow(cosPitchToTile, pitchExponent) *
cameraToCenterDistanceMercator /
state.tileLodScale * nominalScale;
} else {
const vec3 distanceXyz = node.aabb.distanceXYZ(centerCoord);
const double* longestDim = std::max_element(distanceXyz.data(), distanceXyz.data() + distanceXyz.size());
assert(longestDim);
// We're using distance based heuristics to determine if a tile should
// be split into quadrants or not. radiusOfMaxLvlLodInTiles defines that
// there's always a certain number of maxLevel tiles next to the map
// center. Using the fact that a parent node in quadtree is twice the
// size of its children (per dimension) we can define distance
// thresholds for each relative level:
// f(k) = offset + 2 + 4 + 8 + 16 + ... + 2^k
// This is the same as:
// f(k) = offset + 2^(k+1)-2
const double distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - node.zoom)) - 2;
// We're using distance based heuristics to determine if a tile should
// be split into quadrants or not. radiusOfMaxLvlLodInTiles defines that
// there's always a certain number of maxLevel tiles next to the map
// center. Using the fact that a parent node in quadtree is twice the
// size of its children (per dimension) we can define distance
// thresholds for each relative level:
// f(k) = offset + 2 + 4 + 8 + 16 + ... + 2^k
// This is the same as:
// f(k) = offset + 2^(k+1)-2
const double distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - node.zoom)) - 2;
shouldSplitTile = *longestDim * state.tileLodScale < distToSplit;
}
// Have we reached the target depth or is the tile too far away to be any split further?
if (node.zoom == maxZoom || (*longestDim * state.tileLodScale > distToSplit && node.zoom >= minZoom)) {
if (node.zoom == maxZoom || (!shouldSplitTile && node.zoom >= minZoom)) {
// Perform precise intersection test between the frustum and aabb.
// This will cull < 1% false positives missed by the original test
if (node.fullyVisible || frustum.intersectsPrecise(node.aabb, true) != IntersectionResult::Separate) {
const OverscaledTileID id = {
node.zoom == maxZoom ? overscaledZoom : node.zoom, node.wrap, node.zoom, node.x, node.y};
const double dx = node.wrap * numTiles + node.x + 0.5 - centerCoord[0];
const double dy = node.y + 0.5 - centerCoord[1];
vec3 coordToLoadFirst = (state.tileLodMode == TileLodMode::Distance) ? cameraCoord : centerCoord;
const double dx = node.wrap * numTiles + node.x + 0.5 - coordToLoadFirst[0];
const double dy = node.y + 0.5 - coordToLoadFirst[1];
result.push_back({id, dx * dx + dy * dy});
}
continue;
}
for (int i = 0; i < 4; i++) {
const uint32_t childX = (node.x << 1) + (i % 2);
const uint32_t childY = (node.y << 1) + (i >> 1);
// Create child node and push to the stack for traversal
stack.emplace_back(node);
Node& child = stack.back();
child.aabb = node.aabb.quadrant(i);
child.zoom = node.zoom + 1;
child.x = childX;
child.y = childY;
} else {
std::vector<Node> children = childrenOf(node);
stack.insert(stack.end(), children.begin(), children.end());
}
}

View File

@@ -38,12 +38,14 @@ struct TileCoverParameters {
double tileLodMinRadius = 3;
double tileLodScale = 1;
double tileLodPitchThreshold = (60.0 / 180.0) * std::numbers::pi;
TileLodMode tileLodMode = TileLodMode::Default;
};
int32_t coveringZoomLevel(double z, style::SourceType type, uint16_t tileSize) noexcept;
std::vector<OverscaledTileID> tileCover(const TileCoverParameters& state,
uint8_t z,
const Range<uint8_t> zoomRange,
const std::optional<uint8_t>& overscaledZ = std::nullopt);
std::vector<UnwrappedTileID> tileCover(const LatLngBounds&, uint8_t z);
std::vector<UnwrappedTileID> tileCover(const Geometry<double>&, uint8_t z);

View File

@@ -10,6 +10,8 @@
using namespace mbgl;
const Range<uint8_t> zoomRange(0, 14);
TEST(TileCover, Empty) {
EXPECT_EQ((std::vector<UnwrappedTileID>{}), util::tileCover(LatLngBounds::empty(), 0));
}
@@ -44,7 +46,12 @@ TEST(TileCover, Pitch) {
.withPitch(40.0));
EXPECT_EQ((std::vector<OverscaledTileID>{{2, 1, 1}, {2, 2, 1}, {2, 1, 2}, {2, 2, 2}}),
util::tileCover({transform.getState()}, 2));
util::tileCover({transform.getState()}, 2, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{2, 1, 2}, {2, 2, 2}, {2, 1, 1}, {2, 2, 1}}),
util::tileCover(params, 2, zoomRange));
}
TEST(TileCover, PitchIssue15442) {
@@ -62,7 +69,47 @@ TEST(TileCover, PitchIssue15442) {
EXPECT_EQ((std::vector<OverscaledTileID>{
{2, 3, 1}, {2, 2, 1}, {2, 3, 0}, {2, 2, 0}, {2, 1, {2, 0, 0}}, {2, 1, {2, 1, 0}}}),
util::tileCover({transform.getState()}, 2));
util::tileCover({transform.getState()}, 2, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{
{2, 2, 1}, {2, 3, 1}, {2, 2, 0}, {2, 3, 0}, {2, 1, {2, 0, 0}}, {2, 1, {2, 1, 0}}}),
util::tileCover(params, 2, zoomRange));
}
TEST(TileCover, HighPitch) {
Transform transform;
transform.resize({512, 512});
// slightly offset center so that tile order is better defined
transform.jumpTo(CameraOptions()
.withCenter(LatLng{
0.1,
-0.1,
})
.withZoom(2.0)
.withBearing(5.0)
.withPitch(60.0));
util::TileCoverParameters params{transform.getState()};
params.tileLodPitchThreshold = 0.0;
params.tileLodMinRadius = 1.0;
EXPECT_EQ(
(std::vector<OverscaledTileID>{{2, 1, 1}, {2, 2, 1}, {2, 1, 2}, {2, 2, 2}, {2, 1, 0}, {2, 2, 0}, {2, 3, 0}}),
util::tileCover(params, 2, zoomRange));
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{2, 1, 1},
{2, 2, 1},
{3, 3, 4},
{3, 3, 5},
{2, 1, 0},
{2, 2, 0},
{3, 4, 4},
{2, 3, 0},
{3, 4, 5},
{3, 5, 4}}),
util::tileCover(params, 2, zoomRange));
}
TEST(TileCover, PitchOverAllowedByContentInsets) {
@@ -79,7 +126,12 @@ TEST(TileCover, PitchOverAllowedByContentInsets) {
EXPECT_LE(transform.getPitch() + 0.001, util::deg2rad(60));
EXPECT_EQ((std::vector<OverscaledTileID>{{3, 4, 3}, {3, 3, 3}, {3, 4, 4}, {3, 3, 4}}),
util::tileCover({transform.getState()}, 3));
util::tileCover({transform.getState()}, 3, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{3, 3, 4}, {3, 3, 3}, {3, 4, 4}, {3, 4, 3}}),
util::tileCover(params, 3, zoomRange));
}
TEST(TileCover, PitchWithLargerResultSet) {
@@ -95,7 +147,7 @@ TEST(TileCover, PitchWithLargerResultSet) {
.withBearing(-142.2630000003529176)
.withPitch(60.0));
auto cover = util::tileCover({.transformState = transform.getState()}, 5);
auto cover = util::tileCover({.transformState = transform.getState()}, 5, zoomRange);
// Returned vector has above 100 elements, we check first 16 as there is a
// plan to return lower LOD for distant tiles.
EXPECT_EQ((std::vector<OverscaledTileID>{{5, 15, 16},
@@ -147,7 +199,13 @@ TEST(TileCover, CoordinatesAreUnwrapped) {
EXPECT_EQ(
(std::vector<OverscaledTileID>{{1, 0, {1, 1, 0}}, {1, 1, {1, 0, 0}}, {1, 0, {1, 1, 1}}, {1, 1, {1, 0, 1}}}),
util::tileCover({transform.getState()}, 1));
util::tileCover({transform.getState()}, 1, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ(
(std::vector<OverscaledTileID>{{1, 0, {1, 1, 0}}, {1, 1, {1, 0, 0}}, {1, 0, {1, 1, 1}}, {1, 1, {1, 0, 1}}}),
util::tileCover(params, 1, zoomRange));
}
TEST(TileCover, DifferentOverscaledZ) {
@@ -164,7 +222,13 @@ TEST(TileCover, DifferentOverscaledZ) {
EXPECT_EQ(
(std::vector<OverscaledTileID>{{3, 0, {2, 1, 1}}, {3, 0, {2, 2, 1}}, {3, 0, {2, 1, 2}}, {3, 0, {2, 2, 2}}}),
util::tileCover({transform.getState()}, 2, 3));
util::tileCover({transform.getState()}, 2, zoomRange, 3));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ(
(std::vector<OverscaledTileID>{{3, 0, {2, 1, 1}}, {3, 0, {2, 2, 1}}, {3, 0, {2, 1, 2}}, {3, 0, {2, 2, 2}}}),
util::tileCover(params, 2, zoomRange, 3));
}
TEST(TileCover, DifferentOverscaledZWithPitch) {
@@ -176,7 +240,7 @@ TEST(TileCover, DifferentOverscaledZWithPitch) {
-10.0,
})
.withZoom(3.5)
.withPitch(60));
.withPitch(61));
EXPECT_EQ((std::vector<OverscaledTileID>{{5, 0, {3, 3, 3}},
{5, 0, {3, 4, 3}},
@@ -185,7 +249,18 @@ TEST(TileCover, DifferentOverscaledZWithPitch) {
{5, 0, {3, 3, 1}},
{5, 0, {3, 4, 1}},
{5, 0, {3, 2, 1}}}),
util::tileCover({transform.getState()}, 3, 5));
util::tileCover({transform.getState()}, 3, zoomRange, 5));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{5, 0, {3, 3, 3}},
{5, 0, {3, 4, 3}},
{5, 0, {3, 3, 2}},
{5, 0, {3, 4, 2}},
{5, 0, {3, 3, 1}},
{5, 0, {3, 4, 1}},
{5, 0, {3, 2, 1}}}),
util::tileCover(params, 3, zoomRange, 5));
}
TEST(TileCover, DifferentOverscaledZWrapped) {
@@ -200,7 +275,13 @@ TEST(TileCover, DifferentOverscaledZWrapped) {
EXPECT_EQ(
(std::vector<OverscaledTileID>{{2, 0, {1, 1, 0}}, {2, 1, {1, 0, 0}}, {2, 0, {1, 1, 1}}, {2, 1, {1, 0, 1}}}),
util::tileCover({transform.getState()}, 1, 2));
util::tileCover({transform.getState()}, 1, zoomRange, 2));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ(
(std::vector<OverscaledTileID>{{2, 0, {1, 1, 0}}, {2, 1, {1, 0, 0}}, {2, 0, {1, 1, 1}}, {2, 1, {1, 0, 1}}}),
util::tileCover(params, 1, zoomRange, 2));
}
TEST(TileCover, FlippedY) {
@@ -215,7 +296,12 @@ TEST(TileCover, FlippedY) {
.withZoom(1.0));
EXPECT_EQ((std::vector<OverscaledTileID>{{1, 0, 0}, {1, 1, 0}, {1, 0, 1}, {1, 1, 1}}),
util::tileCover({transform.getState()}, 1));
util::tileCover({transform.getState()}, 1, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{1, 0, 0}, {1, 1, 0}, {1, 0, 1}, {1, 1, 1}}),
util::tileCover(params, 1, zoomRange));
}
TEST(TileCover, FlippedYPitch) {
@@ -232,7 +318,12 @@ TEST(TileCover, FlippedYPitch) {
.withPitch(40.0));
EXPECT_EQ((std::vector<OverscaledTileID>{{2, 1, 1}, {2, 2, 1}, {2, 1, 2}, {2, 2, 2}}),
util::tileCover({transform.getState()}, 2));
util::tileCover({transform.getState()}, 2, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{2, 1, 2}, {2, 2, 2}, {2, 1, 1}, {2, 2, 1}}),
util::tileCover(params, 2, zoomRange));
}
TEST(TileCover, FlippedYHelsinki) {
@@ -247,7 +338,12 @@ TEST(TileCover, FlippedYHelsinki) {
.withZoom(11.447425));
EXPECT_EQ((std::vector<OverscaledTileID>{{11, 1165, 592}, {11, 1166, 592}, {11, 1165, 593}, {11, 1166, 593}}),
util::tileCover({transform.getState()}, 11));
util::tileCover({transform.getState()}, 11, zoomRange));
util::TileCoverParameters params{transform.getState()};
params.tileLodMode = TileLodMode::Distance;
EXPECT_EQ((std::vector<OverscaledTileID>{{11, 1165, 592}, {11, 1166, 592}, {11, 1165, 593}, {11, 1166, 593}}),
util::tileCover(params, 11, zoomRange));
}
TEST(TileCoverStream, Arctic) {