diff --git a/src/gdal/gdal_template.cpp b/src/gdal/gdal_template.cpp index 181e143ee..f46cb4a4c 100644 --- a/src/gdal/gdal_template.cpp +++ b/src/gdal/gdal_template.cpp @@ -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) { @@ -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); } @@ -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(); @@ -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; } @@ -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( @@ -635,13 +653,18 @@ void GdalTemplate::queueWantedTiles(const TileWindow& window, bool replace_pendi auto available_slots = std::max( 0, max_queued_screen_tiles - queued_tiles.size() ); + auto available_admissions = std::max( + 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( @@ -659,6 +682,7 @@ void GdalTemplate::queueWantedTiles(const TileWindow& window, bool replace_pendi ); }, missing.fallback ? 2 : 1); --available_slots; + --available_admissions; } } @@ -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) { @@ -930,15 +982,11 @@ 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; } @@ -946,6 +994,31 @@ int GdalTemplate::chooseTiledSubsampling(double scale) const } +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(1, (decoded_bytes + 1023) / 1024); + auto const cache_capacity = tile_cache.maxCost() / estimated_cost; + return std::clamp( + cache_capacity, max_queued_screen_tiles, max_attempted_screen_tiles + ); +} + + int GdalTemplate::workerCountForSource() const { return std::clamp(QThread::idealThreadCount(), 1, 4); diff --git a/src/gdal/gdal_template.h b/src/gdal/gdal_template.h index ebacdef1e..b8bbf162c 100644 --- a/src/gdal/gdal_template.h +++ b/src/gdal/gdal_template.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -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 @@ -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); @@ -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; @@ -191,6 +202,8 @@ class GdalTemplate : public TemplateImage std::atomic tile_generation{0}; QThreadPool tile_pool; QHash queued_tiles; + TileWindow attempted_window; + QSet attempted_tiles; QCache tile_cache; }; diff --git a/test/gdal_tiled_t.cpp b/test/gdal_tiled_t.cpp index 79148f4c0..f3952cc1e 100644 --- a/test/gdal_tiled_t.cpp +++ b/test/gdal_tiled_t.cpp @@ -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;