Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ SpacesInSquareBrackets: false
SortIncludes: CaseSensitive
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<[a-z].*>' # System/STL headers
Priority: 1
- Regex: '^<Q.*>' # Qt headers
Priority: 1
- Regex: '^<[a-z].*>' # System/STL headers
CaseSensitive: true
Priority: 2
- Regex: '.*' # Project headers
- Regex: '.*' # Project headers
Priority: 3

# Alignment
Expand Down
10 changes: 8 additions & 2 deletions src/GeoMap/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@

target_sources(${CMAKE_PROJECT_NAME}
PRIVATE
ElevationTilePyramid.cc
ElevationTilePyramid.h
HeightField.cc
HeightField.h
HeightSource.cc
HeightSource.h
SurfaceAnalysis.cc
SurfaceAnalysis.h
SurfaceModel.cc
SurfaceModel.h
TerrariumHeightSource.cc
TerrariumHeightSource.h
TerrariumTileFetcher.cc
TerrariumTileFetcher.h
TileImageSource.cc
TileImageSource.h
TileMath.cc
Expand Down Expand Up @@ -55,6 +59,8 @@ target_link_libraries(GeoMapModule
Qt6::Positioning
Qt6::Qml
Qt6::Quick3D
PRIVATE
QGCLogging
)

target_include_directories(GeoMapModule PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
1 change: 1 addition & 0 deletions src/GeoMap/CheckerboardTextureData.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "CheckerboardTextureData.h"

#include <QtCore/QSize>

#include <algorithm>

CheckerboardTextureData::CheckerboardTextureData(QQuick3DObject* parent) : QQuick3DTextureData(parent)
Expand Down
107 changes: 107 additions & 0 deletions src/GeoMap/ElevationTilePyramid.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/****************************************************************************
*
* (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
*
* QGroundControl is licensed according to the terms in the file
* COPYING.md in the root of the source code directory.
*
****************************************************************************/

#include "ElevationTilePyramid.h"

#include <limits>
#include <utility>

#include "QGCLoggingCategory.h"

QGC_LOGGING_CATEGORY(GeoMapElevationTilePyramidVerboseLog, "GeoMap.ElevationTilePyramid.Verbose")

namespace {

/// Applies \a delta to the descendant count of every ancestor of \a key,
/// pruning zeroed entries
void adjustAncestorCounts(QHash<TileMath::TileKey, int>& counts, const TileMath::TileKey& key, int delta)
{
for (int zoom = key.zoom - 1; zoom >= TileMath::kMinZoom; zoom--) {
const int shift = key.zoom - zoom;
const TileMath::TileKey ancestor{key.x >> shift, key.y >> shift, zoom};
const int count = counts.value(ancestor, 0) + delta;
if (count > 0) {
counts.insert(ancestor, count);
} else {
counts.remove(ancestor);
}
}
}

} // namespace

bool ElevationTilePyramid::insertTile(const TileMath::TileKey& key, Grid grid, TileMath::TileKey* evictedKey)
{
if (!TileMath::isValidKey(key) || !grid.isValid()) {
return false;
}
const bool replacing = _tiles.contains(key);
if (!replacing && (_tiles.count() >= kMaxTiles)) {
const TileMath::TileKey evicted = _evictLeastRecentlyUsed();
if (TileMath::isValidKey(evicted) && evictedKey) {
*evictedKey = evicted;
}
}
_tiles.insert(key, std::move(grid));
_lastUsed.insert(key, ++_useTick);
if (!replacing) {
adjustAncestorCounts(_descendantCounts, key, +1);
}
return true;
}

TileMath::TileKey ElevationTilePyramid::_evictLeastRecentlyUsed()
{
// Pinned tiles back rendered patches: never evict them. When everything
// is pinned there is no victim and the working set grows past the cap.
// O(tileCount) victim scan: fine at kMaxTiles, revisit if the cap grows
TileMath::TileKey lruKey{0, 0, -1};
qint64 lruTick = std::numeric_limits<qint64>::max();
for (auto it = _lastUsed.cbegin(); it != _lastUsed.cend(); ++it) {
if ((it.value() < lruTick) && !_pinnedKeys.contains(it.key())) {
lruTick = it.value();
lruKey = it.key();
}
}
if (!TileMath::isValidKey(lruKey)) {
qCDebug(GeoMapElevationTilePyramidVerboseLog)
<< "all resident tiles pinned, growing past cap, tileCount" << _tiles.count();
return lruKey;
}
// Verbose: fires per insert once the working set is full
qCDebug(GeoMapElevationTilePyramidVerboseLog)
<< "evicting least-recently-used tile" << lruKey << "tileCount" << _tiles.count();
_tiles.remove(lruKey);
_lastUsed.remove(lruKey);
adjustAncestorCounts(_descendantCounts, lruKey, -1);
return lruKey;
}

ElevationTilePyramid::View ElevationTilePyramid::bestTileFor(const TileMath::TileKey& key) const
{
// Valid zoom bounds also cap the ancestor-shift distance below UB
if (!TileMath::isValidKey(key)) {
return View{};
}
_lookupCount++;
for (int zoom = key.zoom; zoom >= TileMath::kMinZoom; zoom--) {
const int shift = key.zoom - zoom;
const TileMath::TileKey candidate{key.x >> shift, key.y >> shift, zoom};
const auto it = _tiles.constFind(candidate);
if (it == _tiles.cend()) {
continue;
}
_lastUsed.insert(candidate, ++_useTick);
const double scale = 1.0 / (1LL << shift);
return View{&it.value(), candidate,
QRectF((key.x - (qint64(candidate.x) << shift)) * scale,
(key.y - (qint64(candidate.y) << shift)) * scale, scale, scale)};
}
return View{};
}
98 changes: 98 additions & 0 deletions src/GeoMap/ElevationTilePyramid.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/****************************************************************************
*
* (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
*
* QGroundControl is licensed according to the terms in the file
* COPYING.md in the root of the source code directory.
*
****************************************************************************/

#pragma once

#include <QtCore/QHash>
#include <QtCore/QList>
#include <QtCore/QRectF>
#include <QtCore/QSet>

#include <utility>

#include "TileMath.h"

/// In-memory working set of decoded elevation tiles, keyed by slippy tile.
///
/// This is the synchronous sampling layer of the continuous-drape design: the
/// persistent store remains QGC's shared tile cache database (encoded tiles,
/// async); this pyramid retains what the fetch path has already decoded so a
/// height estimate is answerable immediately at mesh time. Lookup serves a
/// query tile from itself or its nearest stored ancestor with the sub-window
/// to sample — never a descendant — so coverage is continuous wherever any
/// ancestor data exists.
///
/// Not thread-safe: confine to one thread or synchronize externally. This
/// includes the const lookup methods — they mutate recency/instrumentation
/// state, so even concurrent reads race.
/// Bounded working set: least-recently-used tiles are evicted past kMaxTiles.
class ElevationTilePyramid
{
public:
/// Working-set cap: least-recently-used tiles are evicted at insert time
/// (256x256 float tiles ~256KB each, so the cap bounds memory at ~32MB)
static constexpr int kMaxTiles = 128;

/// Decoded elevation samples for one tile, row-major from the NW corner
struct Grid
{
int width = 0; ///< samples per row
int height = 0; ///< rows
QList<float> heights; ///< meters

bool isValid() const { return (width > 0) && (height > 0) && (heights.size() == qsizetype(width) * height); }
};

/// Where to sample a query tile: the stored tile and the query's extent
/// within it (unit UV from the NW corner)
struct View
{
const Grid* grid = nullptr; ///< null when nothing stored covers the query
TileMath::TileKey key; ///< stored tile the view samples
QRectF subWindow;

bool isValid() const { return grid != nullptr; }
};

/// Stores \a grid for \a key, replacing any previous tile. Invalid grids
/// are rejected (returns false). When the insert evicts a tile, its key is
/// written to \a evictedKey (left untouched otherwise).
bool insertTile(const TileMath::TileKey& key, Grid grid, TileMath::TileKey* evictedKey = nullptr);

bool hasTile(const TileMath::TileKey& key) const { return _tiles.contains(key); }

/// Finest stored tile covering \a key: the tile itself when present, else
/// the nearest ancestor. View pointers stay valid until the next insert.
View bestTileFor(const TileMath::TileKey& key) const;

int tileCount() const { return static_cast<int>(_tiles.count()); }

/// Tiles that must not be evicted (they back rendered patches or resolve
/// them as ancestors). kMaxTiles becomes a soft cap: when every resident
/// tile is pinned, inserts grow past it rather than break a rendered mesh.
void setPinnedKeys(QSet<TileMath::TileKey> keys) { _pinnedKeys = std::move(keys); }

/// True when any stored tile lies strictly deeper within \a key's extent
/// (i.e. a lookup inside \a key could resolve finer than \a key itself)
bool hasDescendant(const TileMath::TileKey& key) const { return _descendantCounts.value(key, 0) > 0; }

/// Perf instrumentation: number of bestTileFor resolutions performed
/// (test hook, not API — semantics track the resolution strategy)
qint64 lookupCountForTest() const { return _lookupCount; }

private:
TileMath::TileKey _evictLeastRecentlyUsed();

QHash<TileMath::TileKey, Grid> _tiles;
QHash<TileMath::TileKey, int> _descendantCounts; ///< stored tiles strictly below each key
QSet<TileMath::TileKey> _pinnedKeys;
mutable QHash<TileMath::TileKey, qint64> _lastUsed;
mutable qint64 _useTick = 0;
mutable qint64 _lookupCount = 0;
};
2 changes: 2 additions & 0 deletions src/GeoMap/GeoMap.qml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ Item {
required property int zoomLevel
required property var heights
required property bool covered
required property var edgeLodDeltas
required property var tileImage
required property bool hasTileImage

Expand All @@ -181,6 +182,7 @@ Item {
gridSize: patchModel.gridSize
span: patchDelegate.span
heights: patchDelegate.heights
edgeLodDeltas: patchDelegate.edgeLodDeltas
}

Texture {
Expand Down
5 changes: 5 additions & 0 deletions src/GeoMap/GeoMapCamera.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#include "GeoMapCamera.h"

#include <QtCore/QtMath>

#include <algorithm>
#include <cmath>

#include "QGCLoggingCategory.h"
#include "TileMath.h"

QGC_LOGGING_CATEGORY(GeoMapCameraLog, "GeoMap.GeoMapCamera")

namespace {

struct Vec3
Expand Down Expand Up @@ -153,6 +157,7 @@ void GeoMapCamera::setMode(Mode mode)
if (mode == _mode) {
return;
}
qCDebug(GeoMapCameraLog) << "mode" << _mode << "->" << mode;
_mode = mode;
emit modeChanged();
}
Expand Down
1 change: 1 addition & 0 deletions src/GeoMap/GeoMapCamera.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <QtGui/QVector3D>
#include <QtPositioning/QGeoCoordinate>
#include <QtQmlIntegration/QtQmlIntegration>

#include <optional>

/// Camera pose model for the GeoMap engine.
Expand Down
5 changes: 5 additions & 0 deletions src/GeoMap/GeoScene.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
#include <cmath>

#include "GeoMapCamera.h"
#include "QGCLoggingCategory.h"
#include "TileMath.h"

QGC_LOGGING_CATEGORY(GeoMapGeoSceneLog, "GeoMap.GeoScene")

GeoScene::GeoScene(QObject* parent) : QObject(parent) {}

void GeoScene::setCamera(GeoMapCamera* camera)
{
if (camera == _camera) {
return;
}
qCDebug(GeoMapGeoSceneLog) << "camera" << (camera ? "set" : "cleared");
if (_camera) {
disconnect(_camera, nullptr, this, nullptr);
}
Expand Down Expand Up @@ -79,5 +83,6 @@ void GeoScene::_maybeReanchor()
}
_origin = cameraWorld;
_originSet = true;
qCDebug(GeoMapGeoSceneLog) << "scene origin re-anchored to" << _origin;
emit sceneOriginChanged();
}
Loading
Loading