Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 83 additions & 10 deletions src/gdal/gdal_template.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ namespace {

constexpr int max_screen_subsampling = 64;
constexpr qsizetype max_queued_screen_tiles = 64;
// Bound per-view work even when the visible exact-resolution set cannot fit
// the cache. The effective limit is reduced for larger decoded tile formats.
constexpr qsizetype max_attempted_screen_tiles = 512;

int tileSubsamplingForScale(double scale, const QSize& block_size)
{
Expand Down Expand Up @@ -398,8 +401,11 @@ void GdalTemplate::updateRenderContext(const ViewRenderContext& context)

auto const scale = std::max(getTemplateScaleX(), getTemplateScaleY()) * context.view_zoom;
auto const subsampling = chooseTiledSubsampling(Util::mmToPixelPhysical(scale));
auto const window = tileWindowForMapRect(context.visible_map_rect, subsampling);
auto const replace_pending_tiles = !wanted_window.intersects(window);
auto const window = screenTileWindowForMapRect(
context.visible_map_rect, subsampling
);
auto const replace_pending_tiles = !(wanted_window == window)
&& !wanted_window.intersects(window);
wanted_window = window;
queueWantedTiles(window, replace_pending_tiles);
}
Expand Down Expand Up @@ -515,6 +521,8 @@ void GdalTemplate::shutdownTiledSource()
tiled_dataset.reset();

queued_tiles.clear();
attempted_window = {};
attempted_tiles.clear();

tile_cache.clear();
tiled_raster_info = GdalImageReader::RasterInfo();
Expand Down Expand Up @@ -551,14 +559,22 @@ void GdalTemplate::queueWantedTiles(const TileWindow& window, bool replace_pendi
tile_pool.clear();
queued_tiles.clear();
}
if (replace_pending_tiles || !(window == attempted_window))
{
attempted_window = window;
attempted_tiles.clear();
}

if (window.isEmpty())
return;
auto const admission_budget = screenTileAdmissionBudget();
if (attempted_tiles.size() >= admission_budget)
return;

auto planIfMissing = [this, &missing_tiles, &planned_tiles](
const GdalTileKey& key, double dist_sq, bool fallback) {
if (tile_cache.contains(key) || queued_tiles.contains(key)
|| planned_tiles.contains(key))
|| attempted_tiles.contains(key) || planned_tiles.contains(key))
{
return;
}
Expand Down Expand Up @@ -605,6 +621,8 @@ void GdalTemplate::queueWantedTiles(const TileWindow& window, bool replace_pendi
coarser >= safe_subsampling * 2;
coarser >>= 1)
{
if (!isTiledSubsamplingAligned(coarser))
continue;
auto const coarser_x = int(qint64(tx) * safe_subsampling / coarser);
auto const coarser_y = int(qint64(ty) * safe_subsampling / coarser);
planIfMissing(
Expand Down Expand Up @@ -635,13 +653,18 @@ void GdalTemplate::queueWantedTiles(const TileWindow& window, bool replace_pendi
auto available_slots = std::max<qsizetype>(
0, max_queued_screen_tiles - queued_tiles.size()
);
auto available_admissions = std::max<qsizetype>(
0, admission_budget - attempted_tiles.size()
);
for (auto const& missing : missing_tiles)
{
if (available_slots == 0)
if (available_slots == 0 || available_admissions == 0)
break;
auto const key = missing.key;
if (tile_cache.contains(key) || queued_tiles.contains(key))
if (tile_cache.contains(key) || queued_tiles.contains(key)
|| attempted_tiles.contains(key))
continue;
attempted_tiles.insert(key);
queued_tiles.insert(key, generation);
tile_pool.start([this, key, generation] {
auto tile = readTileImage(
Expand All @@ -659,6 +682,7 @@ void GdalTemplate::queueWantedTiles(const TileWindow& window, bool replace_pendi
);
}, missing.fallback ? 2 : 1);
--available_slots;
--available_admissions;
}
}

Expand Down Expand Up @@ -913,6 +937,34 @@ GdalTemplate::TileWindow GdalTemplate::tileWindowForMapRect(const QRectF& map_re
}


GdalTemplate::TileWindow GdalTemplate::screenTileWindowForMapRect(
const QRectF& map_rect, int subsampling) const
{
auto window = tileWindowForMapRect(map_rect, subsampling);
auto const admission_budget = screenTileAdmissionBudget();
auto const max_subsampling = std::max(1, std::min({
tiled_raster_info.block_size.width(),
tiled_raster_info.block_size.height(),
max_screen_subsampling,
}));

while (window.tileCount() > admission_budget)
{
auto coarser = window.subsampling * 2;
while (coarser <= max_subsampling
&& !isTiledSubsamplingAligned(coarser))
{
coarser *= 2;
}
if (coarser > max_subsampling)
return {};
window = tileWindowForMapRect(map_rect, coarser);
}

return window;
}


// static
GdalTileKey GdalTemplate::tileKey(int tile_x, int tile_y, int subsampling)
{
Expand All @@ -930,22 +982,43 @@ int GdalTemplate::chooseTileSubsampling(double scale, const QSize& block_size)
int GdalTemplate::chooseTiledSubsampling(double scale) const
{
auto subsampling = chooseTileSubsampling(scale, tiled_raster_info.block_size);
if (!has_tiled_origin_tile)
return subsampling;

// GDAL WMS/TMS overview bands lose the sub-tile remainder bits of TileX
// and TileY when the origin is not aligned with the overview factor.
// Restrict GDAL to overview levels that keep the cropped origin aligned.
while (subsampling > 1
&& (tiled_origin_tile.x() % subsampling != 0
|| tiled_origin_tile.y() % subsampling != 0))
while (subsampling > 1 && !isTiledSubsamplingAligned(subsampling))
{
subsampling >>= 1;
}
return subsampling;
}


bool GdalTemplate::isTiledSubsamplingAligned(int subsampling) const
{
auto const safe_subsampling = std::max(1, subsampling);
return !has_tiled_origin_tile
|| (tiled_origin_tile.x() % safe_subsampling == 0
&& tiled_origin_tile.y() % safe_subsampling == 0);
}


qsizetype GdalTemplate::screenTileAdmissionBudget() const
{
auto const block_size = tiled_raster_info.block_size;
auto const bits_per_pixel = std::max(
8, int(QImage::toPixelFormat(tiled_raster_info.image_format).bitsPerPixel())
);
auto const decoded_bytes = qint64(block_size.width()) * block_size.height()
* bits_per_pixel / 8;
auto const estimated_cost = std::max<qint64>(1, (decoded_bytes + 1023) / 1024);
auto const cache_capacity = tile_cache.maxCost() / estimated_cost;
return std::clamp<qsizetype>(
cache_capacity, max_queued_screen_tiles, max_attempted_screen_tiles
);
}


int GdalTemplate::workerCountForSource() const
{
return std::clamp(QThread::idealThreadCount(), 1, 4);
Expand Down
13 changes: 13 additions & 0 deletions src/gdal/gdal_template.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <QHash>
#include <QImage>
#include <QPoint>
#include <QSet>
#include <QSize>
#include <QString>
#include <QThreadPool>
Expand Down Expand Up @@ -132,6 +133,13 @@ class GdalTemplate : public TemplateImage
return tile_x_min > tile_x_max || tile_y_min > tile_y_max;
}

qint64 tileCount() const
{
return isEmpty() ? 0
: qint64(tile_x_max - tile_x_min + 1)
* qint64(tile_y_max - tile_y_min + 1);
}

bool operator==(const TileWindow& other) const
{
return tile_x_min == other.tile_x_min
Expand Down Expand Up @@ -160,6 +168,7 @@ class GdalTemplate : public TemplateImage
void onTileLoadFailed(const GdalTileKey& key, std::uint64_t generation);
void markTileAreaDirty(int tile_x, int tile_y, int subsampling);
TileWindow tileWindowForMapRect(const QRectF& map_rect, int subsampling) const;
TileWindow screenTileWindowForMapRect(const QRectF& map_rect, int subsampling) const;
const QImage* findBestCachedTile(int tile_x, int tile_y, int subsampling, QRectF* source_rect) const;

static bool readTmsTileOrigin(const QString& template_path, QPoint* origin_tile);
Expand All @@ -179,6 +188,8 @@ class GdalTemplate : public TemplateImage
const QRect& cached_rect,
const QSize& cached_image_size);
int chooseTiledSubsampling(double scale) const;
bool isTiledSubsamplingAligned(int subsampling) const;
qsizetype screenTileAdmissionBudget() const;
int workerCountForSource() const;

GDALDatasetUniquePtr tiled_dataset;
Expand All @@ -191,6 +202,8 @@ class GdalTemplate : public TemplateImage
std::atomic<std::uint64_t> tile_generation{0};
QThreadPool tile_pool;
QHash<GdalTileKey, std::uint64_t> queued_tiles;
TileWindow attempted_window;
QSet<GdalTileKey> attempted_tiles;

QCache<GdalTileKey, QImage> tile_cache;
};
Expand Down
89 changes: 89 additions & 0 deletions test/gdal_tiled_t.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,95 @@ private slots:
QVERIFY(source.queued_tiles.size() <= 64);
}

void oversizedScreenWindowConvergesWithinAdmissionBudget()
{
QTemporaryDir dir;
QVERIFY(dir.isValid());
auto const path = createGeoTiff(
dir.filePath(QStringLiteral("oversized-window.tif")),
{ 2048, 2048 }, { 64, 64 }, false
);
QVERIFY(!path.isEmpty());

Map map;
GdalTemplate source(path, &map);
QVERIFY(source.loadTemplateFileImpl());
source.tile_pool.setMaxThreadCount(4);
// Force every decoded tile out of the cache. A stable scheduler must
// still attempt each admitted key at most once for this view.
source.tile_cache.setMaxCost(1);
GdalTemplate::TileWindow window { 0, 0, 31, 31, 1 };
source.wanted_window = window;
auto const admission_budget = source.screenTileAdmissionBudget();
QCOMPARE(admission_budget, 64);
source.queueWantedTiles(window, true);

QTRY_COMPARE_WITH_TIMEOUT(
source.attempted_tiles.size(), admission_budget, 15000
);
QTRY_VERIFY_WITH_TIMEOUT(source.queued_tiles.isEmpty(), 15000);
QVERIFY(source.attempted_window == window);
QVERIFY(source.tile_cache.totalCost() <= source.tile_cache.maxCost());

auto const stable_attempts = source.attempted_tiles;
QTest::qWait(250);
QVERIFY(source.attempted_tiles == stable_attempts);
QVERIFY(source.queued_tiles.isEmpty());
}

void oddTmsOriginNeverQueuesMisalignedFallbacks()
{
QTemporaryDir dir;
QVERIFY(dir.isValid());
auto const path = createGeoTiff(
dir.filePath(QStringLiteral("odd-origin.tif")),
{ 1024, 1024 }, { 64, 64 }, false
);
QVERIFY(!path.isEmpty());

Map map;
GdalTemplate source(path, &map);
QVERIFY(source.loadTemplateFileImpl());
source.tile_pool.setMaxThreadCount(1);
source.has_tiled_origin_tile = true;
source.tiled_origin_tile = { 3, 5 };
QCOMPARE(source.chooseTiledSubsampling(0.25), 1);
source.tile_cache.setMaxCost(1);
QVERIFY(source.screenTileWindowForMapRect(
source.getTemplateExtent(), 1
).isEmpty());

source.queueWantedTiles({ 0, 0, 15, 15, 1 }, true);
QVERIFY(!source.attempted_tiles.isEmpty());
for (auto const& key : source.attempted_tiles)
QCOMPARE(key.subsampling, 1);

source.shutdownTiledSource();
QVERIFY(source.attempted_tiles.isEmpty());
}

void oversizedScreenWindowUsesCoarsestFittingOverview()
{
QTemporaryDir dir;
QVERIFY(dir.isValid());
auto const path = createGeoTiff(
dir.filePath(QStringLiteral("adaptive-overview.tif")),
{ 2048, 2048 }, { 64, 64 }, false
);
QVERIFY(!path.isEmpty());

Map map;
GdalTemplate source(path, &map);
QVERIFY(source.loadTemplateFileImpl());
source.tile_cache.setMaxCost(1);
auto const window = source.screenTileWindowForMapRect(
source.getTemplateExtent(), 1
);
QVERIFY(!window.isEmpty());
QCOMPARE(window.subsampling, 4);
QCOMPARE(window.tileCount(), 64);
}

void nonGeoreferencedLargeRasterStaysTiled()
{
QTemporaryDir dir;
Expand Down
Loading