diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2fed236a..7b0a12307 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: cesium-native -on: [push, pull_request] +on: [push] jobs: QuickChecks: name: "Quick Checks" diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/SubtreeAvailability.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/SubtreeAvailability.h index 71b1dc475..bfd140893 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/SubtreeAvailability.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/SubtreeAvailability.h @@ -85,14 +85,18 @@ class SubtreeAvailability { * @return A future that resolves to a `SubtreeAvailability` instance for the * subtree file, or std::nullopt if something goes wrong. */ - static CesiumAsync::Future> loadSubtree( + + using LoadResult = + std::pair, CesiumAsync::RequestData>; + + static CesiumAsync::Future loadSubtree( ImplicitTileSubdivisionScheme subdivisionScheme, uint32_t levelsInSubtree, const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, const std::shared_ptr& pLogger, - const std::string& subtreeUrl, - const std::vector& requestHeaders); + const std::string& baseUrl, + const CesiumAsync::IAssetResponse* baseResponse, + const CesiumAsync::UrlResponseDataMap& additionalResponse); /** * @brief An AvailibilityView that indicates that either all tiles are diff --git a/Cesium3DTilesContent/src/SubtreeAvailability.cpp b/Cesium3DTilesContent/src/SubtreeAvailability.cpp index 6281e05df..fb98c7f2f 100644 --- a/Cesium3DTilesContent/src/SubtreeAvailability.cpp +++ b/Cesium3DTilesContent/src/SubtreeAvailability.cpp @@ -137,44 +137,56 @@ std::optional parseAvailabilityView( std::move(subtree)); } -/*static*/ CesiumAsync::Future> +/*static*/ CesiumAsync::Future SubtreeAvailability::loadSubtree( ImplicitTileSubdivisionScheme subdivisionScheme, uint32_t levelsInSubtree, const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, const std::shared_ptr& pLogger, - const std::string& subtreeUrl, - const std::vector& requestHeaders) { + const std::string& baseUrl, + const CesiumAsync::IAssetResponse* baseResponse, + const CesiumAsync::UrlResponseDataMap& additionalResponse) { auto pReader = std::make_shared(); - return pReader->load(asyncSystem, pAssetAccessor, subtreeUrl, requestHeaders) + return pReader->load(asyncSystem, baseUrl, baseResponse, additionalResponse) .thenInMainThread( - [pLogger, subtreeUrl, subdivisionScheme, levelsInSubtree, pReader]( + [pLogger, baseUrl, subdivisionScheme, levelsInSubtree, pReader]( ReadJsonResult&& subtree) - -> std::optional { + -> SubtreeAvailability::LoadResult { if (!subtree.errors.empty()) { SPDLOG_LOGGER_ERROR( pLogger, "Errors while loading subtree from {}:\n- {}", - subtreeUrl, + baseUrl, CesiumUtility::joinToString(subtree.errors, "\n- ")); } if (!subtree.warnings.empty()) { SPDLOG_LOGGER_WARN( pLogger, "Warnings while loading subtree from {}:\n- {}", - subtreeUrl, + baseUrl, CesiumUtility::joinToString(subtree.warnings, "\n- ")); } + if (!subtree.urlNeeded.empty()) { + return SubtreeAvailability::LoadResult{ + std::nullopt, + CesiumAsync::RequestData{subtree.urlNeeded, {}}}; + } if (!subtree.value) { - return std::nullopt; + return SubtreeAvailability::LoadResult{ + std::nullopt, + CesiumAsync::RequestData{}}; } - return SubtreeAvailability::fromSubtree( - subdivisionScheme, - levelsInSubtree, - std::move(*subtree.value)); + std::optional returnedSubtree = + SubtreeAvailability::fromSubtree( + subdivisionScheme, + levelsInSubtree, + std::move(*subtree.value)); + + return SubtreeAvailability::LoadResult{ + std::move(returnedSubtree), + CesiumAsync::RequestData{}}; }); } diff --git a/Cesium3DTilesContent/test/TestSubtreeAvailability.cpp b/Cesium3DTilesContent/test/TestSubtreeAvailability.cpp index 7685398d8..346f27755 100644 --- a/Cesium3DTilesContent/test/TestSubtreeAvailability.cpp +++ b/Cesium3DTilesContent/test/TestSubtreeAvailability.cpp @@ -289,16 +289,30 @@ std::optional mockLoadSubtreeJson( auto pMockTaskProcessor = std::make_shared(); CesiumAsync::AsyncSystem asyncSystem{pMockTaskProcessor}; + auto testEntry = pMockAssetAccessor->mockCompletedRequests.find("test"); + assert(testEntry != pMockAssetAccessor->mockCompletedRequests.end()); + const CesiumAsync::IAssetResponse* testResponse = + testEntry->second->response(); + assert(testResponse); + + CesiumAsync::UrlResponseDataMap additionalResponses; + auto bufferEntry = pMockAssetAccessor->mockCompletedRequests.find("buffer"); + additionalResponses.emplace( + bufferEntry->first, + CesiumAsync::ResponseData{ + bufferEntry->second.get(), + bufferEntry->second->response()}); + auto subtreeFuture = SubtreeAvailability::loadSubtree( ImplicitTileSubdivisionScheme::Quadtree, levelsInSubtree, asyncSystem, - pMockAssetAccessor, spdlog::default_logger(), "test", - {}); + testResponse, + additionalResponses); - return waitForFuture(asyncSystem, std::move(subtreeFuture)); + return waitForFuture(asyncSystem, std::move(subtreeFuture)).first; } } // namespace @@ -557,10 +571,6 @@ TEST_CASE("Test parsing subtree format") { "test", CesiumAsync::HttpHeaders{}, std::move(pMockResponse)); - std::map> mapUrlToRequest{ - {"test", std::move(pMockRequest)}}; - auto pMockAssetAccessor = - std::make_shared(std::move(mapUrlToRequest)); // mock async system auto pMockTaskProcessor = std::make_shared(); @@ -570,13 +580,14 @@ TEST_CASE("Test parsing subtree format") { ImplicitTileSubdivisionScheme::Quadtree, maxSubtreeLevels, asyncSystem, - pMockAssetAccessor, spdlog::default_logger(), "test", - {}); + pMockRequest->response(), + CesiumAsync::UrlResponseDataMap{}); asyncSystem.dispatchMainThreadTasks(); - auto parsedSubtree = subtreeFuture.wait(); + SubtreeAvailability::LoadResult loadResult = subtreeFuture.wait(); + std::optional& parsedSubtree = loadResult.first; REQUIRE(parsedSubtree != std::nullopt); for (const auto& tileID : availableTileIDs) { @@ -595,7 +606,6 @@ TEST_CASE("Test parsing subtree format") { CHECK(parsedSubtree->isSubtreeAvailable( libmorton::morton2D_64_encode(subtreeID.x, subtreeID.y))); } - for (const auto& subtreeID : unavailableSubtreeIDs) { CHECK(!parsedSubtree->isSubtreeAvailable( libmorton::morton2D_64_encode(subtreeID.x, subtreeID.y))); diff --git a/Cesium3DTilesReader/include/Cesium3DTilesReader/SubtreeFileReader.h b/Cesium3DTilesReader/include/Cesium3DTilesReader/SubtreeFileReader.h index dc9658019..927f22bbf 100644 --- a/Cesium3DTilesReader/include/Cesium3DTilesReader/SubtreeFileReader.h +++ b/Cesium3DTilesReader/include/Cesium3DTilesReader/SubtreeFileReader.h @@ -57,47 +57,38 @@ class CESIUM3DTILESREADER_API SubtreeFileReader { CesiumAsync::Future> load( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& headers = {}) + const std::string& baseUrl, + const CesiumAsync::IAssetResponse* baseResponse, + const CesiumAsync::UrlResponseDataMap& additionalResponses) const noexcept; CesiumAsync::Future> load( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::shared_ptr& pRequest) - const noexcept; - - CesiumAsync::Future> - load( - const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, + const std::string& baseUrl, + const CesiumAsync::UrlResponseDataMap& additionalResponses, const gsl::span& data) const noexcept; private: CesiumAsync::Future> loadBinary( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, - const gsl::span& data) const noexcept; + const std::string& baseUrl, + const gsl::span& data, + const CesiumAsync::UrlResponseDataMap& additionalResponses) + const noexcept; CesiumAsync::Future> loadJson( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, - const gsl::span& data) const noexcept; + const std::string& baseUrl, + const gsl::span& data, + const CesiumAsync::UrlResponseDataMap& additionalResponses) + const noexcept; CesiumAsync::Future> postprocess( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, + const std::string& baseUrl, + const CesiumAsync::UrlResponseDataMap& additionalResponses, CesiumJsonReader::ReadJsonResult&& loaded) const noexcept; diff --git a/Cesium3DTilesReader/src/SubtreeFileReader.cpp b/Cesium3DTilesReader/src/SubtreeFileReader.cpp index 0d606d19f..755457df7 100644 --- a/Cesium3DTilesReader/src/SubtreeFileReader.cpp +++ b/Cesium3DTilesReader/src/SubtreeFileReader.cpp @@ -19,46 +19,14 @@ SubtreeFileReader::getOptions() const { return this->_reader.getOptions(); } -Future> SubtreeFileReader::load( - const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& headers) const noexcept { - return pAssetAccessor->get(asyncSystem, url, headers) - .thenInWorkerThread([asyncSystem, pAssetAccessor, this]( - std::shared_ptr&& pRequest) { - return this->load(asyncSystem, pAssetAccessor, pRequest); - }); -} - Future> SubtreeFileReader::load( const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::shared_ptr& pRequest) const noexcept { - const IAssetResponse* pResponse = pRequest->response(); - if (pResponse == nullptr) { - ReadJsonResult result; - result.errors.emplace_back("Request failed."); - return asyncSystem.createResolvedFuture(std::move(result)); - } - - uint16_t statusCode = pResponse->statusCode(); - if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - CesiumJsonReader::ReadJsonResult result; - result.errors.emplace_back( - fmt::format("Request failed with status code {}", statusCode)); - return asyncSystem.createResolvedFuture(std::move(result)); - } - - std::vector requestHeaders( - pRequest->headers().begin(), - pRequest->headers().end()); - return this->load( - asyncSystem, - pAssetAccessor, - pRequest->url(), - requestHeaders, - pResponse->data()); + const std::string& baseUrl, + const CesiumAsync::IAssetResponse* baseResponse, + const UrlResponseDataMap& additionalResponses) const noexcept { + assert(baseResponse); + return this + ->load(asyncSystem, baseUrl, additionalResponses, baseResponse->data()); } namespace { @@ -67,10 +35,10 @@ constexpr const char SUBTREE_MAGIC[] = "subt"; Future> SubtreeFileReader::load( const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, + const std::string& baseUrl, + const UrlResponseDataMap& additionalResponses, const gsl::span& data) const noexcept { + if (data.size() < 4) { CesiumJsonReader::ReadJsonResult result; result.errors.emplace_back(fmt::format( @@ -89,11 +57,9 @@ Future> SubtreeFileReader::load( } if (isBinarySubtree) { - return this - ->loadBinary(asyncSystem, pAssetAccessor, url, requestHeaders, data); + return this->loadBinary(asyncSystem, baseUrl, data, additionalResponses); } else { - return this - ->loadJson(asyncSystem, pAssetAccessor, url, requestHeaders, data); + return this->loadJson(asyncSystem, baseUrl, data, additionalResponses); } } @@ -110,10 +76,9 @@ struct SubtreeHeader { Future> SubtreeFileReader::loadBinary( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, - const gsl::span& data) const noexcept { + const std::string& baseUrl, + const gsl::span& data, + const CesiumAsync::UrlResponseDataMap& additionalResponses) const noexcept { if (data.size() < sizeof(SubtreeHeader)) { CesiumJsonReader::ReadJsonResult result; result.errors.emplace_back(fmt::format( @@ -189,24 +154,21 @@ Future> SubtreeFileReader::loadBinary( return postprocess( asyncSystem, - pAssetAccessor, - url, - requestHeaders, + baseUrl, + additionalResponses, std::move(result)); } CesiumAsync::Future> SubtreeFileReader::loadJson( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, - const gsl::span& data) const noexcept { + const std::string& baseUrl, + const gsl::span& data, + const CesiumAsync::UrlResponseDataMap& additionalResponses) const noexcept { ReadJsonResult result = this->_reader.readFromJson(data); return postprocess( asyncSystem, - pAssetAccessor, - url, - requestHeaders, + baseUrl, + additionalResponses, std::move(result)); } @@ -217,85 +179,77 @@ struct RequestedSubtreeBuffer { std::vector data; }; -CesiumAsync::Future requestBuffer( - const std::shared_ptr& pAssetAccessor, - const CesiumAsync::AsyncSystem& asyncSystem, +RequestedSubtreeBuffer requestBuffer( size_t bufferIdx, - std::string&& subtreeUrl, - const std::vector& requestHeaders) { - return pAssetAccessor->get(asyncSystem, subtreeUrl, requestHeaders) - .thenInWorkerThread( - [bufferIdx]( - std::shared_ptr&& pCompletedRequest) { - const CesiumAsync::IAssetResponse* pResponse = - pCompletedRequest->response(); - if (!pResponse) { - return RequestedSubtreeBuffer{bufferIdx, {}}; - } - - uint16_t statusCode = pResponse->statusCode(); - if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - return RequestedSubtreeBuffer{bufferIdx, {}}; - } + uint16_t statusCode, + const gsl::span& data) { + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + return RequestedSubtreeBuffer{bufferIdx, {}}; + } - const gsl::span& data = pResponse->data(); - return RequestedSubtreeBuffer{ - bufferIdx, - std::vector(data.begin(), data.end())}; - }); + return RequestedSubtreeBuffer{ + bufferIdx, + std::vector(data.begin(), data.end())}; } } // namespace Future> SubtreeFileReader::postprocess( const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& url, - const std::vector& requestHeaders, + const std::string& baseUrl, + const CesiumAsync::UrlResponseDataMap& additionalResponses, ReadJsonResult&& loaded) const noexcept { if (!loaded.value) { return asyncSystem.createResolvedFuture(std::move(loaded)); } // Load any external buffers - std::vector> bufferRequests; + std::vector bufferRequests; const std::vector& buffers = loaded.value->buffers; for (size_t i = 0; i < buffers.size(); ++i) { const Buffer& buffer = buffers[i]; if (buffer.uri && !buffer.uri->empty()) { - std::string bufferUrl = CesiumUtility::Uri::resolve(url, *buffer.uri); + std::string bufferUrl = CesiumUtility::Uri::resolve(baseUrl, *buffer.uri); + + // Find this buffer in our responses + auto bufferUrlIt = additionalResponses.find(bufferUrl); + if (bufferUrlIt == additionalResponses.end()) { + // We need to request this buffer + loaded.urlNeeded = bufferUrl; + return asyncSystem.createResolvedFuture(std::move(loaded)); + } + + assert(bufferUrlIt->second.pResponse); + bufferRequests.emplace_back(requestBuffer( - pAssetAccessor, - asyncSystem, i, - std::move(bufferUrl), - requestHeaders)); + bufferUrlIt->second.pResponse->statusCode(), + bufferUrlIt->second.pResponse->data())); } } if (!bufferRequests.empty()) { - return asyncSystem.all(std::move(bufferRequests)) - .thenInMainThread( - [loaded = std::move(loaded)](std::vector&& - completedBuffers) mutable { - for (RequestedSubtreeBuffer& completedBuffer : completedBuffers) { - Buffer& buffer = loaded.value->buffers[completedBuffer.index]; - if (buffer.byteLength > - static_cast(completedBuffer.data.size())) { - loaded.warnings.emplace_back(fmt::format( - "Buffer byteLength ({}) is greater than the size of the " - "downloaded resource ({} bytes). The byteLength will be " - "updated to match.", - buffer.byteLength, - completedBuffer.data.size())); - buffer.byteLength = - static_cast(completedBuffer.data.size()); - } - buffer.cesium.data = std::move(completedBuffer.data); - } - - return std::move(loaded); - }); + return asyncSystem.runInMainThread( + [loaded = std::move(loaded), + completedBuffers = std::move(bufferRequests)]() mutable { + for (RequestedSubtreeBuffer& completedBuffer : completedBuffers) { + Buffer& buffer = loaded.value->buffers[completedBuffer.index]; + if (buffer.byteLength > + static_cast(completedBuffer.data.size())) { + loaded.warnings.emplace_back(fmt::format( + "Buffer byteLength ({}) is greater than the size of the " + "downloaded resource ({} bytes). The byteLength will be " + "updated to match.", + buffer.byteLength, + completedBuffer.data.size())); + buffer.byteLength = + static_cast(completedBuffer.data.size()); + } + buffer.cesium.data = std::move(completedBuffer.data); + } + + return std::move(loaded); + }); } return asyncSystem.createResolvedFuture(std::move(loaded)); diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/RasterMappedTo3DTile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/RasterMappedTo3DTile.h index 7845c983e..a91b41f12 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/RasterMappedTo3DTile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/RasterMappedTo3DTile.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -177,17 +178,6 @@ class RasterMappedTo3DTile final { IPrepareRendererResources& prepareRendererResources, Tile& tile) noexcept; - /** - * @brief Does a throttled load of the mapped {@link RasterOverlayTile}. - * - * @return If the mapped tile is already in the process of loading or it has - * already finished loading, this method does nothing and returns true. If too - * many loads are already in progress, this method does nothing and returns - * false. Otherwise, it begins the asynchronous process to load the tile and - * returns true. - */ - bool loadThrottled() noexcept; - /** * @brief Creates a maping between a {@link RasterOverlay} and a {@link Tile}. * diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileLoadResult.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileLoadResult.h index d3eacf4e9..37d5daf7c 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileLoadResult.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileLoadResult.h @@ -3,6 +3,7 @@ #include "BoundingVolume.h" #include "TileContent.h" +#include #include #include #include @@ -62,7 +63,12 @@ enum class TileLoadResultState { * background work happenning and * __none__ of the fields in {@link TileLoadResult} or {@link TileChildrenResult} are applied to the tile */ - RetryLater + RetryLater, + + /** + * @brief The operation requires the client to make another content request + */ + RequestRequired }; /** @@ -104,7 +110,7 @@ struct CESIUM3DTILESSELECTION_API TileLoadResult { /** * @brief The request that is created to download the tile content. */ - std::shared_ptr pCompletedRequest; + std::string originalRequestUrl; /** * @brief A callback that is invoked in the main thread immediately when the @@ -114,6 +120,11 @@ struct CESIUM3DTILESSELECTION_API TileLoadResult { */ std::function tileInitializer; + /** + * @brief Optional additional request needed + */ + CesiumAsync::RequestData additionalRequestData; + /** * @brief The result of loading a tile. Note that if the state is Failed or * RetryLater, __none__ of the fields above (including {@link TileLoadResult::tileInitializer}) will be @@ -124,18 +135,21 @@ struct CESIUM3DTILESSELECTION_API TileLoadResult { /** * @brief Create a result with Failed state * - * @param pCompletedRequest The failed request */ - static TileLoadResult createFailedResult( - std::shared_ptr pCompletedRequest); + static TileLoadResult createFailedResult(); /** * @brief Create a result with RetryLater state * - * @param pCompletedRequest The failed request */ - static TileLoadResult createRetryLaterResult( - std::shared_ptr pCompletedRequest); + static TileLoadResult createRetryLaterResult(); + + /** + * @brief Create a result with RequestRequired state + * + */ + static TileLoadResult + createRequestResult(const CesiumAsync::RequestData& request); }; } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileWorkManager.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileWorkManager.h new file mode 100644 index 000000000..62439c4ee --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileWorkManager.h @@ -0,0 +1,188 @@ +#pragma once + +#include "Tile.h" +#include "TilesetContentLoader.h" + +namespace Cesium3DTilesSelection { +class TilesetMetadata; + +struct TileProcessingData { + Tile* pTile = nullptr; + TileLoaderCallback loaderCallback = {}; + std::vector projections{}; + TilesetContentOptions contentOptions = {}; + std::any rendererOptions = {}; +}; + +struct RasterProcessingData { + RasterMappedTo3DTile* pRasterTile = nullptr; + CesiumRasterOverlays::RasterProcessingCallback rasterCallback = {}; +}; + +class TileWorkManager { + +public: + TileWorkManager( + CesiumAsync::AsyncSystem asyncSystem, + std::shared_ptr pAssetAccessor, + std::shared_ptr pLogger) + : _asyncSystem(asyncSystem), + _pAssetAccessor(pAssetAccessor), + _pLogger(pLogger) {} + ~TileWorkManager() noexcept; + + using ProcessingData = std::variant; + + struct Order { + CesiumAsync::RequestData requestData = {}; + + ProcessingData processingData = {}; + + TileLoadPriorityGroup group = TileLoadPriorityGroup::Normal; + double priority = 0; + + std::vector childOrders = {}; + + bool operator<(const Order& rhs) const noexcept { + if (this->group == rhs.group) + return this->priority < rhs.priority; + else + return this->group > rhs.group; + } + }; + + using TileSource = std::variant; + + struct Work { + TileSource uniqueId = {}; + + Order order = {}; + + std::vector pendingRequests = {}; + CesiumAsync::UrlAssetRequestMap completedRequests = {}; + + TileLoadResult tileLoadResult = {}; + void* pRenderResources = nullptr; + + void fillResponseDataMap(CesiumAsync::UrlResponseDataMap& responseDataMap) { + for (auto& pair : completedRequests) { + responseDataMap.emplace( + pair.first, + CesiumAsync::ResponseData{ + pair.second.get(), + pair.second->response()}); + } + } + + CesiumAsync::RequestData* getNextRequest() { + // Next request always comes from the back + // Order isn't important here + if (pendingRequests.empty()) { + return nullptr; + } else { + assert(!pendingRequests.back().url.empty()); + return &pendingRequests.back(); + } + } + }; + + static void TryAddOrders( + std::shared_ptr& thiz, + std::vector& orders, + size_t maxSimultaneousRequests, + std::vector& workCreated); + + static void + RequeueWorkForRequest(std::shared_ptr& thiz, Work* work); + + struct DoneOrder { + TileLoadResult loadResult = {}; + void* pRenderResources = nullptr; + Order order = {}; + }; + + struct FailedOrder { + std::string failureReason = ""; + Order order = {}; + }; + + void TakeCompletedWork( + std::vector& outCompleted, + std::vector& outFailed); + + void SignalWorkComplete(Work* work); + + static void TryDispatchProcessing(std::shared_ptr& thiz); + + void GetPendingCount(size_t& pendingRequests, size_t& pendingProcessing); + size_t GetActiveWorkCount(); + + void GetLoadingWorkStats( + size_t& requestCount, + size_t& inFlightCount, + size_t& processingCount, + size_t& failedCount); + + void Shutdown(); + + using TileDispatchFunc = std::function; + + using RasterDispatchFunc = std::function; + + void SetDispatchFunctions( + TileDispatchFunc& tileDispatch, + RasterDispatchFunc& rasterDispatch); + +private: + static void throttleOrders( + size_t existingCount, + size_t maxCount, + std::vector& inOutOrders); + + static void transitionRequests(std::shared_ptr& thiz); + + static void transitionProcessing(std::shared_ptr& thiz); + + void onRequestFinished( + std::shared_ptr& pCompletedRequest); + + void stageWork(Work* pWork); + + Work* createWorkFromOrder(Order* pOrder); + + void ordersToWork( + const std::vector& orders, + std::vector& instancesCreated); + + std::mutex _requestsLock; + + bool _shutdownSignaled = false; + + std::map _ownedWork; + + std::vector _requestsPending; + std::map> _requestsInFlight; + + std::vector _processingPending; + std::map _processingInFlight; + + using FailedWorkPair = std::pair; + std::vector _failedWork; + std::vector _doneWork; + + TileDispatchFunc _tileDispatchFunc; + RasterDispatchFunc _rasterDispatchFunc; + + CesiumAsync::AsyncSystem _asyncSystem; + std::shared_ptr _pAssetAccessor; + std::shared_ptr _pLogger; + size_t _maxSimultaneousRequests; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index f8bb44fb7..66c4d4c8a 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -422,6 +422,8 @@ class CESIUM3DTILESSELECTION_API Tileset final { float deltaTime, ViewUpdateResult& result) const noexcept; + void _logLoadingWorkStats(const std::string& prefix); + TilesetExternals _externals; CesiumAsync::AsyncSystem _asyncSystem; @@ -430,59 +432,8 @@ class CESIUM3DTILESSELECTION_API Tileset final { int32_t _previousFrameNumber; ViewUpdateResult _updateResult; - enum class TileLoadPriorityGroup { - /** - * @brief Low priority tiles that aren't needed right now, but - * are being preloaded for the future. - */ - Preload = 0, - - /** - * @brief Medium priority tiles that are needed to render the current view - * the appropriate level-of-detail. - */ - Normal = 1, - - /** - * @brief High priority tiles that are causing extra detail to be rendered - * in the scene, potentially creating a performance problem and aliasing - * artifacts. - */ - Urgent = 2 - }; - - struct TileLoadTask { - /** - * @brief The tile to be loaded. - */ - Tile* pTile; - - /** - * @brief The priority group (low / medium / high) in which to load this - * tile. - * - * All tiles in a higher priority group are given a chance to load before - * any tiles in a lower priority group. - */ - TileLoadPriorityGroup group; - - /** - * @brief The priority of this tile within its priority group. - * - * Tiles with a _lower_ value for this property load sooner! - */ - double priority; - - bool operator<(const TileLoadTask& rhs) const noexcept { - if (this->group == rhs.group) - return this->priority < rhs.priority; - else - return this->group > rhs.group; - } - }; - - std::vector _mainThreadLoadQueue; - std::vector _workerThreadLoadQueue; + std::vector _mainThreadLoadQueue; + std::vector _workerThreadLoadQueue; Tile::LoadedLinkedList _loadedTiles; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetContentLoader.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetContentLoader.h index 5564cc22e..9decba22f 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetContentLoader.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetContentLoader.h @@ -21,7 +21,58 @@ #include namespace Cesium3DTilesSelection { -class Tile; +class TilesetContentLoader; + +enum class TileLoadPriorityGroup { + /** + * @brief Low priority tiles that aren't needed right now, but + * are being preloaded for the future. + */ + Preload = 0, + + /** + * @brief Medium priority tiles that are needed to render the current view + * the appropriate level-of-detail. + */ + Normal = 1, + + /** + * @brief High priority tiles that are causing extra detail to be rendered + * in the scene, potentially creating a performance problem and aliasing + * artifacts. + */ + Urgent = 2 +}; + +struct TileLoadRequest { + /** + * @brief The tile to be loaded. + */ + Tile* pTile = nullptr; + + /** + * @brief The priority group (low / medium / high) in which to load this + * tile. + * + * All tiles in a higher priority group are given a chance to load before + * any tiles in a lower priority group. + */ + TileLoadPriorityGroup group = TileLoadPriorityGroup::Normal; + + /** + * @brief The priority of this tile within its priority group. + * + * Tiles with a _lower_ value for this property load sooner! + */ + double priority = 0; + + bool operator<(const TileLoadRequest& rhs) const noexcept { + if (this->group == rhs.group) + return this->priority < rhs.priority; + else + return this->group > rhs.group; + } +}; /** * @brief Store the parameters that are needed to load a tile @@ -33,18 +84,16 @@ struct CESIUM3DTILESSELECTION_API TileLoadInput { * @param tile The {@link Tile} that the content belongs to. * @param contentOptions The content options the {@link TilesetContentLoader} will use to process the content of the tile. * @param asyncSystem The async system to use for tile content loading. - * @param pAssetAccessor The asset accessor to make further requests with. * @param pLogger The logger that will be used - * @param requestHeaders The request headers that will be attached to the + * @param UrlResponseDataMap Content responses fetched by the caller * request. */ TileLoadInput( const Tile& tile, const TilesetContentOptions& contentOptions, const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, const std::shared_ptr& pLogger, - const std::vector& requestHeaders); + const CesiumAsync::UrlResponseDataMap& responsesByUrl); /** * @brief The tile that the {@link TilesetContentLoader} will request the server for the content. @@ -61,21 +110,15 @@ struct CESIUM3DTILESSELECTION_API TileLoadInput { */ const CesiumAsync::AsyncSystem& asyncSystem; - /** - * @brief The asset accessor to make requests for the tile content over the - * wire. - */ - const std::shared_ptr& pAssetAccessor; - /** * @brief The logger that receives details of loading errors and warnings. */ const std::shared_ptr& pLogger; /** - * @brief The request headers that will be attached to the request. + * @brief Response data provided by the caller, stored by url */ - const std::vector& requestHeaders; + const CesiumAsync::UrlResponseDataMap& responsesByUrl; }; /** @@ -101,6 +144,10 @@ struct CESIUM3DTILESSELECTION_API TileChildrenResult { TileLoadResultState state; }; +using TileLoaderCallback = std::function( + const TileLoadInput& loadInput, + TilesetContentLoader*)>; + /** * @brief The loader interface to load the tile content */ @@ -120,6 +167,11 @@ class CESIUM3DTILESSELECTION_API TilesetContentLoader { virtual CesiumAsync::Future loadTileContent(const TileLoadInput& input) = 0; + virtual bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) = 0; + /** * @brief Create the tile's children. * diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetOptions.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetOptions.h index e702e711f..2fce1c6ef 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetOptions.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetOptions.h @@ -98,13 +98,7 @@ struct CESIUM3DTILESSELECTION_API TilesetOptions { * @brief The maximum number of tiles that may simultaneously be in the * process of loading. */ - uint32_t maximumSimultaneousTileLoads = 20; - - /** - * @brief The maximum number of subtrees that may simultaneously be in the - * process of loading. - */ - uint32_t maximumSimultaneousSubtreeLoads = 20; + uint32_t maximumSimultaneousTileLoads = 24; /** * @brief Indicates whether the ancestors of rendered tiles should be diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ViewUpdateResult.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ViewUpdateResult.h index 70c9034d5..bddfeb306 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ViewUpdateResult.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/ViewUpdateResult.h @@ -39,17 +39,11 @@ class CESIUM3DTILESSELECTION_API ViewUpdateResult final { */ std::unordered_set tilesFadingOut; - /** - * @brief The number of tiles in the worker thread load queue. - */ - int32_t workerThreadTileLoadQueueLength = 0; - - /** - * @brief The number of tiles in the main thread load queue. - */ - int32_t mainThreadTileLoadQueueLength = 0; - //! @cond Doxygen_Suppress + size_t workerThreadTileLoadQueueLength = 0; + size_t mainThreadTileLoadQueueLength = 0; + size_t mainThreadTotalTileLoads = 0; + uint32_t tilesVisited = 0; uint32_t culledTilesVisited = 0; uint32_t tilesCulled = 0; @@ -57,6 +51,31 @@ class CESIUM3DTILESSELECTION_API ViewUpdateResult final { uint32_t tilesWaitingForOcclusionResults = 0; uint32_t tilesKicked = 0; uint32_t maxDepthVisited = 0; + + uint32_t tilesLoading = 0; + uint32_t tilesLoaded = 0; + uint32_t rastersLoading = 0; + uint32_t rastersLoaded = 0; + size_t activeWorkCount = 0; + + void resetStats() { + workerThreadTileLoadQueueLength = 0; + mainThreadTileLoadQueueLength = 0; + + tilesVisited = 0; + culledTilesVisited = 0; + tilesCulled = 0; + tilesOccluded = 0; + tilesWaitingForOcclusionResults = 0; + tilesKicked = 0; + maxDepthVisited = 0; + + tilesLoading = 0; + tilesLoaded = 0; + rastersLoading = 0; + rastersLoaded = 0; + activeWorkCount = 0; + } //! @endcond int32_t frameNumber = 0; diff --git a/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.cpp b/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.cpp index 5ea6e2bae..578f37a56 100644 --- a/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.cpp +++ b/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.cpp @@ -48,9 +48,8 @@ std::string createEndpointResource( * @return The access token if successful */ std::optional getNewAccessToken( - const CesiumAsync::IAssetResponse* pIonResponse, + const gsl::span& data, const std::shared_ptr& pLogger) { - const gsl::span data = pIonResponse->data(); rapidjson::Document ionResponse; ionResponse.Parse(reinterpret_cast(data.data()), data.size()); if (ionResponse.HasParseError()) { @@ -352,43 +351,85 @@ CesiumIonTilesetLoader::CesiumIonTilesetLoader( CesiumAsync::Future CesiumIonTilesetLoader::loadTileContent(const TileLoadInput& loadInput) { - if (this->_refreshTokenState == TokenRefreshState::Loading) { + + // For all responses, determine if our token needs a refresh + // 401 - Unauthorized response + bool staleTokenDetected = false; + for (auto responseData : loadInput.responsesByUrl) { + if (responseData.second.pResponse->statusCode() == 401) { + staleTokenDetected = true; + break; + } + } + + if (staleTokenDetected) { + // + // Queue up a token refresh if one isn't already in progress + // + // TODO: the way this is structured, requests already in progress + // with the old key might complete after the key has been updated, + // and there's nothing here clever enough to avoid refreshing the + // key _again_ in that instance. + // + bool refreshInProgress = + this->_refreshTokenState == TokenRefreshState::Loading; + if (!refreshInProgress) { + this->_refreshTokenState = TokenRefreshState::Loading; + + std::string url = createEndpointResource( + this->_ionAssetID, + this->_ionAccessToken, + this->_ionAssetEndpointUrl); + + return loadInput.asyncSystem.createResolvedFuture( + TileLoadResult::createRequestResult( + CesiumAsync::RequestData{std::move(url), {}})); + } + + // Let this tile retry later return loadInput.asyncSystem.createResolvedFuture( - TileLoadResult::createRetryLaterResult(nullptr)); - } else if (this->_refreshTokenState == TokenRefreshState::Failed) { + TileLoadResult::createRetryLaterResult()); + } + + // No stale tokens. If we are refreshing, our new token has arrived + if (this->_refreshTokenState == TokenRefreshState::Loading) { + assert(loadInput.responsesByUrl.size() == 1); + const std::string& requestUrl = loadInput.responsesByUrl.begin()->first; + const CesiumAsync::IAssetResponse* response = + loadInput.responsesByUrl.begin()->second.pResponse; + + this->refreshToken( + loadInput.pLogger, + requestUrl, + response->statusCode(), + response->data()); + return loadInput.asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createRetryLaterResult()); } - const auto& asyncSystem = loadInput.asyncSystem; - const auto& pAssetAccessor = loadInput.pAssetAccessor; - const auto& pLogger = loadInput.pLogger; - - // TODO: the way this is structured, requests already in progress - // with the old key might complete after the key has been updated, - // and there's nothing here clever enough to avoid refreshing the - // key _again_ in that instance. - auto refreshTokenInMainThread = - [this, pLogger, pAssetAccessor, asyncSystem]() { - this->refreshTokenInMainThread(pLogger, pAssetAccessor, asyncSystem); - }; - - return this->_pAggregatedLoader->loadTileContent(loadInput).thenImmediately( - [asyncSystem, - refreshTokenInMainThread = std::move(refreshTokenInMainThread)]( - TileLoadResult&& result) mutable { - // check to see if we need to refresh token - if (result.pCompletedRequest) { - auto response = result.pCompletedRequest->response(); - if (response->statusCode() == 401) { - // retry later - result.state = TileLoadResultState::RetryLater; - asyncSystem.runInMainThread(std::move(refreshTokenInMainThread)); - } - } + // If our token has failed to refresh + if (this->_refreshTokenState == TokenRefreshState::Failed) + return loadInput.asyncSystem.createResolvedFuture( + TileLoadResult::createRetryLaterResult()); - return std::move(result); - }); + assert( + this->_refreshTokenState == TokenRefreshState::None || + this->_refreshTokenState == TokenRefreshState::Done); + + return this->_pAggregatedLoader->loadTileContent(loadInput); +} + +bool CesiumIonTilesetLoader::getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) { + + // If token refresh is in progress, cannot queue work yet + if (this->_refreshTokenState == TokenRefreshState::Loading) + return false; + + return this->_pAggregatedLoader->getLoadWork(pTile, outRequest, outCallback); } TileChildrenResult @@ -397,53 +438,37 @@ CesiumIonTilesetLoader::createTileChildren(const Tile& tile) { return pLoader->createTileChildren(tile); } -void CesiumIonTilesetLoader::refreshTokenInMainThread( +void CesiumIonTilesetLoader::refreshToken( const std::shared_ptr& pLogger, - const std::shared_ptr& pAssetAccessor, - const CesiumAsync::AsyncSystem& asyncSystem) { - if (this->_refreshTokenState == TokenRefreshState::Loading) { + const std::string& requestUrl, + const uint16_t responseStatusCode, + const gsl::span& responseData) { + + assert(this->_refreshTokenState == TokenRefreshState::Loading); + + if (responseData.empty()) { + this->_refreshTokenState = TokenRefreshState::Failed; return; } - this->_refreshTokenState = TokenRefreshState::Loading; - - std::string url = createEndpointResource( - this->_ionAssetID, - this->_ionAccessToken, - this->_ionAssetEndpointUrl); - pAssetAccessor->get(asyncSystem, url) - .thenInMainThread( - [this, - pLogger](std::shared_ptr&& pIonRequest) { - const CesiumAsync::IAssetResponse* pIonResponse = - pIonRequest->response(); - - if (!pIonResponse) { - this->_refreshTokenState = TokenRefreshState::Failed; - return; - } - - uint16_t statusCode = pIonResponse->statusCode(); - if (statusCode >= 200 && statusCode < 300) { - auto accessToken = getNewAccessToken(pIonResponse, pLogger); - if (accessToken) { - this->_headerChangeListener( - "Authorization", - "Bearer " + *accessToken); - - // update cache with new access token - auto cacheIt = endpointCache.find(pIonRequest->url()); - if (cacheIt != endpointCache.end()) { - cacheIt->second.accessToken = accessToken.value(); - } - - this->_refreshTokenState = TokenRefreshState::Done; - return; - } - } - - this->_refreshTokenState = TokenRefreshState::Failed; - }); + uint16_t statusCode = responseStatusCode; + if (statusCode >= 200 && statusCode < 300) { + auto accessToken = getNewAccessToken(responseData, pLogger); + if (accessToken) { + this->_headerChangeListener("Authorization", "Bearer " + *accessToken); + + // update cache with new access token + auto cacheIt = endpointCache.find(requestUrl); + if (cacheIt != endpointCache.end()) { + cacheIt->second.accessToken = accessToken.value(); + } + + this->_refreshTokenState = TokenRefreshState::Done; + return; + } + } + + this->_refreshTokenState = TokenRefreshState::Failed; } CesiumAsync::Future> diff --git a/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.h b/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.h index f8c022b8f..ada347085 100644 --- a/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.h +++ b/Cesium3DTilesSelection/src/CesiumIonTilesetLoader.h @@ -26,6 +26,11 @@ class CesiumIonTilesetLoader : public TilesetContentLoader { CesiumAsync::Future loadTileContent(const TileLoadInput& loadInput) override; + bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) override; + TileChildrenResult createTileChildren(const Tile& tile) override; static CesiumAsync::Future> @@ -50,10 +55,11 @@ class CesiumIonTilesetLoader : public TilesetContentLoader { TilesetContentLoaderResult&& result); private: - void refreshTokenInMainThread( + void refreshToken( const std::shared_ptr& pLogger, - const std::shared_ptr& pAssetAccessor, - const CesiumAsync::AsyncSystem& asyncSystem); + const std::string& requestUrl, + const uint16_t responseStatusCode, + const gsl::span& responseData); TokenRefreshState _refreshTokenState; int64_t _ionAssetID; diff --git a/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp b/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp index d6f9e1c94..1d7e6435b 100644 --- a/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp +++ b/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp @@ -100,76 +100,49 @@ std::vector populateSubtree( CesiumAsync::Future requestTileContent( const std::shared_ptr& pLogger, const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, const std::string& tileUrl, - const std::vector& requestHeaders, + const gsl::span& responseData, CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets, bool applyTextureTransform) { - return pAssetAccessor->get(asyncSystem, tileUrl, requestHeaders) - .thenInWorkerThread([pLogger, - ktx2TranscodeTargets, - applyTextureTransform]( - std::shared_ptr&& - pCompletedRequest) mutable { - const CesiumAsync::IAssetResponse* pResponse = - pCompletedRequest->response(); - const std::string& tileUrl = pCompletedRequest->url(); - if (!pResponse) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Did not receive a valid response for tile content {}", - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - uint16_t statusCode = pResponse->statusCode(); - if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Received status code {} for tile content {}", - statusCode, - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } + return asyncSystem.runInWorkerThread([pLogger, + ktx2TranscodeTargets, + applyTextureTransform, + tileUrl = tileUrl, + responseData = responseData]() mutable { + // find gltf converter + auto converter = GltfConverters::getConverterByMagic(responseData); + if (!converter) { + converter = GltfConverters::getConverterByFileExtension(tileUrl); + } - // find gltf converter - const auto& responseData = pResponse->data(); - auto converter = GltfConverters::getConverterByMagic(responseData); - if (!converter) { - converter = GltfConverters::getConverterByFileExtension( - pCompletedRequest->url()); - } + if (converter) { + // Convert to gltf + CesiumGltfReader::GltfReaderOptions gltfOptions; + gltfOptions.ktx2TranscodeTargets = ktx2TranscodeTargets; + gltfOptions.applyTextureTransform = applyTextureTransform; + GltfConverterResult result = converter(responseData, gltfOptions); + + // Report any errors if there are any + logTileLoadResult(pLogger, tileUrl, result.errors); + if (result.errors || !result.model) { + return TileLoadResult::createFailedResult(); + } - if (converter) { - // Convert to gltf - CesiumGltfReader::GltfReaderOptions gltfOptions; - gltfOptions.ktx2TranscodeTargets = ktx2TranscodeTargets; - gltfOptions.applyTextureTransform = applyTextureTransform; - GltfConverterResult result = converter(responseData, gltfOptions); - - // Report any errors if there are any - logTileLoadResult(pLogger, tileUrl, result.errors); - if (result.errors || !result.model) { - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - return TileLoadResult{ - std::move(*result.model), - CesiumGeometry::Axis::Y, - std::nullopt, - std::nullopt, - std::nullopt, - std::move(pCompletedRequest), - {}, - TileLoadResultState::Success}; - } + return TileLoadResult{ + std::move(*result.model), + CesiumGeometry::Axis::Y, + std::nullopt, + std::nullopt, + std::nullopt, + tileUrl, + {}, + CesiumAsync::RequestData{}, + TileLoadResultState::Success}; + } - // content type is not supported - return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); - }); + // content type is not supported + return TileLoadResult::createFailedResult(); + }); } } // namespace @@ -177,17 +150,16 @@ CesiumAsync::Future ImplicitOctreeLoader::loadTileContent(const TileLoadInput& loadInput) { const auto& tile = loadInput.tile; const auto& asyncSystem = loadInput.asyncSystem; - const auto& pAssetAccessor = loadInput.pAssetAccessor; const auto& pLogger = loadInput.pLogger; - const auto& requestHeaders = loadInput.requestHeaders; const auto& contentOptions = loadInput.contentOptions; + const auto& responsesByUrl = loadInput.responsesByUrl; // make sure the tile is a octree tile const CesiumGeometry::OctreeTileID* pOctreeID = std::get_if(&tile.getTileID()); if (!pOctreeID) { return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // find the subtree ID @@ -197,8 +169,8 @@ ImplicitOctreeLoader::loadTileContent(const TileLoadInput& loadInput) { *pOctreeID); uint32_t subtreeLevelIdx = subtreeID.level / this->_subtreeLevels; if (subtreeLevelIdx >= _loadedSubtrees.size()) { - return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); } uint64_t subtreeMortonIdx = @@ -206,33 +178,60 @@ ImplicitOctreeLoader::loadTileContent(const TileLoadInput& loadInput) { auto subtreeIt = this->_loadedSubtrees[subtreeLevelIdx].find(subtreeMortonIdx); if (subtreeIt == this->_loadedSubtrees[subtreeLevelIdx].end()) { - // subtree is not loaded, so load it now. std::string subtreeUrl = ImplicitTilingUtilities::resolveUrl( this->_baseUrl, this->_subtreeUrlTemplate, subtreeID); + + // If subtree url is not loaded, request it and come back later + auto foundIt = responsesByUrl.find(subtreeUrl); + if (foundIt == responsesByUrl.end()) { + return asyncSystem.createResolvedFuture( + TileLoadResult::createRequestResult( + CesiumAsync::RequestData{subtreeUrl, {}})); + } + + auto baseResponse = foundIt->second.pResponse; + + uint16_t statusCode = baseResponse->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + subtreeUrl); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); + } + return SubtreeAvailability::loadSubtree( ImplicitTileSubdivisionScheme::Octree, this->_subtreeLevels, asyncSystem, - pAssetAccessor, pLogger, subtreeUrl, - requestHeaders) - .thenInMainThread([this, subtreeID](std::optional&& - subtreeAvailability) mutable { - if (subtreeAvailability) { - this->addSubtreeAvailability( - subtreeID, - std::move(*subtreeAvailability)); - - // tell client to retry later - return TileLoadResult::createRetryLaterResult(nullptr); - } else { - // Subtree load failed, so this tile fails, too. - return TileLoadResult::createFailedResult(nullptr); - } - }); + baseResponse, + responsesByUrl) + .thenInMainThread( + [this, + subtreeID](SubtreeAvailability::LoadResult&& loadResult) mutable { + if (loadResult.first) { + // Availability success + this->addSubtreeAvailability( + subtreeID, + std::move(*loadResult.first)); + + // tell client to retry later + return TileLoadResult::createRetryLaterResult(); + } else if (!loadResult.second.url.empty()) { + // No availability, but a url was requested + // Let this work go back into the request queue + return TileLoadResult::createRequestResult(loadResult.second); + } else { + // Subtree load failed, so this tile fails, too. + return TileLoadResult::createFailedResult(); + } + }); } // subtree is available, so check if tile has content or not. If it has, then @@ -245,8 +244,9 @@ ImplicitOctreeLoader::loadTileContent(const TileLoadInput& loadInput) { std::nullopt, std::nullopt, std::nullopt, - nullptr, + std::string(), {}, + CesiumAsync::RequestData{}, TileLoadResultState::Success}); } @@ -254,16 +254,50 @@ ImplicitOctreeLoader::loadTileContent(const TileLoadInput& loadInput) { this->_baseUrl, this->_contentUrlTemplate, *pOctreeID); + + // If tile url is not loaded, request it and come back later + auto foundIt = responsesByUrl.find(tileUrl); + if (foundIt == responsesByUrl.end()) { + return asyncSystem.createResolvedFuture( + TileLoadResult::createRequestResult( + CesiumAsync::RequestData{tileUrl, {}})); + } + + const CesiumAsync::ResponseData& responseData = foundIt->second; + assert(responseData.pResponse); + uint16_t statusCode = responseData.pResponse->statusCode(); + assert(statusCode != 0); + if (statusCode < 200 || statusCode >= 300) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + tileUrl); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); + } + return requestTileContent( pLogger, asyncSystem, - pAssetAccessor, tileUrl, - requestHeaders, + foundIt->second.pResponse->data(), contentOptions.ktx2TranscodeTargets, contentOptions.applyTextureTransform); } +bool ImplicitOctreeLoader::getLoadWork( + const Tile*, + CesiumAsync::RequestData&, + TileLoaderCallback& outCallback) { + // loadTileContent will control request / processing flow + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + return true; +} + TileChildrenResult ImplicitOctreeLoader::createTileChildren(const Tile& tile) { const CesiumGeometry::OctreeTileID* pOctreeID = std::get_if(&tile.getTileID()); diff --git a/Cesium3DTilesSelection/src/ImplicitOctreeLoader.h b/Cesium3DTilesSelection/src/ImplicitOctreeLoader.h index c49c59f95..f61b08ee6 100644 --- a/Cesium3DTilesSelection/src/ImplicitOctreeLoader.h +++ b/Cesium3DTilesSelection/src/ImplicitOctreeLoader.h @@ -40,6 +40,11 @@ class ImplicitOctreeLoader : public TilesetContentLoader { CesiumAsync::Future loadTileContent(const TileLoadInput& loadInput) override; + bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) override; + TileChildrenResult createTileChildren(const Tile& tile) override; uint32_t getSubtreeLevels() const noexcept; diff --git a/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp b/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp index 0432aa53d..91bec2238 100644 --- a/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp +++ b/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp @@ -110,76 +110,49 @@ std::vector populateSubtree( CesiumAsync::Future requestTileContent( const std::shared_ptr& pLogger, const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, const std::string& tileUrl, - const std::vector& requestHeaders, + const gsl::span& responseData, CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets, bool applyTextureTransform) { - return pAssetAccessor->get(asyncSystem, tileUrl, requestHeaders) - .thenInWorkerThread([pLogger, - ktx2TranscodeTargets, - applyTextureTransform]( - std::shared_ptr&& - pCompletedRequest) mutable { - const CesiumAsync::IAssetResponse* pResponse = - pCompletedRequest->response(); - const std::string& tileUrl = pCompletedRequest->url(); - if (!pResponse) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Did not receive a valid response for tile content {}", - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - uint16_t statusCode = pResponse->statusCode(); - if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Received status code {} for tile content {}", - statusCode, - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } + return asyncSystem.runInWorkerThread([pLogger, + ktx2TranscodeTargets, + applyTextureTransform, + tileUrl = tileUrl, + responseData = responseData]() mutable { + // find gltf converter + auto converter = GltfConverters::getConverterByMagic(responseData); + if (!converter) { + converter = GltfConverters::getConverterByFileExtension(tileUrl); + } - // find gltf converter - const auto& responseData = pResponse->data(); - auto converter = GltfConverters::getConverterByMagic(responseData); - if (!converter) { - converter = GltfConverters::getConverterByFileExtension( - pCompletedRequest->url()); - } + if (converter) { + // Convert to gltf + CesiumGltfReader::GltfReaderOptions gltfOptions; + gltfOptions.ktx2TranscodeTargets = ktx2TranscodeTargets; + gltfOptions.applyTextureTransform = applyTextureTransform; + GltfConverterResult result = converter(responseData, gltfOptions); + + // Report any errors if there are any + logTileLoadResult(pLogger, tileUrl, result.errors); + if (result.errors || !result.model) { + return TileLoadResult::createFailedResult(); + } - if (converter) { - // Convert to gltf - CesiumGltfReader::GltfReaderOptions gltfOptions; - gltfOptions.ktx2TranscodeTargets = ktx2TranscodeTargets; - gltfOptions.applyTextureTransform = applyTextureTransform; - GltfConverterResult result = converter(responseData, gltfOptions); - - // Report any errors if there are any - logTileLoadResult(pLogger, tileUrl, result.errors); - if (result.errors || !result.model) { - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - return TileLoadResult{ - std::move(*result.model), - CesiumGeometry::Axis::Y, - std::nullopt, - std::nullopt, - std::nullopt, - std::move(pCompletedRequest), - {}, - TileLoadResultState::Success}; - } + return TileLoadResult{ + std::move(*result.model), + CesiumGeometry::Axis::Y, + std::nullopt, + std::nullopt, + std::nullopt, + tileUrl, + {}, + CesiumAsync::RequestData{}, + TileLoadResultState::Success}; + } - // content type is not supported - return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); - }); + // content type is not supported + return TileLoadResult::createFailedResult(); + }); } } // namespace @@ -187,10 +160,9 @@ CesiumAsync::Future ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { const auto& tile = loadInput.tile; const auto& asyncSystem = loadInput.asyncSystem; - const auto& pAssetAccessor = loadInput.pAssetAccessor; const auto& pLogger = loadInput.pLogger; - const auto& requestHeaders = loadInput.requestHeaders; const auto& contentOptions = loadInput.contentOptions; + const auto& responsesByUrl = loadInput.responsesByUrl; // Ensure CesiumGeometry::QuadtreeTileID only has 32-bit components. There are // solutions below if the ID has more than 32-bit components. @@ -215,7 +187,7 @@ ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { std::get_if(&tile.getTileID()); if (!pQuadtreeID) { return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // find the subtree ID @@ -226,7 +198,7 @@ ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { uint32_t subtreeLevelIdx = subtreeID.level / this->_subtreeLevels; if (subtreeLevelIdx >= _loadedSubtrees.size()) { return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // the below morton index hash to the subtree assumes that tileID's components @@ -245,33 +217,60 @@ ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { auto subtreeIt = this->_loadedSubtrees[subtreeLevelIdx].find(subtreeMortonIdx); if (subtreeIt == this->_loadedSubtrees[subtreeLevelIdx].end()) { - // subtree is not loaded, so load it now. std::string subtreeUrl = ImplicitTilingUtilities::resolveUrl( this->_baseUrl, this->_subtreeUrlTemplate, subtreeID); + + // If subtree url is not loaded, request it and come back later + auto foundIt = responsesByUrl.find(subtreeUrl); + if (foundIt == responsesByUrl.end()) { + return asyncSystem.createResolvedFuture( + TileLoadResult::createRequestResult( + CesiumAsync::RequestData{subtreeUrl, {}})); + } + + auto baseResponse = foundIt->second.pResponse; + + uint16_t statusCode = baseResponse->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + subtreeUrl); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); + } + return SubtreeAvailability::loadSubtree( ImplicitTileSubdivisionScheme::Quadtree, this->_subtreeLevels, asyncSystem, - pAssetAccessor, pLogger, subtreeUrl, - requestHeaders) - .thenInMainThread([this, subtreeID](std::optional&& - subtreeAvailability) mutable { - if (subtreeAvailability) { - this->addSubtreeAvailability( - subtreeID, - std::move(*subtreeAvailability)); - - // tell client to retry later - return TileLoadResult::createRetryLaterResult(nullptr); - } else { - // Subtree load failed, so this tile fails, too. - return TileLoadResult::createFailedResult(nullptr); - } - }); + baseResponse, + responsesByUrl) + .thenInMainThread( + [this, + subtreeID](SubtreeAvailability::LoadResult&& loadResult) mutable { + if (loadResult.first) { + // Availability success + this->addSubtreeAvailability( + subtreeID, + std::move(*loadResult.first)); + + // tell client to retry later + return TileLoadResult::createRetryLaterResult(); + } else if (!loadResult.second.url.empty()) { + // No availability, but a url was requested + // Let this work go back into the request queue + return TileLoadResult::createRequestResult(loadResult.second); + } else { + // Subtree load failed, so this tile fails, too. + return TileLoadResult::createFailedResult(); + } + }); } // subtree is available, so check if tile has content or not. If it has, then @@ -284,8 +283,9 @@ ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { std::nullopt, std::nullopt, std::nullopt, - nullptr, + std::string(), {}, + CesiumAsync::RequestData{}, TileLoadResultState::Success}); } @@ -293,16 +293,50 @@ ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { this->_baseUrl, this->_contentUrlTemplate, *pQuadtreeID); + + // If tile url is not loaded, request it and come back later + auto foundIt = responsesByUrl.find(tileUrl); + if (foundIt == responsesByUrl.end()) { + return asyncSystem.createResolvedFuture( + TileLoadResult::createRequestResult( + CesiumAsync::RequestData{tileUrl, {}})); + } + + const CesiumAsync::ResponseData& responseData = foundIt->second; + assert(responseData.pResponse); + uint16_t statusCode = responseData.pResponse->statusCode(); + assert(statusCode != 0); + if (statusCode < 200 || statusCode >= 300) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + tileUrl); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); + } + return requestTileContent( pLogger, asyncSystem, - pAssetAccessor, tileUrl, - requestHeaders, + foundIt->second.pResponse->data(), contentOptions.ktx2TranscodeTargets, contentOptions.applyTextureTransform); } +bool ImplicitQuadtreeLoader::getLoadWork( + const Tile*, + CesiumAsync::RequestData&, + TileLoaderCallback& outCallback) { + // loadTileContent will control request / processing flow + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + return true; +} + TileChildrenResult ImplicitQuadtreeLoader::createTileChildren(const Tile& tile) { const CesiumGeometry::QuadtreeTileID* pQuadtreeID = diff --git a/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.h b/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.h index 80c3dd36a..5136a8475 100644 --- a/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.h +++ b/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.h @@ -42,6 +42,11 @@ class ImplicitQuadtreeLoader : public TilesetContentLoader { CesiumAsync::Future loadTileContent(const TileLoadInput& loadInput) override; + bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) override; + TileChildrenResult createTileChildren(const Tile& tile) override; uint32_t getSubtreeLevels() const noexcept; diff --git a/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.cpp b/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.cpp index 45224cd68..74a9b7f33 100644 --- a/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.cpp +++ b/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.cpp @@ -48,7 +48,7 @@ BoundingVolume createDefaultLooseEarthBoundingVolume( TileLoadResult convertToTileLoadResult(QuantizedMeshLoadResult&& loadResult) { if (loadResult.errors || !loadResult.model) { - return TileLoadResult::createFailedResult(loadResult.pRequest); + return TileLoadResult::createFailedResult(); } return TileLoadResult{ @@ -57,8 +57,9 @@ TileLoadResult convertToTileLoadResult(QuantizedMeshLoadResult&& loadResult) { loadResult.updatedBoundingVolume, std::nullopt, std::nullopt, - nullptr, + std::string(), {}, + RequestData{}, TileLoadResultState::Success}; } @@ -635,90 +636,36 @@ std::string resolveTileUrl( Future requestTileContent( const std::shared_ptr& pLogger, const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, const QuadtreeTileID& tileID, const BoundingRegion& boundingRegion, - const LayerJsonTerrainLoader::Layer& layer, - const std::vector& requestHeaders, - bool enableWaterMask) { - std::string url = resolveTileUrl(tileID, layer); - return pAssetAccessor->get(asyncSystem, url, requestHeaders) - .thenInWorkerThread( - [asyncSystem, pLogger, tileID, boundingRegion, enableWaterMask]( - std::shared_ptr&& pRequest) { - const IAssetResponse* pResponse = pRequest->response(); - if (!pResponse) { - QuantizedMeshLoadResult result; - result.errors.emplaceError(fmt::format( - "Did not receive a valid response for tile content {}", - pRequest->url())); - result.pRequest = std::move(pRequest); - return result; - } - - if (pResponse->statusCode() != 0 && - (pResponse->statusCode() < 200 || - pResponse->statusCode() >= 300)) { - QuantizedMeshLoadResult result; - result.errors.emplaceError(fmt::format( - "Receive status code {} for tile content {}", - pResponse->statusCode(), - pRequest->url())); - result.pRequest = std::move(pRequest); - return result; - } - - return QuantizedMeshLoader::load( - tileID, - boundingRegion, - pRequest->url(), - pResponse->data(), - enableWaterMask); - }); + bool enableWaterMask, + const std::string& requestUrl, + const gsl::span& responseData) { + return asyncSystem.runInWorkerThread([asyncSystem, + pLogger, + tileID, + boundingRegion, + enableWaterMask, + requestUrl, + responseData]() { + return QuantizedMeshLoader::load( + tileID, + boundingRegion, + requestUrl, + responseData, + enableWaterMask); + }); } -Future loadTileAvailability( - const std::shared_ptr& pLogger, - const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const QuadtreeTileID& tileID, - LayerJsonTerrainLoader::Layer& layer, - const std::vector& requestHeaders) { - std::string url = resolveTileUrl(tileID, layer); - return pAssetAccessor->get(asyncSystem, url, requestHeaders) - .thenInWorkerThread([pLogger, - tileID](std::shared_ptr&& pRequest) { - const IAssetResponse* pResponse = pRequest->response(); - if (pResponse) { - uint16_t statusCode = pResponse->statusCode(); - - if (!(statusCode != 0 && (statusCode < 200 || statusCode >= 300))) { - return QuantizedMeshLoader::loadMetadata(pResponse->data(), tileID); - } - } - - SPDLOG_LOGGER_ERROR( - pLogger, - "Failed to load availability data from {}", - pRequest->url()); - return QuantizedMeshMetadataResult(); - }) - .thenInMainThread([&layer, - tileID](QuantizedMeshMetadataResult&& metadata) { - addRectangleAvailabilityToLayer(layer, tileID, metadata.availability); - return 0; - }); -} } // namespace Future LayerJsonTerrainLoader::loadTileContent(const TileLoadInput& loadInput) { const auto& tile = loadInput.tile; const auto& asyncSystem = loadInput.asyncSystem; - const auto& pAssetAccessor = loadInput.pAssetAccessor; const auto& pLogger = loadInput.pLogger; - const auto& requestHeaders = loadInput.requestHeaders; const auto& contentOptions = loadInput.contentOptions; + const auto& responsesByUrl = loadInput.responsesByUrl; // This type of loader should never have child loaders. assert(tile.getLoader() == this); @@ -732,7 +679,7 @@ LayerJsonTerrainLoader::loadTileContent(const TileLoadInput& loadInput) { if (!pUpsampleTileID) { // This loader only handles QuadtreeTileIDs and UpsampledQuadtreeNode. return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // now do upsampling @@ -751,7 +698,7 @@ LayerJsonTerrainLoader::loadTileContent(const TileLoadInput& loadInput) { if (firstAvailableIt == this->_layers.end()) { // No layer has this tile available. return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // Also load the same tile in any underlying layers for which this tile @@ -764,21 +711,62 @@ LayerJsonTerrainLoader::loadTileContent(const TileLoadInput& loadInput) { auto it = firstAvailableIt; ++it; - while (it != this->_layers.end()) { - if (it->availabilityLevels >= 1 && - (int32_t(pQuadtreeTileID->level) % it->availabilityLevels) == 0) { - if (!isSubtreeLoadedInLayer(*pQuadtreeTileID, *it)) { - availabilityRequests.emplace_back(loadTileAvailability( - pLogger, - asyncSystem, - pAssetAccessor, - *pQuadtreeTileID, - *it, - requestHeaders)); - } + for (; it != this->_layers.end(); ++it) { + LayerJsonTerrainLoader::Layer& layer = *it; + + bool hasAvailability = + layer.availabilityLevels >= 1 && + (int32_t(pQuadtreeTileID->level) % layer.availabilityLevels) == 0; + if (!hasAvailability) + continue; + + if (isSubtreeLoadedInLayer(*pQuadtreeTileID, layer)) + continue; + + std::string url = resolveTileUrl(*pQuadtreeTileID, layer); + + // If url is not loaded, request it and come back later + auto foundIt = responsesByUrl.find(url); + if (foundIt == responsesByUrl.end()) { + return asyncSystem.createResolvedFuture( + TileLoadResult::createRequestResult( + CesiumAsync::RequestData{url, {}})); } - ++it; + const IAssetResponse* pResponse = foundIt->second.pResponse; + assert(pResponse); + uint16_t statusCode = pResponse->statusCode(); + assert(statusCode != 0); + bool statusValid = statusCode >= 200 && statusCode < 300; + + if (!statusValid) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Failed to load availability data from {}", + url); + + addRectangleAvailabilityToLayer( + layer, + *pQuadtreeTileID, + std::vector{}); + } else { + Future requestFuture = + asyncSystem + .runInWorkerThread( + [tileID = *pQuadtreeTileID, data = pResponse->data()]() { + return QuantizedMeshLoader::loadMetadata(data, tileID); + }) + .thenInMainThread([&layer, tileID = *pQuadtreeTileID]( + QuantizedMeshMetadataResult&& metadata) { + addRectangleAvailabilityToLayer( + layer, + tileID, + metadata.availability); + return 0; + }); + + availabilityRequests.emplace_back(std::move(requestFuture)); + } } const BoundingRegion* pRegion = @@ -786,20 +774,49 @@ LayerJsonTerrainLoader::loadTileContent(const TileLoadInput& loadInput) { if (!pRegion) { // This tile does not have the required bounding volume type. return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // Start the actual content request. auto& currentLayer = *firstAvailableIt; + + std::string url = resolveTileUrl(*pQuadtreeTileID, currentLayer); + + auto foundIt = responsesByUrl.find(url); + assert(foundIt != responsesByUrl.end()); + + auto response = foundIt->second.pResponse; + assert(response); + + uint16_t statusCode = response->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + url); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); + } + + const gsl::span& responseData = response->data(); + if (responseData.empty()) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Did not receive a valid response for tile content {}", + url); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); + } + Future futureQuantizedMesh = requestTileContent( pLogger, asyncSystem, - pAssetAccessor, *pQuadtreeTileID, *pRegion, - currentLayer, - requestHeaders, - contentOptions.enableWaterMask); + contentOptions.enableWaterMask, + url, + responseData); // determine if this tile is at the availability level of the current layer // and if we need to add the availability rectangles to the current layer. We @@ -891,6 +908,41 @@ LayerJsonTerrainLoader::loadTileContent(const TileLoadInput& loadInput) { }); } +bool LayerJsonTerrainLoader::getLoadWork( + const Tile* pTile, + RequestData& outRequest, + TileLoaderCallback& outCallback) { + + const QuadtreeTileID* pQuadtreeTileID = + std::get_if(&pTile->getTileID()); + if (pQuadtreeTileID) { + // Always request the tile from the first layer in which this tile ID is + // available. + auto firstAvailableIt = this->_layers.begin(); + while (firstAvailableIt != this->_layers.end() && + !firstAvailableIt->contentAvailability.isTileAvailable( + *pQuadtreeTileID)) { + ++firstAvailableIt; + } + + if (firstAvailableIt == this->_layers.end()) + return false; // No layer has this tile available. + + // Start the actual content request. + auto& currentLayer = *firstAvailableIt; + outRequest.url = resolveTileUrl(*pQuadtreeTileID, currentLayer); + } else { + // Upsampling tiles do not have a tile id and do not request a url + } + + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + + return true; +} + TileChildrenResult LayerJsonTerrainLoader::createTileChildren(const Tile& tile) { const CesiumGeometry::QuadtreeTileID* pQuadtreeID = @@ -1102,7 +1154,7 @@ CesiumAsync::Future LayerJsonTerrainLoader::upsampleParentTile( parentContent.getRenderContent(); if (!pParentRenderContent) { return asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } const UpsampledQuadtreeNode* pUpsampledTileID = @@ -1141,7 +1193,7 @@ CesiumAsync::Future LayerJsonTerrainLoader::upsampleParentTile( RasterOverlayUtilities::DEFAULT_TEXTURE_COORDINATE_BASE_NAME, textureCoordinateIndex); if (!model) { - return TileLoadResult::createFailedResult(nullptr); + return TileLoadResult::createFailedResult(); } return TileLoadResult{ @@ -1150,8 +1202,9 @@ CesiumAsync::Future LayerJsonTerrainLoader::upsampleParentTile( std::nullopt, std::nullopt, std::nullopt, - nullptr, + std::string(), {}, + RequestData{}, TileLoadResultState::Success}; }); } diff --git a/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.h b/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.h index a539711ce..df9d9a366 100644 --- a/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.h +++ b/Cesium3DTilesSelection/src/LayerJsonTerrainLoader.h @@ -69,6 +69,11 @@ class LayerJsonTerrainLoader : public TilesetContentLoader { CesiumAsync::Future loadTileContent(const TileLoadInput& loadInput) override; + bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) override; + TileChildrenResult createTileChildren(const Tile& tile) override; const CesiumGeometry::QuadtreeTilingScheme& getTilingScheme() const noexcept; diff --git a/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp b/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp index 3e10e2008..fd685e06a 100644 --- a/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp +++ b/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp @@ -211,17 +211,6 @@ void RasterMappedTo3DTile::detachFromTile( this->_state = AttachmentState::Unattached; } -bool RasterMappedTo3DTile::loadThrottled() noexcept { - CESIUM_TRACE("RasterMappedTo3DTile::loadThrottled"); - RasterOverlayTile* pLoading = this->getLoadingTile(); - if (!pLoading) { - return true; - } - - RasterOverlayTileProvider& provider = pLoading->getTileProvider(); - return provider.loadTileThrottled(*pLoading); -} - namespace { IntrusivePointer diff --git a/Cesium3DTilesSelection/src/RasterOverlayUpsampler.cpp b/Cesium3DTilesSelection/src/RasterOverlayUpsampler.cpp index 608087280..9ea3bcb17 100644 --- a/Cesium3DTilesSelection/src/RasterOverlayUpsampler.cpp +++ b/Cesium3DTilesSelection/src/RasterOverlayUpsampler.cpp @@ -19,7 +19,7 @@ RasterOverlayUpsampler::loadTileContent(const TileLoadInput& loadInput) { const Tile* pParent = loadInput.tile.getParent(); if (pParent == nullptr) { return loadInput.asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } const CesiumGeometry::UpsampledQuadtreeNode* pTileID = @@ -28,7 +28,7 @@ RasterOverlayUpsampler::loadTileContent(const TileLoadInput& loadInput) { if (pTileID == nullptr) { // this tile is not marked to be upsampled, so just fail it return loadInput.asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } // The tile content manager guarantees that the parent tile is already loaded @@ -43,7 +43,7 @@ RasterOverlayUpsampler::loadTileContent(const TileLoadInput& loadInput) { if (!pParentRenderContent) { // parent doesn't have mesh, so it's not possible to upsample return loadInput.asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } int32_t index = 0; @@ -75,7 +75,7 @@ RasterOverlayUpsampler::loadTileContent(const TileLoadInput& loadInput) { RasterOverlayUtilities::DEFAULT_TEXTURE_COORDINATE_BASE_NAME, textureCoordinateIndex); if (!model) { - return TileLoadResult::createFailedResult(nullptr); + return TileLoadResult::createFailedResult(); } return TileLoadResult{ @@ -84,12 +84,24 @@ RasterOverlayUpsampler::loadTileContent(const TileLoadInput& loadInput) { std::nullopt, std::nullopt, std::nullopt, - nullptr, + std::string(), {}, + CesiumAsync::RequestData{}, TileLoadResultState::Success}; }); } +bool RasterOverlayUpsampler::getLoadWork( + const Tile*, + CesiumAsync::RequestData&, + TileLoaderCallback& outCallback) { + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + return true; +} + TileChildrenResult RasterOverlayUpsampler::createTileChildren([[maybe_unused]] const Tile& tile) { return {{}, TileLoadResultState::Failed}; diff --git a/Cesium3DTilesSelection/src/RasterOverlayUpsampler.h b/Cesium3DTilesSelection/src/RasterOverlayUpsampler.h index 6d3120e64..8c9218622 100644 --- a/Cesium3DTilesSelection/src/RasterOverlayUpsampler.h +++ b/Cesium3DTilesSelection/src/RasterOverlayUpsampler.h @@ -8,6 +8,11 @@ class RasterOverlayUpsampler : public TilesetContentLoader { CesiumAsync::Future loadTileContent(const TileLoadInput& loadInput) override; + bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) override; + TileChildrenResult createTileChildren(const Tile& tile) override; }; } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TileWorkManager.cpp b/Cesium3DTilesSelection/src/TileWorkManager.cpp new file mode 100644 index 000000000..4a5c9e2aa --- /dev/null +++ b/Cesium3DTilesSelection/src/TileWorkManager.cpp @@ -0,0 +1,571 @@ +#include "Cesium3DTilesSelection/TileWorkManager.h" + +#include "CesiumAsync/IAssetResponse.h" + +using namespace CesiumAsync; + +namespace Cesium3DTilesSelection { + +TileWorkManager::~TileWorkManager() noexcept { + assert(_requestsPending.empty()); + assert(_requestsInFlight.empty()); + assert(_processingPending.empty()); + assert(_failedWork.empty()); + assert(_doneWork.empty()); +} + +void TileWorkManager::Shutdown() { + std::lock_guard lock(_requestsLock); + + // We could have requests in flight + // Let them complete, but signal no more work should be done + _shutdownSignaled = true; + + _requestsPending.clear(); + _requestsInFlight.clear(); + _processingPending.clear(); + _processingInFlight.clear(); + _failedWork.clear(); + _doneWork.clear(); +} + +void TileWorkManager::stageWork(Work* pWork) { + // Assert this work is already owned by this manager + assert(_ownedWork.find(pWork->uniqueId) != _ownedWork.end()); + + CesiumAsync::RequestData* pendingRequest = pWork->getNextRequest(); + + if (!pendingRequest) { + // No pending request, go straight to processing queue +#ifndef NDEBUG + for (auto element : _processingPending) + assert(element->uniqueId != pWork->uniqueId); +#endif + + _processingPending.push_back(pWork); + } else { + auto foundIt = _requestsInFlight.find(pendingRequest->url); + if (foundIt == _requestsInFlight.end()) { + // The request isn't in flight, queue it +#ifndef NDEBUG + for (auto element : _requestsPending) + assert(element->uniqueId != pWork->uniqueId); +#endif + _requestsPending.push_back(pWork); + } else { + // Already in flight, tag along +#ifndef NDEBUG + for (auto element : foundIt->second) + assert(element->uniqueId != pWork->uniqueId); +#endif + foundIt->second.push_back(pWork); + } + } +} + +TileWorkManager::Work* TileWorkManager::createWorkFromOrder(Order* pOrder) { + bool workHasTileProcessing = + std::holds_alternative(pOrder->processingData); + + TileSource uniqueId; + if (workHasTileProcessing) { + TileProcessingData workTileProcessing = + std::get(pOrder->processingData); + uniqueId = workTileProcessing.pTile; + } else { + RasterProcessingData workRasterProcessing = + std::get(pOrder->processingData); + uniqueId = workRasterProcessing.pRasterTile; + } + + // Assert any work isn't already owned by this manager + assert(_ownedWork.find(uniqueId) == _ownedWork.end()); + + Work newWork{uniqueId, std::move(*pOrder)}; + + if (!newWork.order.requestData.url.empty()) + newWork.pendingRequests.push_back(newWork.order.requestData); + + auto returnPair = _ownedWork.emplace(uniqueId, std::move(newWork)); + assert(returnPair.second); + + Work* pWork = &returnPair.first->second; + + stageWork(pWork); + + return pWork; +} + +void TileWorkManager::ordersToWork( + const std::vector& orders, + std::vector& instancesCreated) { + + for (Order* order : orders) { + Work* newInstance = createWorkFromOrder(order); + + instancesCreated.push_back(newInstance); + + // Create child work, if exists. Link parent->child with raw pointers + // Only support one level deep, for now + for (Order& childWork : newInstance->order.childOrders) { + Work* newChildInstance = createWorkFromOrder(&childWork); + instancesCreated.push_back(newChildInstance); + } + } +} + +void TileWorkManager::throttleOrders( + size_t existingCount, + size_t maxCount, + std::vector& inOutOrders) { + if (existingCount >= maxCount) { + // No request slots open, don't submit anything + inOutOrders.clear(); + } else { + size_t slotsOpen = maxCount - existingCount; + if (slotsOpen >= inOutOrders.size()) { + // We can take all incoming work + return; + } else { + // We can only take part of the incoming work + // Just submit the highest priority + // Put highest priority at front of vector + // then chop off the rest + std::sort(begin(inOutOrders), end(inOutOrders), [](Order* a, Order* b) { + return (*a) < (*b); + }); + + inOutOrders.resize(slotsOpen); + } + } +} + +void TileWorkManager::TryAddOrders( + std::shared_ptr& thiz, + std::vector& orders, + size_t maxSimultaneousRequests, + std::vector& workCreated) { + if (orders.empty()) + return; + + // Request work will always go to that queue first + // Work with only processing can bypass it + std::vector requestOrders; + std::vector processingOrders; + for (Order& order : orders) { + if (order.requestData.url.empty()) + processingOrders.push_back(&order); + else + requestOrders.push_back(&order); + } + + // Figure out how much url work we will accept + // + // For requests, we want some work to be ready to go in between frames + // so the dispatcher doesn't starve while we wait for a tick + // + // For processing we don't want excessive amounts of work queued + // Ex. Spinning around, content is cached, content requests are beating + // processing work and queueing up faster than they are consumed + size_t requestsMax = maxSimultaneousRequests + 10; + size_t processingMax = maxSimultaneousRequests * 10; + + size_t requestsCount, processingCount; + thiz->GetPendingCount(requestsCount, processingCount); + TileWorkManager::throttleOrders(requestsCount, requestsMax, requestOrders); + TileWorkManager::throttleOrders( + processingCount, + processingMax, + processingOrders); + + if (requestOrders.empty() && processingOrders.empty()) + return; + + { + std::lock_guard lock(thiz->_requestsLock); + thiz->_maxSimultaneousRequests = maxSimultaneousRequests; + + // Copy load requests into internal work we will own + thiz->ordersToWork(requestOrders, workCreated); + thiz->ordersToWork(processingOrders, workCreated); + } + + if (requestOrders.size()) + transitionRequests(thiz); +} + +void TileWorkManager::RequeueWorkForRequest( + std::shared_ptr& thiz, + Work* work) { + { + std::lock_guard lock(thiz->_requestsLock); + + if (thiz->_shutdownSignaled) + return; + + // This processing work should be in flight, remove it + auto foundIt = thiz->_processingInFlight.find(work->uniqueId); + assert(foundIt != thiz->_processingInFlight.end()); + thiz->_processingInFlight.erase(foundIt); + + thiz->stageWork(work); + } + + transitionRequests(thiz); +} + +void TileWorkManager::SignalWorkComplete(Work* work) { + std::lock_guard lock(_requestsLock); + + if (_shutdownSignaled) + return; + + // Assert this work is already owned by this manager + assert(_ownedWork.find(work->uniqueId) != _ownedWork.end()); + + // This processing work should be in flight, remove it + auto foundIt = _processingInFlight.find(work->uniqueId); + assert(foundIt != _processingInFlight.end()); + _processingInFlight.erase(foundIt); + + // Assert this is not in any other queues +#ifndef NDEBUG + for (auto element : _requestsPending) + assert(element->uniqueId != work->uniqueId); + for (auto& urlWorkVecPair : _requestsInFlight) + for (auto element : urlWorkVecPair.second) + assert(element->uniqueId != work->uniqueId); + for (auto element : _processingPending) + assert(element->uniqueId != work->uniqueId); +#endif + + // Put in the done list + _doneWork.push_back(work); +} + +void TileWorkManager::TryDispatchProcessing( + std::shared_ptr& thiz) { + transitionProcessing(thiz); +} + +void TileWorkManager::onRequestFinished( + std::shared_ptr& pCompletedRequest) { + + std::lock_guard lock(_requestsLock); + + if (_shutdownSignaled) + return; + + const IAssetResponse* response = pCompletedRequest->response(); + uint16_t responseStatusCode = response ? response->statusCode() : 0; + + // Find this request + auto foundIt = _requestsInFlight.find(pCompletedRequest->url()); + assert(foundIt != _requestsInFlight.end()); + + // Handle results + std::vector& requestWorkVec = foundIt->second; + for (Work* requestWork : requestWorkVec) { + CesiumAsync::RequestData* workNextRequest = requestWork->getNextRequest(); + assert(workNextRequest); + assert(pCompletedRequest->url() == workNextRequest->url); + + assert(_ownedWork.find(requestWork->uniqueId) != _ownedWork.end()); + + // A response code of 0 is not a valid HTTP code + // and probably indicates a non-network error. + // 404 is not found, which is failure + // Put this work in a failed queue to be handled later + if (responseStatusCode == 0 || responseStatusCode == 404) { + std::string errorReason; + if (responseStatusCode == 0) + errorReason = "Invalid response for tile content"; + else + errorReason = "Received status code 404 for tile content"; + _failedWork.emplace_back( + FailedWorkPair(std::move(errorReason), requestWork)); + continue; + } + + // Handle requested in the finished work + const std::string& key = workNextRequest->url; + assert( + requestWork->completedRequests.find(key) == + requestWork->completedRequests.end()); + + requestWork->completedRequests[key] = pCompletedRequest; + requestWork->pendingRequests.pop_back(); + + // Put it back into the appropriate queue + stageWork(requestWork); + } + + // Remove it + _requestsInFlight.erase(foundIt); +} + +void TileWorkManager::GetPendingCount( + size_t& pendingRequests, + size_t& pendingProcessing) { + std::lock_guard lock(_requestsLock); + pendingRequests = _requestsPending.size() + _requestsInFlight.size(); + pendingProcessing = _processingPending.size(); +} + +size_t TileWorkManager::GetActiveWorkCount() { + std::lock_guard lock(_requestsLock); + return _requestsPending.size() + _requestsInFlight.size() + + _processingPending.size() + _processingInFlight.size(); +} + +void TileWorkManager::GetLoadingWorkStats( + size_t& requestCount, + size_t& inFlightCount, + size_t& processingCount, + size_t& failedCount) { + std::lock_guard lock(_requestsLock); + requestCount = _requestsPending.size(); + inFlightCount = _requestsInFlight.size(); + processingCount = _processingPending.size(); + failedCount = _failedWork.size(); +} + +void TileWorkManager::TakeCompletedWork( + std::vector& outCompleted, + std::vector& outFailed) { + std::lock_guard lock(_requestsLock); + + for (auto work : _doneWork) { + // We should own this and it should not be in any other queues + auto foundIt = _ownedWork.find(work->uniqueId); + +#ifndef NDEBUG + assert(foundIt != _ownedWork.end()); + for (auto element : _requestsPending) + assert(element->uniqueId != work->uniqueId); + for (auto& urlWorkVecPair : _requestsInFlight) + for (auto element : urlWorkVecPair.second) + assert(element->uniqueId != work->uniqueId); + for (auto element : _processingPending) + assert(element->uniqueId != work->uniqueId); + assert( + _processingInFlight.find(work->uniqueId) == _processingInFlight.end()); +#endif + + outCompleted.emplace_back(DoneOrder{ + std::move(work->tileLoadResult), + work->pRenderResources, + std::move(work->order)}); + _ownedWork.erase(foundIt); + } + _doneWork.clear(); + + for (auto workPair : _failedWork) { + Work* work = workPair.second; + + // We should own this and it should not be in any other queues + auto foundIt = _ownedWork.find(work->uniqueId); + +#ifndef NDEBUG + assert(foundIt != _ownedWork.end()); + for (auto element : _requestsPending) + assert(element->uniqueId != work->uniqueId); + for (auto& urlWorkVecPair : _requestsInFlight) + for (auto element : urlWorkVecPair.second) + assert(element->uniqueId != work->uniqueId); + for (auto element : _processingPending) + assert(element->uniqueId != work->uniqueId); + assert( + _processingInFlight.find(work->uniqueId) == _processingInFlight.end()); +#endif + + outFailed.emplace_back(FailedOrder{workPair.first, std::move(work->order)}); + _ownedWork.erase(foundIt); + } + _failedWork.clear(); +} + +void TileWorkManager::transitionRequests( + std::shared_ptr& thiz) { + std::vector workNeedingDispatch; + { + std::lock_guard lock(thiz->_requestsLock); + + if (thiz->_shutdownSignaled) + return; + + size_t queueCount = thiz->_requestsPending.size(); + if (queueCount == 0) + return; + + // We have work to do, check if there's a slot for it + size_t slotsTotal = thiz->_maxSimultaneousRequests; + size_t slotsUsed = thiz->_requestsInFlight.size(); + assert(slotsUsed <= slotsTotal); + if (slotsUsed == slotsTotal) + return; + + // At least one slot is open + // Sort our incoming request queue by priority + // Want highest priority at back of vector + std::sort( + begin(thiz->_requestsPending), + end(thiz->_requestsPending), + [](Work* a, Work* b) { return b->order < a->order; }); + + // Loop through all pending until no more slots (or pending) + while (!thiz->_requestsPending.empty() && slotsUsed < slotsTotal) { + + // Start from back of queue (highest priority). + Work* requestWork = thiz->_requestsPending.back(); + + CesiumAsync::RequestData* nextRequest = requestWork->getNextRequest(); + assert(nextRequest); + + const std::string& workUrl = nextRequest->url; + + // The first work with this url needs dispatch + workNeedingDispatch.push_back(requestWork); + + // Gather all work with urls that match this + using WorkVecIterator = std::vector::iterator; + std::vector matchingUrlWork; + auto matchIt = thiz->_requestsPending.end() - 1; + matchingUrlWork.push_back(matchIt); + + while (matchIt != thiz->_requestsPending.begin()) { + --matchIt; + Work* otherWork = *matchIt; + CesiumAsync::RequestData* otherRequest = otherWork->getNextRequest(); + assert(otherRequest); + if (otherRequest->url == workUrl) + matchingUrlWork.push_back(matchIt); + } + + // Set up a new inflight request + // Erase related entries from pending queue + std::vector newWorkVec; + assert( + thiz->_requestsInFlight.find(workUrl) == + thiz->_requestsInFlight.end()); + for (WorkVecIterator& it : matchingUrlWork) { + newWorkVec.push_back(*it); + thiz->_requestsPending.erase(it); + } + + thiz->_requestsInFlight.emplace(workUrl, std::move(newWorkVec)); + ++slotsUsed; + } + } + + for (Work* requestWork : workNeedingDispatch) { + // Keep the manager alive while the load is in progress + // Capture the shared pointer by value + + CesiumAsync::RequestData* nextRequest = requestWork->getNextRequest(); + assert(nextRequest); + + thiz->_pAssetAccessor + ->get(thiz->_asyncSystem, nextRequest->url, nextRequest->headers) + .thenImmediately( + [thiz](std::shared_ptr&& pCompletedRequest) mutable { + assert(thiz.get()); + thiz->onRequestFinished(pCompletedRequest); + transitionRequests(thiz); + }) + .thenInMainThread([thiz]() mutable { transitionProcessing(thiz); }); + } +} + +void TileWorkManager::transitionProcessing( + std::shared_ptr& thiz) { + std::vector workNeedingDispatch; + { + std::lock_guard lock(thiz->_requestsLock); + + if (thiz->_shutdownSignaled) + return; + + size_t pendingCount = thiz->_processingPending.size(); + if (pendingCount == 0) + return; + + // We have work to do, check if there's a slot for it + size_t slotsTotal = thiz->_maxSimultaneousRequests; + size_t slotsUsed = thiz->_processingInFlight.size(); + assert(slotsUsed <= slotsTotal); + if (slotsUsed == slotsTotal) + return; + + // At least one slot is open + size_t slotsAvailable = slotsTotal - slotsUsed; + + if (slotsAvailable >= pendingCount) { + // We can take all of it + for (auto work : thiz->_processingPending) { + assert( + thiz->_processingInFlight.find(work->uniqueId) == + thiz->_processingInFlight.end()); + thiz->_processingInFlight[work->uniqueId] = work; + + workNeedingDispatch.push_back(work); + } + thiz->_processingPending.clear(); + } else { + // We can only take part of the incoming work + // Put highest priority at end of vector + std::sort( + begin(thiz->_processingPending), + end(thiz->_processingPending), + [](Work* a, Work* b) { return (b->order) < (a->order); }); + + size_t countToTake = slotsAvailable; + + // Move candidate work to in flight, highest priority subset + size_t copyStart = pendingCount - countToTake; + for (size_t workIndex = copyStart; workIndex < pendingCount; + ++workIndex) { + Work* work = thiz->_processingPending[workIndex]; + + assert( + thiz->_processingInFlight.find(work->uniqueId) == + thiz->_processingInFlight.end()); + thiz->_processingInFlight[work->uniqueId] = work; + + workNeedingDispatch.push_back(work); + } + assert(workNeedingDispatch.size() == countToTake); + + size_t newPendingSize = pendingCount - countToTake; + thiz->_processingPending.resize(newPendingSize); + } + } + + for (Work* work : workNeedingDispatch) { + CesiumAsync::UrlResponseDataMap responseDataMap; + work->fillResponseDataMap(responseDataMap); + + if (std::holds_alternative( + work->order.processingData)) { + TileProcessingData& tileProcessing = + std::get(work->order.processingData); + + thiz->_tileDispatchFunc(tileProcessing, responseDataMap, work); + } else { + RasterProcessingData& rasterProcessing = + std::get(work->order.processingData); + + thiz->_rasterDispatchFunc(rasterProcessing, responseDataMap, work); + } + } +} + +void TileWorkManager::SetDispatchFunctions( + TileDispatchFunc& tileDispatch, + RasterDispatchFunc& rasterDispatch) { + std::lock_guard lock(_requestsLock); + _tileDispatchFunc = tileDispatch; + _rasterDispatchFunc = rasterDispatch; +} + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 1ecb0e45f..f9f5f8fcd 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -25,6 +25,8 @@ #include #include +#define LOG_LOADING_WORK_STATS 0 + using namespace CesiumAsync; using namespace CesiumGeometry; using namespace CesiumGeospatial; @@ -293,6 +295,28 @@ Tileset::updateViewOffline(const std::vector& frustums) { return this->_updateResult; } +void Tileset::_logLoadingWorkStats(const std::string& prefix) { + size_t requestCount, inFlightCount, processingCount, failedCount; + this->_pTilesetContentManager->getLoadingWorkStats( + requestCount, + inFlightCount, + processingCount, + failedCount); + + SPDLOG_LOGGER_INFO( + this->_externals.pLogger, + "{} requests {} | inFlight {} | processing {} || " + "TilesLoading {} | RastersLoading {} | MainThreadQueue {} | Total {}", + prefix, + requestCount, + inFlightCount, + processingCount, + _updateResult.tilesLoading, + _updateResult.rastersLoading, + _updateResult.mainThreadTileLoadQueueLength, + _updateResult.mainThreadTotalTileLoads); +} + const ViewUpdateResult& Tileset::updateView(const std::vector& frustums, float deltaTime) { CESIUM_TRACE("Tileset::updateView"); @@ -302,6 +326,13 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { _options.enableFogCulling = _options.enableFogCulling && !_options.enableLodTransitionPeriod; +#if LOG_LOADING_WORK_STATS + float loadProgress = this->computeLoadProgress(); + bool showWorkStats = loadProgress > 0 && loadProgress < 100; + if (showWorkStats) + _logLoadingWorkStats("Pre :"); +#endif + this->_asyncSystem.dispatchMainThreadTasks(); const int32_t previousFrameNumber = this->_previousFrameNumber; @@ -310,13 +341,10 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { ViewUpdateResult& result = this->_updateResult; result.frameNumber = currentFrameNumber; result.tilesToRenderThisFrame.clear(); - result.tilesVisited = 0; - result.culledTilesVisited = 0; - result.tilesCulled = 0; - result.tilesOccluded = 0; - result.tilesWaitingForOcclusionResults = 0; - result.tilesKicked = 0; - result.maxDepthVisited = 0; + result.resetStats(); + + result.workerThreadTileLoadQueueLength = this->_workerThreadLoadQueue.size(); + result.mainThreadTileLoadQueueLength = this->_mainThreadLoadQueue.size(); if (!_options.enableLodTransitionPeriod) { result.tilesFadingOut.clear(); @@ -357,11 +385,6 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { result = ViewUpdateResult(); } - result.workerThreadTileLoadQueueLength = - static_cast(this->_workerThreadLoadQueue.size()); - result.mainThreadTileLoadQueueLength = - static_cast(this->_mainThreadLoadQueue.size()); - const std::shared_ptr& pOcclusionPool = this->getExternals().pTileOcclusionProxyPool; if (pOcclusionPool) { @@ -371,8 +394,14 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { this->_unloadCachedTiles(this->_options.tileCacheUnloadTimeLimit); this->_processWorkerThreadLoadQueue(); this->_processMainThreadLoadQueue(); + this->_updateLodTransitions(frameState, deltaTime, result); +#if LOG_LOADING_WORK_STATS + if (showWorkStats) + _logLoadingWorkStats("Post:"); +#endif + // aggregate all the credits needed from this tileset for the current frame const std::shared_ptr& pCreditSystem = this->_externals.pCreditSystem; @@ -428,31 +457,33 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { return result; } + int32_t Tileset::getNumberOfTilesLoaded() const { return this->_pTilesetContentManager->getNumberOfTilesLoaded(); } float Tileset::computeLoadProgress() noexcept { - int32_t queueSizeSum = static_cast( - this->_updateResult.workerThreadTileLoadQueueLength + - this->_updateResult.mainThreadTileLoadQueueLength); - int32_t numOfTilesLoading = - this->_pTilesetContentManager->getNumberOfTilesLoading(); - int32_t numOfTilesLoaded = - this->_pTilesetContentManager->getNumberOfTilesLoaded(); - int32_t numOfTilesKicked = - static_cast(this->_updateResult.tilesKicked); - // Amount of work actively being done - int32_t inProgressSum = numOfTilesLoading + queueSizeSum; + size_t queueLengthsSum = _updateResult.mainThreadTileLoadQueueLength + + _updateResult.workerThreadTileLoadQueueLength; + uint32_t inProgressSum = + static_cast(queueLengthsSum) + + static_cast(_updateResult.activeWorkCount) + + _updateResult.tilesLoading + _updateResult.rastersLoading + + static_cast(_updateResult.tilesFadingOut.size()); + uint32_t numOfTilesKicked = this->_updateResult.tilesKicked; + + uint32_t completedSum = + _updateResult.tilesLoaded + _updateResult.rastersLoaded; // Total work so far. Add already loaded tiles and kicked tiles. // Kicked tiles are transient, and never in progress, but are an indicator // that there is more work to do next frame. - int32_t totalNum = inProgressSum + numOfTilesLoaded + numOfTilesKicked; + uint32_t totalNum = inProgressSum + completedSum + numOfTilesKicked; float percentage = - static_cast(numOfTilesLoaded) / static_cast(totalNum); - return (percentage * 100.f); + static_cast(completedSum) / static_cast(totalNum); + + return percentage * 100.0f; } void Tileset::forEachLoadedTile( @@ -1059,10 +1090,10 @@ bool Tileset::_kickDescendantsAndRenderTile( TileSelectionState::Result::Rendered; const bool wasReallyRenderedLastFrame = wasRenderedLastFrame && tile.isRenderable(); + const bool descendantsOverLimit = traversalDetails.notYetRenderableCount > + this->_options.loadingDescendantLimit; - if (!wasReallyRenderedLastFrame && - traversalDetails.notYetRenderableCount > - this->_options.loadingDescendantLimit && + if (!wasReallyRenderedLastFrame && descendantsOverLimit && !tile.isExternalContent() && !tile.getUnconditionallyRefine()) { // Remove all descendants from the load queues. @@ -1070,12 +1101,14 @@ bool Tileset::_kickDescendantsAndRenderTile( _workerThreadLoadQueue.size() + _mainThreadLoadQueue.size(); this->_workerThreadLoadQueue.erase( this->_workerThreadLoadQueue.begin() + - static_cast::iterator::difference_type>( + static_cast< + std::vector::iterator::difference_type>( workerThreadLoadQueueIndex), this->_workerThreadLoadQueue.end()); this->_mainThreadLoadQueue.erase( this->_mainThreadLoadQueue.begin() + - static_cast::iterator::difference_type>( + static_cast< + std::vector::iterator::difference_type>( mainThreadLoadQueueIndex), this->_mainThreadLoadQueue.end()); size_t allQueueEndSize = @@ -1411,25 +1444,24 @@ Tileset::TraversalDetails Tileset::_visitVisibleChildrenNearToFar( void Tileset::_processWorkerThreadLoadQueue() { CESIUM_TRACE("Tileset::_processWorkerThreadLoadQueue"); - int32_t maximumSimultaneousTileLoads = - static_cast(this->_options.maximumSimultaneousTileLoads); + TilesetContentManager* pManager = this->_pTilesetContentManager.get(); + ViewUpdateResult& result = this->_updateResult; - if (this->_pTilesetContentManager->getNumberOfTilesLoading() >= - maximumSimultaneousTileLoads) { - return; - } + result.workerThreadTileLoadQueueLength = this->_workerThreadLoadQueue.size(); - std::vector& queue = this->_workerThreadLoadQueue; - std::sort(queue.begin(), queue.end()); + pManager->processLoadRequests(this->_workerThreadLoadQueue, _options); - for (TileLoadTask& task : queue) { - this->_pTilesetContentManager->loadTileContent(*task.pTile, _options); - if (this->_pTilesetContentManager->getNumberOfTilesLoading() >= - maximumSimultaneousTileLoads) { - break; - } - } + result.tilesLoading = + static_cast(pManager->getNumberOfTilesLoading()); + result.tilesLoaded = + static_cast(pManager->getNumberOfTilesLoaded()); + result.rastersLoading = + static_cast(pManager->getNumberOfRastersLoading()); + result.rastersLoaded = + static_cast(pManager->getNumberOfRastersLoaded()); + result.activeWorkCount = pManager->getActiveWorkCount(); } + void Tileset::_processMainThreadLoadQueue() { CESIUM_TRACE("Tileset::_processMainThreadLoadQueue"); // Process deferred main-thread load tasks with a time budget. @@ -1443,7 +1475,7 @@ void Tileset::_processMainThreadLoadQueue() { auto start = std::chrono::system_clock::now(); auto end = start + std::chrono::milliseconds(static_cast(timeBudget)); - for (TileLoadTask& task : this->_mainThreadLoadQueue) { + for (TileLoadRequest& task : this->_mainThreadLoadQueue) { // We double-check that the tile is still in the ContentLoaded state here, // in case something (such as a child that needs to upsample from this // parent) already pushed the tile into the Done state. Because in that @@ -1451,6 +1483,7 @@ void Tileset::_processMainThreadLoadQueue() { if (task.pTile->getState() == TileLoadState::ContentLoaded && task.pTile->isRenderContent()) { this->_pTilesetContentManager->finishLoading(*task.pTile, this->_options); + ++this->_updateResult.mainThreadTotalTileLoads; } auto time = std::chrono::system_clock::now(); if (timeBudget > 0.0 && time >= end) { @@ -1458,6 +1491,9 @@ void Tileset::_processMainThreadLoadQueue() { } } + this->_updateResult.mainThreadTileLoadQueueLength = + this->_mainThreadLoadQueue.size(); + this->_mainThreadLoadQueue.clear(); } @@ -1467,8 +1503,8 @@ void Tileset::_unloadCachedTiles(double timeBudget) noexcept { const Tile* pRootTile = this->_pTilesetContentManager->getRootTile(); Tile* pTile = this->_loadedTiles.head(); - // A time budget of 0.0 indicates we shouldn't throttle cache unloads. So set - // the end time to the max time_point in that case. + // A time budget of 0.0 indicates we shouldn't throttle cache unloads. So + // set the end time to the max time_point in that case. auto start = std::chrono::system_clock::now(); auto end = (timeBudget <= 0.0) ? std::chrono::time_point::max() @@ -1520,13 +1556,13 @@ void Tileset::addTileToLoadQueue( std::find_if( this->_workerThreadLoadQueue.begin(), this->_workerThreadLoadQueue.end(), - [&](const TileLoadTask& task) { return task.pTile == &tile; }) == + [&](const TileLoadRequest& task) { return task.pTile == &tile; }) == this->_workerThreadLoadQueue.end()); assert( std::find_if( this->_mainThreadLoadQueue.begin(), this->_mainThreadLoadQueue.end(), - [&](const TileLoadTask& task) { return task.pTile == &tile; }) == + [&](const TileLoadRequest& task) { return task.pTile == &tile; }) == this->_mainThreadLoadQueue.end()); if (this->_pTilesetContentManager->tileNeedsWorkerThreadLoading(tile)) { diff --git a/Cesium3DTilesSelection/src/TilesetContentLoader.cpp b/Cesium3DTilesSelection/src/TilesetContentLoader.cpp index d147454bb..460d7dfe1 100644 --- a/Cesium3DTilesSelection/src/TilesetContentLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentLoader.cpp @@ -5,39 +5,52 @@ TileLoadInput::TileLoadInput( const Tile& tile_, const TilesetContentOptions& contentOptions_, const CesiumAsync::AsyncSystem& asyncSystem_, - const std::shared_ptr& pAssetAccessor_, const std::shared_ptr& pLogger_, - const std::vector& requestHeaders_) + const CesiumAsync::UrlResponseDataMap& responsesByUrl_) : tile{tile_}, contentOptions{contentOptions_}, asyncSystem{asyncSystem_}, - pAssetAccessor{pAssetAccessor_}, pLogger{pLogger_}, - requestHeaders{requestHeaders_} {} + responsesByUrl{responsesByUrl_} {} -TileLoadResult TileLoadResult::createFailedResult( - std::shared_ptr pCompletedRequest) { +TileLoadResult TileLoadResult::createFailedResult() { return TileLoadResult{ TileUnknownContent{}, CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - std::move(pCompletedRequest), + std::string(), {}, + CesiumAsync::RequestData{}, TileLoadResultState::Failed}; } -TileLoadResult TileLoadResult::createRetryLaterResult( - std::shared_ptr pCompletedRequest) { +TileLoadResult TileLoadResult::createRetryLaterResult() { return TileLoadResult{ TileUnknownContent{}, CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - std::move(pCompletedRequest), + std::string(), {}, + CesiumAsync::RequestData{}, TileLoadResultState::RetryLater}; } + +TileLoadResult +TileLoadResult::createRequestResult(const CesiumAsync::RequestData& request) { + return TileLoadResult{ + TileUnknownContent{}, + CesiumGeometry::Axis::Y, + std::nullopt, + std::nullopt, + std::nullopt, + std::string(), + {}, + request, + TileLoadResultState::RequestRequired}; +} + } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index edcbc8c66..9fac7e40d 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -21,6 +21,7 @@ #include #include +#include using namespace CesiumGltfContent; using namespace CesiumRasterOverlays; @@ -274,9 +275,20 @@ void createQuadtreeSubdividedChildren( std::vector mapOverlaysToTile( Tile& tile, RasterOverlayCollection& overlays, - const TilesetOptions& tilesetOptions) { - // when tile fails temporarily, it may still have mapped raster tiles, so - // clear it here + double maximumScreenSpaceError, + const std::vector& defaultHeaders, + std::vector& outWork) { + + // We may still have mapped raster tiles that need to be reset if the tile + // fails temporarily. It shouldn't be in the loading state, which would mean + // it's still in the work manager +#ifndef NDEBUG + for (RasterMappedTo3DTile& pMapped : tile.getMappedRasterTiles()) { + RasterOverlayTile* pLoading = pMapped.getLoadingTile(); + assert(pLoading); + assert(pLoading->getState() != RasterOverlayTile::LoadState::Loading); + } +#endif tile.getMappedRasterTiles().clear(); std::vector projections; @@ -286,20 +298,42 @@ std::vector mapOverlaysToTile( placeholders = overlays.getPlaceholderTileProviders(); assert(tileProviders.size() == placeholders.size()); + // Try to load now, but if tile is a placeholder this won't do anything for (size_t i = 0; i < tileProviders.size() && i < placeholders.size(); ++i) { RasterOverlayTileProvider& tileProvider = *tileProviders[i]; RasterOverlayTileProvider& placeholder = *placeholders[i]; - - RasterMappedTo3DTile* pMapped = RasterMappedTo3DTile::mapOverlayToTile( - tilesetOptions.maximumScreenSpaceError, + RasterMappedTo3DTile::mapOverlayToTile( + maximumScreenSpaceError, tileProvider, placeholder, tile, projections); - if (pMapped) { - // Try to load now, but if the mapped raster tile is a placeholder this - // won't do anything. - pMapped->loadThrottled(); + } + + // Get the work from the mapped tiles + for (RasterMappedTo3DTile& pMapped : tile.getMappedRasterTiles()) { + // Default headers come from the this. Loader can override if needed + CesiumAsync::RequestData requestData; + requestData.headers = defaultHeaders; + RasterProcessingCallback rasterCallback; + + // Can't do work without a loading tile + RasterOverlayTile* pLoadingTile = pMapped.getLoadingTile(); + if (!pLoadingTile) + continue; + + RasterOverlayTileProvider& provider = pLoadingTile->getTileProvider(); + provider.getLoadTileThrottledWork( + *pLoadingTile, + requestData, + rasterCallback); + + if (!requestData.url.empty() || rasterCallback != nullptr) { + TilesetContentManager::RasterWorkChain newWorkChain = { + &pMapped, + requestData, + rasterCallback}; + outWork.push_back(newWorkChain); } } @@ -473,8 +507,8 @@ void postProcessGltfInWorkerThread( const TileContentLoadInfo& tileLoadInfo) { CesiumGltf::Model& model = std::get(result.contentKind); - if (result.pCompletedRequest) { - model.extras["Cesium3DTiles_TileUrl"] = result.pCompletedRequest->url(); + if (!result.originalRequestUrl.empty()) { + model.extras["Cesium3DTiles_TileUrl"] = result.originalRequestUrl; } // have to pass the up axis to extra for backward compatibility @@ -497,11 +531,12 @@ void postProcessGltfInWorkerThread( } } -CesiumAsync::Future -postProcessContentInWorkerThread( +CesiumAsync::Future postProcessContent( TileLoadResult&& result, std::vector&& projections, TileContentLoadInfo&& tileLoadInfo, + const std::string& requestBaseUrl, + const std::vector& requestHeaders, const std::any& rendererOptions) { assert( result.state == TileLoadResultState::Success && @@ -512,11 +547,10 @@ postProcessContentInWorkerThread( // Download any external image or buffer urls in the gltf if there are any CesiumGltfReader::GltfReaderResult gltfResult{std::move(model), {}, {}}; - CesiumAsync::HttpHeaders requestHeaders; - std::string baseUrl; - if (result.pCompletedRequest) { - requestHeaders = result.pCompletedRequest->headers(); - baseUrl = result.pCompletedRequest->url(); + CesiumAsync::HttpHeaders httpHeaders; + if (!requestBaseUrl.empty()) { + for (auto pair : requestHeaders) + httpHeaders[pair.first] = pair.second; } CesiumGltfReader::GltfReaderOptions gltfOptions; @@ -529,23 +563,25 @@ postProcessContentInWorkerThread( auto pAssetAccessor = tileLoadInfo.pAssetAccessor; return CesiumGltfReader::GltfReader::resolveExternalData( asyncSystem, - baseUrl, - requestHeaders, + requestBaseUrl, + httpHeaders, pAssetAccessor, gltfOptions, std::move(gltfResult)) - .thenInWorkerThread( + .thenImmediately( + // Run this immediately. In non-error cases, we're already in a worker [result = std::move(result), projections = std::move(projections), tileLoadInfo = std::move(tileLoadInfo), + requestBaseUrl, rendererOptions]( CesiumGltfReader::GltfReaderResult&& gltfResult) mutable { if (!gltfResult.errors.empty()) { - if (result.pCompletedRequest) { + if (!requestBaseUrl.empty()) { SPDLOG_LOGGER_ERROR( tileLoadInfo.pLogger, "Failed resolving external glTF buffers from {}:\n- {}", - result.pCompletedRequest->url(), + requestBaseUrl, CesiumUtility::joinToString(gltfResult.errors, "\n- ")); } else { SPDLOG_LOGGER_ERROR( @@ -556,12 +592,12 @@ postProcessContentInWorkerThread( } if (!gltfResult.warnings.empty()) { - if (result.pCompletedRequest) { + if (!requestBaseUrl.empty()) { SPDLOG_LOGGER_WARN( tileLoadInfo.pLogger, "Warning when resolving external gltf buffers from " "{}:\n- {}", - result.pCompletedRequest->url(), + requestBaseUrl, CesiumUtility::joinToString(gltfResult.errors, "\n- ")); } else { SPDLOG_LOGGER_ERROR( @@ -574,7 +610,7 @@ postProcessContentInWorkerThread( if (!gltfResult.model) { return tileLoadInfo.asyncSystem.createResolvedFuture( TileLoadResultAndRenderResources{ - TileLoadResult::createFailedResult(nullptr), + TileLoadResult::createFailedResult(), nullptr}); } @@ -617,12 +653,16 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, + _rasterLoadsInProgress{0}, + _loadedRastersCount{0}, _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, _rootTileAvailableFuture{ this->_rootTileAvailablePromise.getFuture().share()} { + createWorkManager(externals); + this->_rootTileAvailablePromise.resolve(); } @@ -646,12 +686,16 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, + _rasterLoadsInProgress{0}, + _loadedRastersCount{0}, _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, _rootTileAvailableFuture{ this->_rootTileAvailablePromise.getFuture().share()} { + createWorkManager(externals); + if (!url.empty()) { this->notifyTileStartLoading(nullptr); @@ -785,12 +829,16 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, + _rasterLoadsInProgress{0}, + _loadedRastersCount{0}, _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, _rootTileAvailableFuture{ this->_rootTileAvailablePromise.getFuture().share()} { + createWorkManager(externals); + if (ionAssetID > 0) { auto authorizationChangeListener = [this]( const std::string& header, @@ -841,6 +889,32 @@ TilesetContentManager::TilesetContentManager( } } +void TilesetContentManager::createWorkManager( + const TilesetExternals& externals) { + _pTileWorkManager = std::make_shared( + externals.asyncSystem, + externals.pAssetAccessor, + externals.pLogger); + + TileWorkManager::TileDispatchFunc tileDispatch = + [this]( + TileProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work) { + return this->dispatchTileWork(processingData, responseDataMap, work); + }; + + TileWorkManager::RasterDispatchFunc rasterDispatch = + [this]( + RasterProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work) { + return this->dispatchRasterWork(processingData, responseDataMap, work); + }; + + _pTileWorkManager->SetDispatchFunctions(tileDispatch, rasterDispatch); +} + CesiumAsync::SharedFuture& TilesetContentManager::getAsyncDestructionCompleteEvent() { return this->_destructionCompleteFuture; @@ -853,149 +927,40 @@ TilesetContentManager::getRootTileAvailableEvent() { TilesetContentManager::~TilesetContentManager() noexcept { assert(this->_tileLoadsInProgress == 0); + assert(this->_rasterLoadsInProgress == 0); this->unloadAll(); this->_destructionCompletePromise.resolve(); } -void TilesetContentManager::loadTileContent( - Tile& tile, - const TilesetOptions& tilesetOptions) { - CESIUM_TRACE("TilesetContentManager::loadTileContent"); +void TilesetContentManager::processLoadRequests( + std::vector& requests, + TilesetOptions& options) { - if (tile.getState() == TileLoadState::Unloading) { - // We can't load a tile that is unloading; it has to finish unloading first. - return; - } + std::vector orders; + discoverLoadWork(requests, options.maximumScreenSpaceError, options, orders); - if (tile.getState() != TileLoadState::Unloaded && - tile.getState() != TileLoadState::FailedTemporarily) { - // No need to load geometry, but give previously-throttled - // raster overlay tiles a chance to load. - for (RasterMappedTo3DTile& rasterTile : tile.getMappedRasterTiles()) { - rasterTile.loadThrottled(); - } + assert(options.maximumSimultaneousTileLoads > 0); + size_t maxTileLoads = + static_cast(options.maximumSimultaneousTileLoads); - return; - } + std::vector workCreated; + TileWorkManager::TryAddOrders( + this->_pTileWorkManager, + orders, + maxTileLoads, + workCreated); - // Below are the guarantees the loader can assume about upsampled tile. If any - // of those guarantees are wrong, it's a bug: - // - Any tile that is marked as upsampled tile, we will guarantee that the - // parent is always loaded. It lets the loader takes care of upsampling only - // without requesting the parent tile. If a loader tries to upsample tile, but - // the parent is not loaded, it is a bug. - // - This manager will also guarantee that the parent tile will be alive until - // the upsampled tile content returns to the main thread. So the loader can - // capture the parent geometry by reference in the worker thread to upsample - // the current tile. Warning: it's not thread-safe to modify the parent - // geometry in the worker thread at the same time though - const CesiumGeometry::UpsampledQuadtreeNode* pUpsampleID = - std::get_if(&tile.getTileID()); - if (pUpsampleID) { - // We can't upsample this tile until its parent tile is done loading. - Tile* pParentTile = tile.getParent(); - if (pParentTile) { - if (pParentTile->getState() != TileLoadState::Done) { - loadTileContent(*pParentTile, tilesetOptions); - - // Finalize the parent if necessary, otherwise it may never reach the - // Done state. Also double check that we have render content in ensure - // we don't assert / crash in finishLoading. The latter will only ever - // be a problem in a pathological tileset with a non-renderable leaf - // tile, but that sort of thing does happen. - if (pParentTile->getState() == TileLoadState::ContentLoaded && - pParentTile->isRenderContent()) { - finishLoading(*pParentTile, tilesetOptions); - } - return; - } - } else { - // we cannot upsample this tile if it doesn't have parent - return; - } - } - - // map raster overlay to tile - std::vector projections = - mapOverlaysToTile(tile, this->_overlayCollection, tilesetOptions); - - // begin loading tile - notifyTileStartLoading(&tile); - tile.setState(TileLoadState::ContentLoading); - - TileContentLoadInfo tileLoadInfo{ - this->_externals.asyncSystem, - this->_externals.pAssetAccessor, - this->_externals.pPrepareRendererResources, - this->_externals.pLogger, - tilesetOptions.contentOptions, - tile}; - - TilesetContentLoader* pLoader; - if (tile.getLoader() == &this->_upsampler) { - pLoader = &this->_upsampler; - } else { - pLoader = this->_pLoader.get(); - } + markWorkTilesAsLoading(workCreated); - TileLoadInput loadInput{ - tile, - tilesetOptions.contentOptions, - this->_externals.asyncSystem, - this->_externals.pAssetAccessor, - this->_externals.pLogger, - this->_requestHeaders}; + // Dispatch more processing work. More may have been added, or slots may have + // freed up from any work that completed after update_view called + // dispatchMainThreadTasks and now + TileWorkManager::TryDispatchProcessing(this->_pTileWorkManager); - // Keep the manager alive while the load is in progress. - CesiumUtility::IntrusivePointer thiz = this; - - pLoader->loadTileContent(loadInput) - .thenImmediately([tileLoadInfo = std::move(tileLoadInfo), - projections = std::move(projections), - rendererOptions = tilesetOptions.rendererOptions]( - TileLoadResult&& result) mutable { - // the reason we run immediate continuation, instead of in the - // worker thread, is that the loader may run the task in the main - // thread. And most often than not, those main thread task is very - // light weight. So when those tasks return, there is no need to - // spawn another worker thread if the result of the task isn't - // related to render content. We only ever spawn a new task in the - // worker thread if the content is a render content - if (result.state == TileLoadResultState::Success) { - if (std::holds_alternative(result.contentKind)) { - auto asyncSystem = tileLoadInfo.asyncSystem; - return asyncSystem.runInWorkerThread( - [result = std::move(result), - projections = std::move(projections), - tileLoadInfo = std::move(tileLoadInfo), - rendererOptions]() mutable { - return postProcessContentInWorkerThread( - std::move(result), - std::move(projections), - std::move(tileLoadInfo), - rendererOptions); - }); - } - } - - return tileLoadInfo.asyncSystem - .createResolvedFuture( - {std::move(result), nullptr}); - }) - .thenInMainThread([&tile, thiz](TileLoadResultAndRenderResources&& pair) { - setTileContent(tile, std::move(pair.result), pair.pRenderResources); - - thiz->notifyTileDoneLoading(&tile); - }) - .catchInMainThread([pLogger = this->_externals.pLogger, &tile, thiz]( - std::exception&& e) { - thiz->notifyTileDoneLoading(&tile); - SPDLOG_LOGGER_ERROR( - pLogger, - "An unexpected error occurs when loading tile: {}", - e.what()); - }); + // Finish main thread tasks for any work that completed after update_view + // called dispatchMainThreadTasks and now + handleCompletedWork(); } void TilesetContentManager::updateTileContent( @@ -1043,6 +1008,15 @@ bool TilesetContentManager::unloadTileContent(Tile& tile) { return false; } + // Are any raster mapped tiles currently loading? + // If so, wait until they are done before unloading + for (RasterMappedTo3DTile& mapped : tile.getMappedRasterTiles()) { + RasterOverlayTile* pLoadingTile = mapped.getLoadingTile(); + if (pLoadingTile && + pLoadingTile->getState() == RasterOverlayTile::LoadState::Loading) + return false; + } + // Detach raster tiles first so that the renderer's tile free // process doesn't need to worry about them. for (RasterMappedTo3DTile& mapped : tile.getMappedRasterTiles()) { @@ -1086,6 +1060,8 @@ bool TilesetContentManager::unloadTileContent(Tile& tile) { } void TilesetContentManager::unloadAll() { + this->_pTileWorkManager->Shutdown(); + // TODO: use the linked-list of loaded tiles instead of walking the entire // tile tree. if (this->_pRootTile) { @@ -1094,25 +1070,15 @@ void TilesetContentManager::unloadAll() { } void TilesetContentManager::waitUntilIdle() { - // Wait for all asynchronous loading to terminate. - // If you're hanging here, it's most likely caused by _tileLoadsInProgress not - // being decremented correctly when an async load ends. - while (this->_tileLoadsInProgress > 0) { + // Tiles are loaded either on construction (root tile) or through the work + // manager. Wait for all asynchronous loading to terminate. + bool workInProgress = this->_tileLoadsInProgress > 0 || + this->_pTileWorkManager->GetActiveWorkCount() > 0; + while (workInProgress) { this->_externals.pAssetAccessor->tick(); this->_externals.asyncSystem.dispatchMainThreadTasks(); - } - - // Wait for all overlays to wrap up their loading, too. - uint32_t rasterOverlayTilesLoading = 1; - while (rasterOverlayTilesLoading > 0) { - this->_externals.pAssetAccessor->tick(); - this->_externals.asyncSystem.dispatchMainThreadTasks(); - - rasterOverlayTilesLoading = 0; - for (const auto& pTileProvider : - this->_overlayCollection.getTileProviders()) { - rasterOverlayTilesLoading += pTileProvider->getNumberOfTilesLoading(); - } + workInProgress = this->_tileLoadsInProgress > 0 || + this->_pTileWorkManager->GetActiveWorkCount() > 0; } } @@ -1175,6 +1141,30 @@ int64_t TilesetContentManager::getTotalDataUsed() const noexcept { return bytes; } +int32_t TilesetContentManager::getNumberOfRastersLoading() const noexcept { + return this->_rasterLoadsInProgress; +} + +int32_t TilesetContentManager::getNumberOfRastersLoaded() const noexcept { + return this->_loadedRastersCount; +} + +size_t TilesetContentManager::getActiveWorkCount() { + return this->_pTileWorkManager->GetActiveWorkCount(); +} + +void TilesetContentManager::getLoadingWorkStats( + size_t& requestCount, + size_t& inFlightCount, + size_t& processingCount, + size_t& failedCount) { + return this->_pTileWorkManager->GetLoadingWorkStats( + requestCount, + inFlightCount, + processingCount, + failedCount); +} + bool TilesetContentManager::tileNeedsWorkerThreadLoading( const Tile& tile) const noexcept { auto state = tile.getState(); @@ -1447,6 +1437,18 @@ void TilesetContentManager::unloadDoneState(Tile& tile) { pRenderContent->setRenderResources(nullptr); } +void TilesetContentManager::notifyRasterStartLoading() noexcept { + ++this->_rasterLoadsInProgress; +} + +void TilesetContentManager::notifyRasterDoneLoading() noexcept { + assert( + this->_rasterLoadsInProgress > 0 && + "There are no raster loads currently in flight"); + --this->_rasterLoadsInProgress; + ++this->_loadedRastersCount; +} + void TilesetContentManager::notifyTileStartLoading( [[maybe_unused]] const Tile* pTile) noexcept { ++this->_tileLoadsInProgress; @@ -1511,4 +1513,454 @@ void TilesetContentManager::propagateTilesetContentLoaderResult( this->_pRootTile = std::move(result.pRootTile); } } + +void TilesetContentManager::discoverLoadWork( + const std::vector& requests, + double maximumScreenSpaceError, + const TilesetOptions& tilesetOptions, + std::vector& outOrders) { + std::set tileWorkAdded; + for (const TileLoadRequest& loadRequest : requests) { + // Failed tiles don't get another chance + if (loadRequest.pTile->getState() == TileLoadState::Failed) + continue; + + std::vector parsedTileWork; + this->parseTileWork( + loadRequest.pTile, + 0, + maximumScreenSpaceError, + parsedTileWork); + + // It's valid for a tile to not have any work + // It may be waiting for a parent tile to complete + if (parsedTileWork.empty()) + continue; + + // Sort by depth, which should bubble parent tasks up to the top + std::sort(parsedTileWork.begin(), parsedTileWork.end()); + + // Work with max depth is at top of list + size_t maxDepth = parsedTileWork.begin()->depthIndex; + + // Add all the work, biasing priority by depth + // Give parents a higher priority (lower value) + for (ParsedTileWork& work : parsedTileWork) { + double priorityBias = double(maxDepth - work.depthIndex); + double resultPriority = loadRequest.priority + priorityBias; + + // We always need a source (non raster) tile + assert(work.tileWorkChain.isValid()); + Tile* pTile = work.tileWorkChain.pTile; + + // If order for source tile already exists, skip adding more work for it + // Ex. Tile work needs to load its parent, and multiple children point + // to that same parent. Don't add the parent more than once + if (tileWorkAdded.find(pTile) != tileWorkAdded.end()) + continue; + tileWorkAdded.insert(pTile); + + auto& newOrder = outOrders.emplace_back(TileWorkManager::Order{ + std::move(work.tileWorkChain.requestData), + TileProcessingData{ + work.tileWorkChain.pTile, + work.tileWorkChain.tileCallback, + work.projections, + tilesetOptions.contentOptions, + tilesetOptions.rendererOptions}, + loadRequest.group, + resultPriority}); + + // Embed child work in parent + for (auto& rasterWorkChain : work.rasterWorkChains) { + assert(rasterWorkChain.isValid()); + newOrder.childOrders.emplace_back(TileWorkManager::Order{ + std::move(rasterWorkChain.requestData), + RasterProcessingData{ + rasterWorkChain.pRasterTile, + rasterWorkChain.rasterCallback}, + loadRequest.group, + resultPriority}); + } + } + } +} + +void TilesetContentManager::markWorkTilesAsLoading( + const std::vector& workVector) { + + for (const TileWorkManager::Work* work : workVector) { + if (std::holds_alternative( + work->order.processingData)) { + TileProcessingData tileProcessing = + std::get(work->order.processingData); + assert(tileProcessing.pTile); + assert( + tileProcessing.pTile->getState() == TileLoadState::Unloaded || + tileProcessing.pTile->getState() == TileLoadState::FailedTemporarily); + + tileProcessing.pTile->setState(TileLoadState::ContentLoading); + } else { + RasterProcessingData rasterProcessing = + std::get(work->order.processingData); + assert(rasterProcessing.pRasterTile); + + RasterOverlayTile* pLoading = + rasterProcessing.pRasterTile->getLoadingTile(); + assert(pLoading); + assert(pLoading->getState() == RasterOverlayTile::LoadState::Unloaded); + + pLoading->setState(RasterOverlayTile::LoadState::Loading); + } + } +} + +void TilesetContentManager::handleCompletedWork() { + std::vector doneOrders; + std::vector failedOrders; + _pTileWorkManager->TakeCompletedWork(doneOrders, failedOrders); + + for (auto& doneOrder : doneOrders) { + const TileWorkManager::Order& order = doneOrder.order; + + if (std::holds_alternative(order.processingData)) { + TileProcessingData tileProcessing = + std::get(order.processingData); + assert(tileProcessing.pTile); + + this->setTileContent( + *tileProcessing.pTile, + std::move(doneOrder.loadResult), + doneOrder.pRenderResources); + }; + } + + for (auto& failedOrder : failedOrders) { + const TileWorkManager::Order& order = failedOrder.order; + + SPDLOG_LOGGER_ERROR( + this->_externals.pLogger, + "{}: {}", + failedOrder.failureReason, + order.requestData.url); + + if (std::holds_alternative(order.processingData)) { + TileProcessingData tileProcessing = + std::get(order.processingData); + assert(tileProcessing.pTile); + tileProcessing.pTile->setState(TileLoadState::Failed); + } else { + RasterProcessingData rasterProcessing = + std::get(order.processingData); + assert(rasterProcessing.pRasterTile); + + RasterOverlayTile* pLoading = + rasterProcessing.pRasterTile->getLoadingTile(); + assert(pLoading); + + pLoading->setState(RasterOverlayTile::LoadState::Failed); + } + } +} + +void TilesetContentManager::dispatchTileWork( + TileProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work) { + Tile* pTile = processingData.pTile; + + // Optionally could move this to work manager + this->notifyTileStartLoading(pTile); + + // Keep the manager alive while the load is in progress. + CesiumUtility::IntrusivePointer thiz = this; + + TileContentLoadInfo tileLoadInfo{ + this->_externals.asyncSystem, + this->_externals.pAssetAccessor, + this->_externals.pPrepareRendererResources, + this->_externals.pLogger, + processingData.contentOptions, + *pTile}; + + TilesetContentLoader* pLoader; + if (pTile->getLoader() == &this->_upsampler) { + pLoader = &this->_upsampler; + } else { + pLoader = this->_pLoader.get(); + } + + TileLoadInput loadInput{ + *pTile, + processingData.contentOptions, + this->_externals.asyncSystem, + this->_externals.pLogger, + responseDataMap}; + + assert(processingData.loaderCallback); + + processingData.loaderCallback(loadInput, pLoader) + .thenImmediately([tileLoadInfo = std::move(tileLoadInfo), + &requestHeaders = this->_requestHeaders, + &projections = processingData.projections, + &rendererOptions = processingData.rendererOptions, + pThis = this, + pWorkManager = thiz->_pTileWorkManager, + _work = work](TileLoadResult&& result) mutable { + // the reason we run immediate continuation, instead of in the + // worker thread, is that the loader may run the task in the main + // thread. And most often than not, those main thread task is very + // light weight. So when those tasks return, there is no need to + // spawn another worker thread if the result of the task isn't + // related to render content. We only ever spawn a new task in the + // worker thread if the content is a render content + if (result.state == TileLoadResultState::RequestRequired) { + // This work goes back into the work manager queue + CesiumAsync::RequestData& request = result.additionalRequestData; + + // Add new requests here + assert( + _work->completedRequests.find(request.url) == + _work->completedRequests.end()); + _work->pendingRequests.push_back(std::move(request)); + + TileWorkManager::RequeueWorkForRequest(pWorkManager, _work); + + return tileLoadInfo.asyncSystem + .createResolvedFuture( + TileLoadResultState::RequestRequired); + } + + if (result.state == TileLoadResultState::Success && + std::holds_alternative(result.contentKind)) { + return postProcessContent( + std::move(result), + std::move(projections), + std::move(tileLoadInfo), + result.originalRequestUrl, + requestHeaders, + rendererOptions) + .thenInMainThread( + [pThis = pThis, pWorkManager = pWorkManager, _work = _work]( + TileLoadResultAndRenderResources&& pair) mutable { + _work->tileLoadResult = std::move(pair.result); + _work->pRenderResources = pair.pRenderResources; + pWorkManager->SignalWorkComplete(_work); + + pThis->handleCompletedWork(); + TileWorkManager::TryDispatchProcessing(pWorkManager); + return TileLoadResultState::Success; + }); + } + + // We're successful with no gltf model, or in a failure state + _work->tileLoadResult = std::move(result); + pWorkManager->SignalWorkComplete(_work); + + return tileLoadInfo.asyncSystem.runInMainThread( + [pThis = pThis, + pWorkManager = pWorkManager, + state = _work->tileLoadResult.state]() mutable { + pThis->handleCompletedWork(); + TileWorkManager::TryDispatchProcessing(pWorkManager); + return state; + }); + }) + .thenInMainThread( + [_thiz = thiz, _pTile = pTile](TileLoadResultState state) mutable { + // Wrap up this tile and also keep intrusive pointer alive + if (state == TileLoadResultState::Success) + _thiz->notifyTileDoneLoading(_pTile); + else + _thiz->notifyTileDoneLoading(nullptr); + }) + .catchInMainThread( + [_pTile = pTile, _thiz = this, pLogger = this->_externals.pLogger]( + std::exception&& e) { + _pTile->setState(TileLoadState::Failed); + + _thiz->notifyTileDoneLoading(_pTile); + SPDLOG_LOGGER_ERROR( + pLogger, + "An unexpected error occurs when loading tile: {}", + e.what()); + }); +} + +void TilesetContentManager::dispatchRasterWork( + RasterProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work) { + RasterMappedTo3DTile* pRasterTile = processingData.pRasterTile; + assert(pRasterTile); + + RasterOverlayTile* pLoadingTile = pRasterTile->getLoadingTile(); + if (!pLoadingTile) { + // Can't do any work + this->_pTileWorkManager->SignalWorkComplete(work); + return; + } + + // Optionally could move this to work manager + this->notifyRasterStartLoading(); + + RasterOverlayTileProvider& provider = pLoadingTile->getTileProvider(); + + // Keep the these objects alive while the load is in progress. + CesiumUtility::IntrusivePointer thiz = this; + IntrusivePointer pTile = pLoadingTile; + IntrusivePointer pProvider = &provider; + + provider + .loadTileThrottled( + *pLoadingTile, + responseDataMap, + processingData.rasterCallback) + .thenImmediately([pWorkManager = thiz->_pTileWorkManager, + _work = work](RasterLoadResult&& result) mutable { + if (result.state == RasterOverlayTile::LoadState::RequestRequired) { + // This work goes back into the work manager queue + assert(!result.missingRequests.empty()); + + for (auto& request : result.missingRequests) { + // Make sure we're not requesting something we have +#ifndef NDEBUG + assert( + _work->completedRequests.find(request.url) == + _work->completedRequests.end()); + for (auto pending : _work->pendingRequests) + assert(pending.url != request.url); +#endif + // Add new requests here + _work->pendingRequests.push_back(std::move(request)); + } + + TileWorkManager::RequeueWorkForRequest(pWorkManager, _work); + } else { + pWorkManager->SignalWorkComplete(_work); + } + + return std::move(result); + }) + .thenInMainThread([_thiz = thiz, pTile = pTile, pProvider = pProvider]( + RasterLoadResult&& result) mutable { + if (result.state == RasterOverlayTile::LoadState::RequestRequired) { + // Nothing to do + } else { + pTile->_rectangle = result.rectangle; + pTile->_pRendererResources = result.pRendererResources; + assert(result.image.has_value()); + pTile->_image = std::move(result.image.value()); + pTile->_tileCredits = std::move(result.credits); + pTile->_moreDetailAvailable = + result.moreDetailAvailable + ? RasterOverlayTile::MoreDetailAvailable::Yes + : RasterOverlayTile::MoreDetailAvailable::No; + pTile->setState(result.state); + + result.pTile = pTile; + + pProvider->incrementTileDataBytes(pTile->getImage()); + } + + _thiz->notifyRasterDoneLoading(); + + TileWorkManager::TryDispatchProcessing(_thiz->_pTileWorkManager); + }) + .catchInMainThread( + [_thiz = thiz, pTile = pLoadingTile](const std::exception& /*e*/) { + pTile->_pRendererResources = nullptr; + pTile->_image = {}; + pTile->_tileCredits = {}; + pTile->_moreDetailAvailable = + RasterOverlayTile::MoreDetailAvailable::No; + pTile->setState(RasterOverlayTile::LoadState::Failed); + + _thiz->notifyRasterDoneLoading(); + }); +} + +void TilesetContentManager::parseTileWork( + Tile* pTile, + size_t depthIndex, + double maximumScreenSpaceError, + std::vector& outWork) { + CESIUM_TRACE("TilesetContentManager::parseTileWork"); + + // We can't load a tile that is unloading; it has to finish unloading first. + if (pTile->getState() == TileLoadState::Unloading) + return; + + assert( + pTile->getState() == TileLoadState::Unloaded || + pTile->getState() == TileLoadState::FailedTemporarily); + + // Below are the guarantees the loader can assume about upsampled tile. If any + // of those guarantees are wrong, it's a bug: + // - Any tile that is marked as upsampled tile, we will guarantee that the + // parent is always loaded. It lets the loader takes care of upsampling only + // without requesting the parent tile. If a loader tries to upsample tile, but + // the parent is not loaded, it is a bug. + // - This manager will also guarantee that the parent tile will be alive until + // the upsampled tile content returns to the main thread. So the loader can + // capture the parent geometry by reference in the worker thread to upsample + // the current tile. Warning: it's not thread-safe to modify the parent + // geometry in the worker thread at the same time though + const CesiumGeometry::UpsampledQuadtreeNode* pUpsampleID = + std::get_if(&pTile->getTileID()); + if (pUpsampleID) { + // We can't upsample this tile if no parent + Tile* pParentTile = pTile->getParent(); + if (!pParentTile) + return; + + TileLoadState parentState = pParentTile->getState(); + + // If not currently loading, queue some work + if (parentState < TileLoadState::ContentLoading) { + parseTileWork( + pParentTile, + depthIndex + 1, + maximumScreenSpaceError, + outWork); + return; + } + + // We can't proceed until our parent is done. Wait another tick + if (parentState != TileLoadState::Done) + return; + + // Parent is done, continue adding work for this tile + } + + // Parse any content fetch work + TilesetContentLoader* pLoader; + if (pTile->getLoader() == &this->_upsampler) { + pLoader = &this->_upsampler; + } else { + pLoader = this->_pLoader.get(); + } + + // Default headers come from the this. Loader can override if needed + CesiumAsync::RequestData requestData; + requestData.headers = this->_requestHeaders; + TileLoaderCallback tileCallback; + + if (pLoader->getLoadWork(pTile, requestData, tileCallback)) { + // New work was found, add it and any raster work + ParsedTileWork newWork = { + depthIndex, + TileWorkChain{pTile, requestData, tileCallback}}; + + newWork.projections = mapOverlaysToTile( + *pTile, + this->_overlayCollection, + maximumScreenSpaceError, + this->_requestHeaders, + newWork.rasterWorkChains); + + outWork.push_back(newWork); + } +} + } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index d750b2207..4d2c40646 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -60,7 +62,51 @@ class TilesetContentManager ~TilesetContentManager() noexcept; - void loadTileContent(Tile& tile, const TilesetOptions& tilesetOptions); + struct TileWorkChain { + Tile* pTile; + CesiumAsync::RequestData requestData; + TileLoaderCallback tileCallback; + + // Must have a valid tile and some work + bool isValid() { + return pTile && (!requestData.url.empty() || tileCallback); + } + }; + + struct RasterWorkChain { + RasterMappedTo3DTile* pRasterTile; + CesiumAsync::RequestData requestData; + CesiumRasterOverlays::RasterProcessingCallback rasterCallback; + + // Must have a valid tile and some work + bool isValid() { + return pRasterTile && (!requestData.url.empty() || rasterCallback); + } + }; + + struct ParsedTileWork { + size_t depthIndex = 0; + + TileWorkChain tileWorkChain = {}; + + std::vector rasterWorkChains = {}; + + std::vector projections = {}; + + bool operator<(const ParsedTileWork& rhs) const noexcept { + return this->depthIndex > rhs.depthIndex; + } + }; + + void processLoadRequests( + std::vector& requests, + TilesetOptions& options); + + void parseTileWork( + Tile* pTile, + size_t depthIndex, + double maximumScreenSpaceError, + std::vector& outWork); void updateTileContent(Tile& tile, const TilesetOptions& tilesetOptions); @@ -100,6 +146,18 @@ class TilesetContentManager int64_t getTotalDataUsed() const noexcept; + int32_t getNumberOfRastersLoading() const noexcept; + + int32_t getNumberOfRastersLoaded() const noexcept; + + size_t getActiveWorkCount(); + + void getLoadingWorkStats( + size_t& requestCount, + size_t& inFlightCount, + size_t& processingCount, + size_t& failedCount); + bool tileNeedsWorkerThreadLoading(const Tile& tile) const noexcept; bool tileNeedsMainThreadLoading(const Tile& tile) const noexcept; @@ -127,6 +185,10 @@ class TilesetContentManager void notifyTileUnloading(const Tile* pTile) noexcept; + void notifyRasterStartLoading() noexcept; + + void notifyRasterDoneLoading() noexcept; + template void propagateTilesetContentLoaderResult( TilesetLoadType type, @@ -134,6 +196,29 @@ class TilesetContentManager loadErrorCallback, TilesetContentLoaderResult&& result); + void createWorkManager(const TilesetExternals& externals); + + void discoverLoadWork( + const std::vector& requests, + double maximumScreenSpaceError, + const TilesetOptions& tilesetOptions, + std::vector& outOrders); + + void markWorkTilesAsLoading( + const std::vector& workVector); + + void handleCompletedWork(); + + void dispatchTileWork( + TileProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work); + + void dispatchRasterWork( + RasterProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work); + TilesetExternals _externals; std::vector _requestHeaders; std::unique_ptr _pLoader; @@ -142,10 +227,17 @@ class TilesetContentManager std::vector _tilesetCredits; RasterOverlayUpsampler _upsampler; RasterOverlayCollection _overlayCollection; + + // Thread safe shared pointer + std::shared_ptr _pTileWorkManager; + int32_t _tileLoadsInProgress; int32_t _loadedTilesCount; int64_t _tilesDataUsed; + int32_t _rasterLoadsInProgress; + int32_t _loadedRastersCount; + CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp index 70e0a028b..22045f2a8 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp @@ -670,24 +670,21 @@ TileLoadResult parseExternalTilesetInWorkerThread( CesiumGeometry::Axis upAxis, TileRefine tileRefine, const std::shared_ptr& pLogger, - std::shared_ptr&& pCompletedRequest, + const std::string& tileUrl, + const gsl::span& responseBytes, ExternalContentInitializer&& externalContentInitializer) { // create external tileset - const CesiumAsync::IAssetResponse* pResponse = pCompletedRequest->response(); - const auto& responseData = pResponse->data(); - const auto& tileUrl = pCompletedRequest->url(); - rapidjson::Document tilesetJson; tilesetJson.Parse( - reinterpret_cast(responseData.data()), - responseData.size()); + reinterpret_cast(responseBytes.data()), + responseBytes.size()); if (tilesetJson.HasParseError()) { SPDLOG_LOGGER_ERROR( pLogger, "Error when parsing tileset JSON, error code {} at byte offset {}", tilesetJson.GetParseError(), tilesetJson.GetErrorOffset()); - return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); + return TileLoadResult::createFailedResult(); } // Save the parsed external tileset into custom data. @@ -713,7 +710,7 @@ TileLoadResult parseExternalTilesetInWorkerThread( logTileLoadResult(pLogger, tileUrl, errors); // since the json cannot be parsed, we don't know the content of this tile - return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); + return TileLoadResult::createFailedResult(); } externalContentInitializer.pExternalTilesetLoaders = @@ -727,8 +724,9 @@ TileLoadResult parseExternalTilesetInWorkerThread( std::nullopt, std::nullopt, std::nullopt, - std::move(pCompletedRequest), + tileUrl, std::move(externalContentInitializer), + CesiumAsync::RequestData{}, TileLoadResultState::Success}; } @@ -841,10 +839,31 @@ TilesetJsonLoader::loadTileContent(const TileLoadInput& loadInput) { } // this loader only handles Url ID - const std::string* url = std::get_if(&tile.getTileID()); - if (!url) { + const std::string* urlFromTileId = + std::get_if(&tile.getTileID()); + if (!urlFromTileId) { return loadInput.asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); + } + + const auto& pLogger = loadInput.pLogger; + const auto& responsesByUrl = loadInput.responsesByUrl; + + assert(responsesByUrl.size() == 1); + const std::string& responseUrl = responsesByUrl.begin()->first; + const CesiumAsync::IAssetResponse* pResponse = + responsesByUrl.begin()->second.pResponse; + assert(pResponse); + + uint16_t statusCode = pResponse->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + responseUrl); + return loadInput.asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult()); } const glm::dmat4& tileTransform = tile.getTransform(); @@ -853,87 +872,85 @@ TilesetJsonLoader::loadTileContent(const TileLoadInput& loadInput) { ExternalContentInitializer externalContentInitializer{nullptr, this, {}}; const auto& asyncSystem = loadInput.asyncSystem; - const auto& pAssetAccessor = loadInput.pAssetAccessor; - const auto& pLogger = loadInput.pLogger; - const auto& requestHeaders = loadInput.requestHeaders; const auto& contentOptions = loadInput.contentOptions; - std::string resolvedUrl = - CesiumUtility::Uri::resolve(this->_baseUrl, *url, true); - return pAssetAccessor->get(asyncSystem, resolvedUrl, requestHeaders) - .thenInWorkerThread( - [pLogger, - contentOptions, - tileTransform, - tileRefine, - upAxis = _upAxis, - externalContentInitializer = std::move(externalContentInitializer)]( - std::shared_ptr&& - pCompletedRequest) mutable { - auto pResponse = pCompletedRequest->response(); - const std::string& tileUrl = pCompletedRequest->url(); - if (!pResponse) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Did not receive a valid response for tile content {}", - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - uint16_t statusCode = pResponse->statusCode(); - if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Received status code {} for tile content {}", - statusCode, - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - // find gltf converter - const auto& responseData = pResponse->data(); - auto converter = GltfConverters::getConverterByMagic(responseData); - if (!converter) { - converter = GltfConverters::getConverterByFileExtension(tileUrl); - } - - if (converter) { - // Convert to gltf - CesiumGltfReader::GltfReaderOptions gltfOptions; - gltfOptions.ktx2TranscodeTargets = - contentOptions.ktx2TranscodeTargets; - gltfOptions.applyTextureTransform = - contentOptions.applyTextureTransform; - GltfConverterResult result = converter(responseData, gltfOptions); - - // Report any errors if there are any - logTileLoadResult(pLogger, tileUrl, result.errors); - if (result.errors) { - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - return TileLoadResult{ - std::move(*result.model), - upAxis, - std::nullopt, - std::nullopt, - std::nullopt, - std::move(pCompletedRequest), - {}, - TileLoadResultState::Success}; - } else { - // not a renderable content, then it must be external tileset - return parseExternalTilesetInWorkerThread( - tileTransform, - upAxis, - tileRefine, - pLogger, - std::move(pCompletedRequest), - std::move(externalContentInitializer)); - } - }); + + return asyncSystem.runInWorkerThread( + [pLogger, + contentOptions, + responseBytes = pResponse->data(), + tileUrl = responseUrl, + tileTransform, + tileRefine, + upAxis = _upAxis, + externalContentInitializer = + std::move(externalContentInitializer)]() mutable { + // find gltf converter + auto converter = GltfConverters::getConverterByMagic(responseBytes); + if (!converter) { + converter = GltfConverters::getConverterByFileExtension(tileUrl); + } + + if (converter) { + // Convert to gltf + CesiumGltfReader::GltfReaderOptions gltfOptions; + gltfOptions.ktx2TranscodeTargets = + contentOptions.ktx2TranscodeTargets; + gltfOptions.applyTextureTransform = + contentOptions.applyTextureTransform; + GltfConverterResult result = converter(responseBytes, gltfOptions); + + // Report any errors if there are any + logTileLoadResult(pLogger, tileUrl, result.errors); + if (result.errors) { + return TileLoadResult::createFailedResult(); + } + + return TileLoadResult{ + std::move(*result.model), + upAxis, + std::nullopt, + std::nullopt, + std::nullopt, + tileUrl, + {}, + CesiumAsync::RequestData{}, + TileLoadResultState::Success}; + } else { + // not a renderable content, then it must be external tileset + return parseExternalTilesetInWorkerThread( + tileTransform, + upAxis, + tileRefine, + pLogger, + tileUrl, + responseBytes, + std::move(externalContentInitializer)); + } + }); +} + +bool TilesetJsonLoader::getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) { + // check if this tile belongs to a child loader + auto currentLoader = pTile->getLoader(); + if (currentLoader != this) { + return currentLoader->getLoadWork(pTile, outRequest, outCallback); + } + + // this loader only handles Url ID + const std::string* url = std::get_if(&pTile->getTileID()); + if (!url) + return false; + + outRequest.url = CesiumUtility::Uri::resolve(this->_baseUrl, *url, true); + + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + return true; } TileChildrenResult TilesetJsonLoader::createTileChildren(const Tile& tile) { diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index 0fa6ac857..8f8a187d4 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -21,6 +21,11 @@ class TilesetJsonLoader : public TilesetContentLoader { CesiumAsync::Future loadTileContent(const TileLoadInput& loadInput) override; + bool getLoadWork( + const Tile* pTile, + CesiumAsync::RequestData& outRequest, + TileLoaderCallback& outCallback) override; + TileChildrenResult createTileChildren(const Tile& tile) override; const std::string& getBaseUrl() const noexcept; diff --git a/Cesium3DTilesSelection/test/TestImplicitOctreeLoader.cpp b/Cesium3DTilesSelection/test/TestImplicitOctreeLoader.cpp index bc01b0591..8d442c657 100644 --- a/Cesium3DTilesSelection/test/TestImplicitOctreeLoader.cpp +++ b/Cesium3DTilesSelection/test/TestImplicitOctreeLoader.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -47,19 +48,76 @@ TEST_CASE("Test implicit octree loader") { Tile tile(&loader); tile.setTileID("This is a test tile"); - TileLoadInput loadInput{ - tile, - {}, - asyncSystem, - pMockedAssetAccessor, - spdlog::default_logger(), - {}}; - - auto tileLoadResultFuture = loader.loadTileContent(loadInput); + CesiumAsync::RequestData requestData; + TileLoaderCallback processingCallback; + loader.getLoadWork(&tile, requestData, processingCallback); + + TileWorkManager::Order newOrder = { + requestData, + TileProcessingData{&tile, processingCallback}, + TileLoadPriorityGroup::Normal, + 0}; + + std::shared_ptr workManager = + std::make_shared( + asyncSystem, + pMockedAssetAccessor, + spdlog::default_logger()); + + std::vector orders; + orders.push_back(TileWorkManager::Order{ + requestData, + TileProcessingData{&tile, processingCallback}, + TileLoadPriorityGroup::Normal, + 0}); + + TileWorkManager::TileDispatchFunc tileDispatch = + [pLoader = &loader, asyncSystem, workManager]( + TileProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work) mutable { + assert(processingData.pTile); + assert(processingData.loaderCallback); + Tile* pTile = processingData.pTile; + + TileLoadInput loadInput{ + *pTile, + {}, + asyncSystem, + spdlog::default_logger(), + responseDataMap}; + + processingData.loaderCallback(loadInput, pLoader) + .thenInMainThread( + [_work = work, workManager](TileLoadResult&& result) mutable { + _work->tileLoadResult = std::move(result); + workManager->SignalWorkComplete(_work); + TileWorkManager::TryDispatchProcessing(workManager); + }); + }; + + TileWorkManager::RasterDispatchFunc rasterDispatch = + [](RasterProcessingData&, + const CesiumAsync::UrlResponseDataMap&, + TileWorkManager::Work*) {}; + + workManager->SetDispatchFunctions(tileDispatch, rasterDispatch); + + std::vector workCreated; + TileWorkManager::TryAddOrders(workManager, orders, 20, workCreated); + assert(workCreated.size() == 1); + TileWorkManager::TryDispatchProcessing(workManager); asyncSystem.dispatchMainThreadTasks(); - auto tileLoadResult = tileLoadResultFuture.wait(); + std::vector doneOrders; + std::vector failedOrders; + workManager->TakeCompletedWork(doneOrders, failedOrders); + + assert(doneOrders.size() == 1); + assert(failedOrders.size() == 0); + + auto tileLoadResult = doneOrders.begin()->loadResult; CHECK(tileLoadResult.state == TileLoadResultState::Failed); } @@ -83,9 +141,8 @@ TEST_CASE("Test implicit octree loader") { tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + CesiumAsync::UrlResponseDataMap{}}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); @@ -131,13 +188,15 @@ TEST_CASE("Test implicit octree loader") { Tile tile(&loader); tile.setTileID(OctreeTileID{3, 1, 0, 1}); + CesiumAsync::UrlResponseDataMap responseDataMap; + pMockedAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); @@ -184,13 +243,15 @@ TEST_CASE("Test implicit octree loader") { Tile tile(&loader); tile.setTileID(OctreeTileID{1, 0, 1, 0}); + CesiumAsync::UrlResponseDataMap responseDataMap; + pMockedAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); diff --git a/Cesium3DTilesSelection/test/TestImplicitQuadtreeLoader.cpp b/Cesium3DTilesSelection/test/TestImplicitQuadtreeLoader.cpp index c6b0afd4d..8fe490803 100644 --- a/Cesium3DTilesSelection/test/TestImplicitQuadtreeLoader.cpp +++ b/Cesium3DTilesSelection/test/TestImplicitQuadtreeLoader.cpp @@ -51,9 +51,8 @@ TEST_CASE("Test implicit quadtree loader") { tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + CesiumAsync::UrlResponseDataMap{}}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); @@ -83,9 +82,8 @@ TEST_CASE("Test implicit quadtree loader") { tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + CesiumAsync::UrlResponseDataMap{}}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); @@ -131,13 +129,15 @@ TEST_CASE("Test implicit quadtree loader") { Tile tile(&loader); tile.setTileID(QuadtreeTileID{2, 1, 1}); + CesiumAsync::UrlResponseDataMap responseDataMap; + pMockedAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); @@ -184,13 +184,15 @@ TEST_CASE("Test implicit quadtree loader") { Tile tile(&loader); tile.setTileID(QuadtreeTileID{2, 1, 1}); + CesiumAsync::UrlResponseDataMap responseDataMap; + pMockedAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ tile, {}, asyncSystem, - pMockedAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; auto tileLoadResultFuture = loader.loadTileContent(loadInput); diff --git a/Cesium3DTilesSelection/test/TestLayerJsonTerrainLoader.cpp b/Cesium3DTilesSelection/test/TestLayerJsonTerrainLoader.cpp index 6cd3753a8..4f81f55a0 100644 --- a/Cesium3DTilesSelection/test/TestLayerJsonTerrainLoader.cpp +++ b/Cesium3DTilesSelection/test/TestLayerJsonTerrainLoader.cpp @@ -46,7 +46,7 @@ Future loadTile( const QuadtreeTileID& tileID, LayerJsonTerrainLoader& loader, AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor) { + const std::shared_ptr& pAssetAccessor) { Tile tile(&loader); tile.setTileID(tileID); tile.setBoundingVolume(BoundingRegionWithLooseFittingHeights{ @@ -54,15 +54,21 @@ Future loadTile( -1000.0, 9000.0}}); + RequestData requestData; + TileLoaderCallback processingCallback; + loader.getLoadWork(&tile, requestData, processingCallback); + + UrlResponseDataMap responseDataMap; + pAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ tile, {}, asyncSystem, - pAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; - auto tileLoadResultFuture = loader.loadTileContent(loadInput); + auto tileLoadResultFuture = processingCallback(loadInput, &loader); asyncSystem.dispatchMainThreadTasks(); diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index ec842ec48..389e0e0d6 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -42,6 +42,17 @@ class SimpleTilesetContentLoader : public TilesetContentLoader { std::move(mockLoadTileContent)); } + bool getLoadWork( + const Tile*, + CesiumAsync::RequestData&, + TileLoaderCallback& outCallback) override { + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + return true; + }; + TileChildrenResult createTileChildren([[maybe_unused]] const Tile& tile) override { return std::move(mockCreateTileChildren); @@ -180,6 +191,19 @@ CesiumGltf::Model createGlobeGrid( return model; } + +void loadTileWithManager( + Tile* pTile, + TilesetContentManager* pManager, + TilesetOptions& options) { + std::vector loadRequests; + if (pTile) { + loadRequests.emplace_back( + TileLoadRequest{pTile, TileLoadPriorityGroup::Normal}); + } + pManager->processLoadRequests(loadRequests, options); +} + } // namespace TEST_CASE("Test the manager can be initialized with correct loaders") { @@ -316,14 +340,16 @@ TEST_CASE("Test tile state machine") { // create mock loader bool initializerCall = false; auto pMockedLoader = std::make_unique(); + pMockedLoader->mockLoadTileContent = { CesiumGltf::Model(), CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", [&](Tile&) { initializerCall = true; }, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren.children.emplace_back( @@ -349,7 +375,8 @@ TEST_CASE("Test tile state machine") { // test manager loading Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, options); + + loadTileWithManager(&tile, pManager.get(), options); SECTION("Load tile from ContentLoading -> Done") { // Unloaded -> ContentLoading @@ -420,14 +447,16 @@ TEST_CASE("Test tile state machine") { // create mock loader bool initializerCall = false; auto pMockedLoader = std::make_unique(); + pMockedLoader->mockLoadTileContent = { CesiumGltf::Model(), CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", [&](Tile&) { initializerCall = true; }, + CesiumAsync::RequestData(), TileLoadResultState::RetryLater}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren.children.emplace_back( @@ -453,18 +482,12 @@ TEST_CASE("Test tile state machine") { // test manager loading Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, options); - - // Unloaded -> ContentLoading - CHECK(pManager->getNumberOfTilesLoading() == 1); - CHECK(tile.getState() == TileLoadState::ContentLoading); - CHECK(tile.getChildren().empty()); - CHECK(tile.getContent().isUnknownContent()); - CHECK(!tile.getContent().isRenderContent()); - CHECK(!tile.getContent().getRenderContent()); - // ContentLoading -> FailedTemporarily + loadTileWithManager(&tile, pManager.get(), options); pManager->waitUntilIdle(); + + // Unloaded -> FailedTemporarily + CHECK(tile.getState() == TileLoadState::FailedTemporarily); CHECK(pManager->getNumberOfTilesLoading() == 0); CHECK(tile.getChildren().empty()); CHECK(tile.getState() == TileLoadState::FailedTemporarily); @@ -485,24 +508,28 @@ TEST_CASE("Test tile state machine") { CHECK(!tile.getContent().getRenderContent()); CHECK(!initializerCall); - // FailedTemporarily -> ContentLoading - pManager->loadTileContent(tile, options); - CHECK(pManager->getNumberOfTilesLoading() == 1); - CHECK(tile.getState() == TileLoadState::ContentLoading); + // FailedTemporarily -> FailedTemporarily + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); + + CHECK(pManager->getNumberOfTilesLoading() == 0); + CHECK(tile.getState() == TileLoadState::FailedTemporarily); } SECTION("Loader requests failed") { // create mock loader bool initializerCall = false; auto pMockedLoader = std::make_unique(); + pMockedLoader->mockLoadTileContent = { CesiumGltf::Model(), CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", [&](Tile&) { initializerCall = true; }, + CesiumAsync::RequestData(), TileLoadResultState::Failed}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren.children.emplace_back( @@ -528,18 +555,11 @@ TEST_CASE("Test tile state machine") { // test manager loading Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, options); - - // Unloaded -> ContentLoading - CHECK(pManager->getNumberOfTilesLoading() == 1); - CHECK(tile.getState() == TileLoadState::ContentLoading); - CHECK(tile.getChildren().empty()); - CHECK(tile.getContent().isUnknownContent()); - CHECK(!tile.getContent().isRenderContent()); - CHECK(!tile.getContent().getRenderContent()); - // ContentLoading -> Failed + loadTileWithManager(&tile, pManager.get(), options); pManager->waitUntilIdle(); + + // Unloaded -> Failed CHECK(pManager->getNumberOfTilesLoading() == 0); CHECK(tile.getChildren().empty()); CHECK(tile.getState() == TileLoadState::Failed); @@ -561,7 +581,8 @@ TEST_CASE("Test tile state machine") { CHECK(!initializerCall); // cannot transition from Failed -> ContentLoading - pManager->loadTileContent(tile, options); + loadTileWithManager(&tile, pManager.get(), options); + CHECK(pManager->getNumberOfTilesLoading() == 0); CHECK(tile.getState() == TileLoadState::Failed); CHECK(tile.getContent().isUnknownContent()); @@ -593,8 +614,9 @@ TEST_CASE("Test tile state machine") { std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", [&](Tile&) { initializerCall = true; }, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Failed}; @@ -631,7 +653,7 @@ TEST_CASE("Test tile state machine") { Tile& upsampledTile = tile.getChildren().back(); // test manager loading upsample tile - pManager->loadTileContent(upsampledTile, options); + loadTileWithManager(&upsampledTile, pManager.get(), options); // since parent is not yet loaded, it will load the parent first. // The upsampled tile will not be loaded at the moment @@ -646,7 +668,8 @@ TEST_CASE("Test tile state machine") { // try again with upsample tile, but still not able to load it // because parent is not done yet - pManager->loadTileContent(upsampledTile, options); + loadTileWithManager(&upsampledTile, pManager.get(), options); + CHECK(upsampledTile.getState() == TileLoadState::Unloaded); // parent moves from ContentLoaded -> Done @@ -659,19 +682,23 @@ TEST_CASE("Test tile state machine") { // load the upsampled tile again: Unloaded -> ContentLoading initializerCall = false; + pMockedLoaderRaw->mockLoadTileContent = { CesiumGltf::Model(), CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", [&](Tile&) { initializerCall = true; }, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoaderRaw->mockCreateTileChildren = { {}, TileLoadResultState::Failed}; - pManager->loadTileContent(upsampledTile, options); + + loadTileWithManager(&upsampledTile, pManager.get(), options); + CHECK(upsampledTile.getState() == TileLoadState::ContentLoading); // trying to unload parent while upsampled children is loading while put the @@ -686,7 +713,8 @@ TEST_CASE("Test tile state machine") { CHECK(tile.isRenderContent()); // Attempting to load won't do anything - unloading must finish first. - pManager->loadTileContent(tile, options); + loadTileWithManager(&tile, pManager.get(), options); + CHECK(tile.getState() == TileLoadState::Unloading); // upsampled tile: ContentLoading -> ContentLoaded @@ -748,14 +776,16 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { } auto pMockedLoader = std::make_unique(); + pMockedLoader->mockLoadTileContent = { std::move(*modelReadResult.model), CesiumGeometry::Axis::Y, std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", {}, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Failed}; @@ -780,7 +810,10 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { // test the gltf model Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, {}); + + TilesetOptions options; + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); // check the buffer is already loaded @@ -824,8 +857,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", {}, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Failed}; @@ -848,7 +882,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { // test the gltf model Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, options); + + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); // check that normal is generated @@ -891,8 +927,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", {}, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Failed}; @@ -911,7 +948,10 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { std::move(pRootTile)}; Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, {}); + + TilesetOptions options; + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); const auto& renderContent = tile.getContent().getRenderContent(); @@ -941,8 +981,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { std::nullopt, std::nullopt, std::nullopt, - nullptr, + "test", {}, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Failed}; @@ -963,7 +1004,10 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { "Generate raster overlay details when tile don't have loose region") { // test the gltf model Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, {}); + + TilesetOptions options; + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); CHECK(tile.getState() == TileLoadState::ContentLoaded); @@ -1026,7 +1070,12 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { 9000.0}}; tile.setBoundingVolume(originalLooseRegion); - pManager->loadTileContent(tile, {}); + // Tick the manager twice + // Child overlay work has to complete before parent work + TilesetOptions options; + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); + loadTileWithManager(nullptr, pManager.get(), options); pManager->waitUntilIdle(); CHECK(tile.getState() == TileLoadState::ContentLoaded); @@ -1120,7 +1169,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { 9000.0}}; tile.setBoundingVolume(originalLooseRegion); - pManager->loadTileContent(tile, {}); + TilesetOptions options; + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); CHECK(tile.getState() == TileLoadState::ContentLoaded); @@ -1175,8 +1226,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { std::nullopt, std::nullopt, std::move(rasterOverlayDetails), - nullptr, + "test", {}, + CesiumAsync::RequestData(), TileLoadResultState::Success}; pMockedLoader->mockCreateTileChildren = {{}, TileLoadResultState::Failed}; @@ -1194,7 +1246,10 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { std::move(pRootTile)}; Tile& tile = *pManager->getRootTile(); - pManager->loadTileContent(tile, {}); + + TilesetOptions options; + loadTileWithManager(&tile, pManager.get(), options); + pManager->waitUntilIdle(); const auto& renderContent = tile.getContent().getRenderContent(); diff --git a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp index b2dc6e382..fed1ab801 100644 --- a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp @@ -3,6 +3,7 @@ #include "TilesetJsonLoader.h" #include +#include #include #include #include @@ -71,6 +72,7 @@ createLoader(const std::filesystem::path& tilesetPath) { TileLoadResult loadTileContent( const std::filesystem::path& tilePath, + const std::string& tileUrl, TilesetContentLoader& loader, Tile& tile) { auto pMockCompletedResponse = std::make_unique( @@ -80,7 +82,7 @@ TileLoadResult loadTileContent( readFile(tilePath)); auto pMockCompletedRequest = std::make_shared( "GET", - "doesn't matter", + tileUrl, CesiumAsync::HttpHeaders{}, std::move(pMockCompletedResponse)); @@ -94,19 +96,72 @@ TileLoadResult loadTileContent( AsyncSystem asyncSystem{std::make_shared()}; - TileLoadInput loadInput{ - tile, - {}, - asyncSystem, - pMockAssetAccessor, - spdlog::default_logger(), - {}}; + RequestData requestData; + TileLoaderCallback processingCallback; + loader.getLoadWork(&tile, requestData, processingCallback); - auto tileLoadResultFuture = loader.loadTileContent(loadInput); + std::shared_ptr workManager = + std::make_shared( + asyncSystem, + pMockAssetAccessor, + spdlog::default_logger()); + + std::vector orders; + orders.push_back(TileWorkManager::Order{ + requestData, + TileProcessingData{&tile, processingCallback}, + TileLoadPriorityGroup::Normal, + 0}); + + TileWorkManager::TileDispatchFunc tileDispatch = + [pLoader = &loader, asyncSystem, workManager]( + TileProcessingData& processingData, + const CesiumAsync::UrlResponseDataMap& responseDataMap, + TileWorkManager::Work* work) mutable { + assert(processingData.pTile); + assert(processingData.loaderCallback); + Tile* pTile = processingData.pTile; + + TileLoadInput loadInput{ + *pTile, + {}, + asyncSystem, + spdlog::default_logger(), + responseDataMap}; + + processingData.loaderCallback(loadInput, pLoader) + .thenInMainThread( + [_work = work, workManager](TileLoadResult&& result) mutable { + _work->tileLoadResult = std::move(result); + workManager->SignalWorkComplete(_work); + TileWorkManager::TryDispatchProcessing(workManager); + }); + }; + + TileWorkManager::RasterDispatchFunc rasterDispatch = + [](RasterProcessingData&, + const CesiumAsync::UrlResponseDataMap&, + TileWorkManager::Work*) {}; + + workManager->SetDispatchFunctions(tileDispatch, rasterDispatch); + + size_t maxRequests = 20; + std::vector workCreated; + TileWorkManager::TryAddOrders(workManager, orders, maxRequests, workCreated); + assert(workCreated.size() == 1); asyncSystem.dispatchMainThreadTasks(); - return tileLoadResultFuture.wait(); + std::vector doneOrders; + std::vector failedOrders; + workManager->TakeCompletedWork(doneOrders, failedOrders); + + assert(doneOrders.size() == 1); + assert(failedOrders.size() == 0); + + auto tileLoadResult = doneOrders.begin()->loadResult; + + return tileLoadResult; } } // namespace @@ -438,6 +493,7 @@ TEST_CASE("Test loading individual tile of tileset json") { // check tile content auto tileLoadResult = loadTileContent( testDataPath / "ReplaceTileset" / tileID, + "parent.b3dm", *loaderResult.pLoader, *pRootTile); CHECK( @@ -463,6 +519,7 @@ TEST_CASE("Test loading individual tile of tileset json") { // check tile content auto tileLoadResult = loadTileContent( testDataPath / "AddTileset" / tileID, + "tileset2.json", *loaderResult.pLoader, *pRootTile); CHECK(tileLoadResult.updatedBoundingVolume == std::nullopt); @@ -559,13 +616,16 @@ TEST_CASE("Test loading individual tile of tileset json") { { // loader will tell to retry later since it needs subtree + + UrlResponseDataMap responseDataMap; + pMockAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ implicitTile, {}, asyncSystem, - pMockAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; auto implicitContentResultFuture = loaderResult.pLoader->loadTileContent(loadInput); @@ -577,13 +637,15 @@ TEST_CASE("Test loading individual tile of tileset json") { { // loader will be able to load the tile the second time around + UrlResponseDataMap responseDataMap; + pMockAssetAccessor->fillResponseDataMap(responseDataMap); + TileLoadInput loadInput{ implicitTile, {}, asyncSystem, - pMockAssetAccessor, spdlog::default_logger(), - {}}; + responseDataMap}; auto implicitContentResultFuture = loaderResult.pLoader->loadTileContent(loadInput); diff --git a/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp b/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp index d3bb3dc62..9162196f1 100644 --- a/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp @@ -243,6 +243,7 @@ TEST_CASE("Test replace refinement for render") { SECTION("Children cannot be rendered because response has an failed status " "code") { + // remove one of children completed response to mock network error mockAssetAccessor->mockCompletedRequests["ll.b3dm"] ->pResponse->mockStatusCode = 404; @@ -256,7 +257,7 @@ TEST_CASE("Test replace refinement for render") { ViewState viewState = zoomToTileset(tileset); - // 1st frame. Root doesn't meet sse, so it goes to children. But because + // 1st frame. Root doesn't meet sse, so it goes to children.But because // children haven't started loading, root should be rendered. { ViewUpdateResult result = tileset.updateView({viewState}); @@ -266,7 +267,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(root->getState() == TileLoadState::Done); REQUIRE(!doesTileMeetSSE(viewState, *root, tileset)); for (const auto& child : root->getChildren()) { - REQUIRE(child.getState() == TileLoadState::ContentLoading); + REQUIRE(child.getState() == TileLoadState::Failed); REQUIRE(doesTileMeetSSE(viewState, child, tileset)); } @@ -308,6 +309,7 @@ TEST_CASE("Test replace refinement for render") { } SECTION("Parent meets sse but not renderable") { + // Zoom to tileset. Expect the root will not meet sse in this configure ViewState viewState = zoomToTileset(tileset); glm::dvec3 zoomInPosition = @@ -329,11 +331,16 @@ TEST_CASE("Test replace refinement for render") { { ViewUpdateResult result = tileset.updateView({zoomInViewState}); - // check tiles status. All the children should have loading status + // check tiles status REQUIRE(root->getState() == TileLoadState::Done); REQUIRE(!doesTileMeetSSE(zoomInViewState, *root, tileset)); + + // All the children should be loading or failed for (const auto& child : root->getChildren()) { - REQUIRE(child.getState() == TileLoadState::ContentLoading); + bool childStateIsExpected = + child.getState() == TileLoadState::ContentLoading || + child.getState() == TileLoadState::Failed; + REQUIRE(childStateIsExpected); } const Tile& ll = root->getChildren().front(); @@ -556,15 +563,14 @@ TEST_CASE("Test additive refinement") { Tileset tileset(tilesetExternals, "tileset.json"); initializeTileset(tileset); - // root is external tileset. Since its content is loading, we won't know if it - // has children or not + // root is external tileset. Content is loaded immediately, but not done const Tile* pTilesetJson = tileset.getRootTile(); REQUIRE(pTilesetJson); REQUIRE(pTilesetJson->getChildren().size() == 1); const Tile* root = &pTilesetJson->getChildren()[0]; - REQUIRE(root->getState() == TileLoadState::ContentLoading); - REQUIRE(root->getChildren().size() == 0); + REQUIRE(root->getState() == TileLoadState::ContentLoaded); + REQUIRE(root->getChildren().size() == 1); SECTION("Load external tilesets") { ViewState viewState = zoomToTileset(tileset); @@ -587,7 +593,9 @@ TEST_CASE("Test additive refinement") { REQUIRE(parentB3DM.getChildren().size() == 4); for (const Tile& child : parentB3DM.getChildren()) { - REQUIRE(child.getState() == TileLoadState::ContentLoading); + bool childLoading = child.getState() == TileLoadState::ContentLoading || + child.getState() == TileLoadState::ContentLoaded; + REQUIRE(childLoading); REQUIRE(doesTileMeetSSE(viewState, child, tileset)); } @@ -726,10 +734,6 @@ TEST_CASE("Render any tiles even when one of children can't be rendered for " { ViewUpdateResult result = tileset.updateView({viewState}); - for (const Tile& child : root->getChildren()) { - CHECK(child.getState() == TileLoadState::ContentLoading); - } - REQUIRE(result.tilesToRenderThisFrame.size() == 2); REQUIRE(result.tilesFadingOut.size() == 0); REQUIRE(result.tilesVisited == 5); @@ -1512,6 +1516,15 @@ void runUnconditionallyRefinedTestCase(const TilesetOptions& options) { return pRootTile; } + bool getLoadWork(const Tile*, RequestData&, TileLoaderCallback& outCallback) + override { + outCallback = [](const TileLoadInput& loadInput, + TilesetContentLoader* loader) { + return loader->loadTileContent(loadInput); + }; + return true; + }; + virtual CesiumAsync::Future loadTileContent(const TileLoadInput& input) override { if (&input.tile == this->_pRootTile) { @@ -1531,7 +1544,7 @@ void runUnconditionallyRefinedTestCase(const TilesetOptions& options) { } return input.asyncSystem.createResolvedFuture( - TileLoadResult::createFailedResult(nullptr)); + TileLoadResult::createFailedResult()); } virtual TileChildrenResult createTileChildren(const Tile&) override { diff --git a/CesiumAsync/include/CesiumAsync/IAssetAccessor.h b/CesiumAsync/include/CesiumAsync/IAssetAccessor.h index 415e9e63c..0386c50b3 100644 --- a/CesiumAsync/include/CesiumAsync/IAssetAccessor.h +++ b/CesiumAsync/include/CesiumAsync/IAssetAccessor.h @@ -70,4 +70,19 @@ class CESIUMASYNC_API IAssetAccessor { virtual void tick() noexcept = 0; }; +struct RequestData { + std::string url = ""; + std::vector headers = {}; +}; + +struct ResponseData { + const CesiumAsync::IAssetRequest* pRequest; + const CesiumAsync::IAssetResponse* pResponse; +}; + +using UrlResponseDataMap = std::map; + +using UrlAssetRequestMap = + std::map>; + } // namespace CesiumAsync diff --git a/CesiumJsonReader/include/CesiumJsonReader/JsonReader.h b/CesiumJsonReader/include/CesiumJsonReader/JsonReader.h index 7dd67acdf..88497b77d 100644 --- a/CesiumJsonReader/include/CesiumJsonReader/JsonReader.h +++ b/CesiumJsonReader/include/CesiumJsonReader/JsonReader.h @@ -35,6 +35,11 @@ template struct ReadJsonResult { * @brief Warnings that occurred while reading. */ std::vector warnings; + + /** + * @brief Additional url needed for the read + */ + std::string urlNeeded; }; /** diff --git a/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h b/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h index 4c0943a50..fd4a3a17f 100644 --- a/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h +++ b/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h @@ -48,6 +48,16 @@ class SimpleAssetAccessor : public CesiumAsync::IAssetAccessor { virtual void tick() noexcept override {} + void fillResponseDataMap(CesiumAsync::UrlResponseDataMap& responseDataMap) { + for (auto& pair : mockCompletedRequests) { + responseDataMap.emplace( + pair.first, + CesiumAsync::ResponseData{ + pair.second.get(), + pair.second->response()}); + } + } + std::map> mockCompletedRequests; }; diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/QuadtreeRasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/QuadtreeRasterOverlayTileProvider.h index ae2e79e2e..460de6f67 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/QuadtreeRasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/QuadtreeRasterOverlayTileProvider.h @@ -101,23 +101,49 @@ class CESIUMRASTEROVERLAYS_API QuadtreeRasterOverlayTileProvider * @brief Asynchronously loads a tile in the quadtree. * * @param tileID The ID of the quadtree tile to load. + * @param requestUrl Original url of content request + * @param statusCode Response code of content request + * @param data Bytes of content response * @return A Future that resolves to the loaded image data or error * information. */ - virtual CesiumAsync::Future - loadQuadtreeTileImage(const CesiumGeometry::QuadtreeTileID& tileID) const = 0; + virtual CesiumAsync::Future loadQuadtreeTileImage( + const CesiumGeometry::QuadtreeTileID& tileID, + const std::string& requestUrl, + uint16_t statusCode, + const gsl::span& data) const = 0; + + /** + * @brief Gets the request data for a load + * + * @param tileID The ID of the quadtree tile to load. + * @param requestData Output data for content request + * @param errorString Output string for any errors encountered + * @return bool indicating success of failure + */ + virtual bool getQuadtreeTileImageRequest( + const CesiumGeometry::QuadtreeTileID& tileID, + CesiumAsync::RequestData& requestData, + std::string& errorString) const = 0; private: - virtual CesiumAsync::Future - loadTileImage(RasterOverlayTile& overlayTile) override final; + virtual CesiumAsync::Future loadTileImage( + const RasterOverlayTile& overlayTile, + const CesiumAsync::UrlResponseDataMap& responsesByUrl) override final; + + virtual void getLoadTileImageWork( + const RasterOverlayTile& overlayTile, + CesiumAsync::RequestData& outRequest, + RasterProcessingCallback& outCallback) override; struct LoadedQuadtreeImage { - std::shared_ptr pLoaded = nullptr; + std::shared_ptr pResult = nullptr; std::optional subset = std::nullopt; }; - CesiumAsync::SharedFuture - getQuadtreeTile(const CesiumGeometry::QuadtreeTileID& tileID); + CesiumAsync::Future getQuadtreeTile( + const CesiumGeometry::QuadtreeTileID& tileID, + const CesiumAsync::UrlResponseDataMap& responsesByUrl); /** * @brief Map raster tiles to geometry tile. @@ -125,14 +151,16 @@ class CESIUMRASTEROVERLAYS_API QuadtreeRasterOverlayTileProvider * @param geometryRectangle The rectangle for which to load tiles. * @param targetGeometricError The geometric error controlling which quadtree * level to use to cover the rectangle. - * @return A vector of shared futures, each of which will resolve to image - * data that is required to cover the rectangle with the given geometric + * @param responsesByUrl Content responses available + * @param outTiles A vector of shared futures, each of which will resolve to + * image data that is required to cover the rectangle with the given geometric * error. */ - std::vector> - mapRasterTilesToGeometryTile( + void mapRasterTilesToGeometryTile( const CesiumGeometry::Rectangle& geometryRectangle, - const glm::dvec2 targetScreenPixels); + const glm::dvec2 targetScreenPixels, + const CesiumAsync::UrlResponseDataMap& responsesByUrl, + std::vector>& outTiles); void unloadCachedTiles(); @@ -148,7 +176,7 @@ class CESIUMRASTEROVERLAYS_API QuadtreeRasterOverlayTileProvider const CesiumGeometry::Rectangle& targetRectangle, const std::vector& images); - static LoadedRasterOverlayImage combineImages( + static RasterLoadResult combineImages( const CesiumGeometry::Rectangle& targetRectangle, const CesiumGeospatial::Projection& projection, std::vector&& images); @@ -161,7 +189,7 @@ class CESIUMRASTEROVERLAYS_API QuadtreeRasterOverlayTileProvider struct CacheEntry { CesiumGeometry::QuadtreeTileID tileID; - CesiumAsync::SharedFuture future; + LoadedQuadtreeImage loadedImage; }; // Tiles at the beginning of this list are the least recently used (oldest), diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h index f74023e16..8eb8ee758 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h @@ -13,6 +13,10 @@ namespace CesiumUtility { struct Credit; } +namespace Cesium3DTilesSelection { +class TilesetContentManager; +} + namespace CesiumRasterOverlays { class RasterOverlay; @@ -21,7 +25,7 @@ class RasterOverlayTileProvider; /** * @brief Raster image data for a tile in a quadtree. * - * Instances of this clas represent tiles of a quadtree that have + * Instances of this class represent tiles of a quadtree that have * an associated image, which us used as an imagery overlay * for tile geometry. The connection between the imagery data * and the actual tile geometry is established via the @@ -39,12 +43,17 @@ class RasterOverlayTile final /** * @brief Indicator for a placeholder tile. */ - Placeholder = -2, + Placeholder = -3, /** * @brief The image request or image creation failed. */ - Failed = -1, + Failed = -2, + + /** + * @brief An additional content request is needed + */ + RequestRequired = -1, /** * @brief The initial state @@ -243,6 +252,7 @@ class RasterOverlayTile final private: friend class RasterOverlayTileProvider; + friend class Cesium3DTilesSelection::TilesetContentManager; void setState(LoadState newState) noexcept; diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h index 1f5c1d8f1..b92703be8 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,60 +16,39 @@ #include #include +namespace CesiumUtility { +struct Credit; +} // namespace CesiumUtility + namespace CesiumRasterOverlays { class RasterOverlay; class RasterOverlayTile; class IPrepareRasterOverlayRendererResources; -/** - * @brief Summarizes the result of loading an image of a {@link RasterOverlay}. - */ -struct CESIUMRASTEROVERLAYS_API LoadedRasterOverlayImage { - /** - * @brief The loaded image. - * - * This will be an empty optional if the loading failed. In this case, - * the `errors` vector will contain the corresponding error messages. - */ +struct RasterLoadResult { std::optional image{}; + CesiumGeometry::Rectangle rectangle = {}; + std::vector credits = {}; + std::vector errors{}; + std::vector warnings{}; + bool moreDetailAvailable = false; - /** - * @brief The projected rectangle defining the bounds of this image. - * - * The rectangle extends from the left side of the leftmost pixel to the - * right side of the rightmost pixel, and similar for the vertical direction. - */ - CesiumGeometry::Rectangle rectangle{}; - - /** - * @brief The {@link Credit} objects that decribe the attributions that - * are required when using the image. - */ - std::vector credits{}; + std::vector missingRequests = {}; - /** - * @brief Error messages from loading the image. - * - * If the image was loaded successfully, this should be empty. - */ - std::vector errors{}; + RasterOverlayTile::LoadState state = RasterOverlayTile::LoadState::Unloaded; - /** - * @brief Warnings from loading the image. - */ - // Implementation note: In the current implementation, this will - // always be empty, but it might contain warnings in the future, - // when other image types or loaders are used. - std::vector warnings{}; + void* pRendererResources = nullptr; - /** - * @brief Whether more detailed data, beyond this image, is available within - * the bounds of this image. - */ - bool moreDetailAvailable = false; + CesiumUtility::IntrusivePointer pTile = {}; }; +using RasterProcessingCallback = + std::function( + RasterOverlayTile&, + RasterOverlayTileProvider*, + const CesiumAsync::UrlResponseDataMap&)>; + /** * @brief Options for {@link RasterOverlayTileProvider::loadTileImageFromUrl}. */ @@ -83,7 +63,7 @@ struct LoadTileImageFromUrlOptions { * @brief The credits to display with this tile. * * This property is copied verbatim to the - * {@link LoadedRasterOverlayImage::credits} property. + * {@link RasterLoadResult::credits} property. */ std::vector credits{}; @@ -92,38 +72,6 @@ struct LoadTileImageFromUrlOptions { * the bounds of this image. */ bool moreDetailAvailable = true; - - /** - * @brief Whether empty (zero length) images are accepted as a valid - * response. - * - * If true, an otherwise valid response with zero length will be accepted as - * a valid 0x0 image. If false, such a response will be reported as an - * error. - * - * {@link RasterOverlayTileProvider::loadTile} and - * {@link RasterOverlayTileProvider::loadTileThrottled} will treat such an - * image as "failed" and use the quadtree parent (or ancestor) image - * instead, but will not report any error. - * - * This flag should only be set to `true` when the tile source uses a - * zero-length response as an indication that this tile is - as expected - - * not available. - */ - bool allowEmptyImages = false; -}; - -class RasterOverlayTileProvider; - -/** - * @brief Holds a tile and its corresponding tile provider. Used as the return - * value of {@link RasterOverlayTileProvider::loadTile}. - */ -struct TileProviderAndTile { - CesiumUtility::IntrusivePointer pTileProvider; - CesiumUtility::IntrusivePointer pTile; - - ~TileProviderAndTile() noexcept; }; /** @@ -287,11 +235,20 @@ class CESIUMRASTEROVERLAYS_API RasterOverlayTileProvider int64_t getTileDataBytes() const noexcept { return this->_tileDataBytes; } /** - * @brief Returns the number of tiles that are currently loading. + * @brief Incremement number of bytes loaded from outside caller + * + * @param bytes Number of bytes to add to our count */ - uint32_t getNumberOfTilesLoading() const noexcept { - assert(this->_totalTilesCurrentlyLoading > -1); - return this->_totalTilesCurrentlyLoading; + void incrementTileDataBytes(CesiumGltf::ImageCesium& imageCesium) noexcept { + // If the image size hasn't been overridden, store the pixelData + // size now. We'll add this number to our total memory usage now, + // and remove it when the tile is later unloaded, and we must use + // the same size in each case. + if (imageCesium.sizeBytes < 0) { + imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); + } + + _tileDataBytes += imageCesium.sizeBytes; } /** @@ -316,91 +273,101 @@ class CESIUMRASTEROVERLAYS_API RasterOverlayTileProvider /** * @brief Loads a tile immediately, without throttling requests. * - * If the tile is not in the `Tile::LoadState::Unloaded` state, this method - * returns without doing anything. Otherwise, it puts the tile into the - * `Tile::LoadState::Loading` state and begins the asynchronous process - * to load the tile. When the process completes, the tile will be in the - * `Tile::LoadState::Loaded` or `Tile::LoadState::Failed` state. + * If the tile is not in the `RasterOverlayTile::LoadState::Unloading` + * state, this method returns without doing anything. Otherwise, it puts the + * tile into the `RasterOverlayTile::LoadState::Loading` state and + * begins the asynchronous process to load the tile. When the process + * completes, the tile will be in the + * `RasterOverlayTile::LoadState::Loaded` or + * `RasterOverlayTile::LoadState::Failed` state. * * Calling this method on many tiles at once can result in very slow * performance. Consider using {@link loadTileThrottled} instead. * - * @param tile The tile to load. - * @return A future that, when the tile is loaded, resolves to the loaded tile - * and the tile provider that loaded it. + * @param tile The tile to load + * @param responsesByUrl Content responses already fetched by caller + * @param rasterCallback Loader callback to execute */ - CesiumAsync::Future loadTile(RasterOverlayTile& tile); + CesiumAsync::Future loadTile( + RasterOverlayTile& tile, + const CesiumAsync::UrlResponseDataMap& responsesByUrl, + RasterProcessingCallback rasterCallback); /** - * @brief Loads a tile, unless there are too many tile loads already in - * progress. + * @brief Loads a tile * - * If the tile is not in the `Tile::LoadState::Unloading` state, this method - * returns true without doing anything. If too many tile loads are - * already in flight, it returns false without doing anything. Otherwise, it - * puts the tile into the `Tile::LoadState::Loading` state, begins the - * asynchronous process to load the tile, and returns true. When the process - * completes, the tile will be in the `Tile::LoadState::Loaded` or - * `Tile::LoadState::Failed` state. + * It begins the asynchronous process to load the tile, and returns true. When + * the process completes, the tile will be in the + * `RasterOverlayTile::LoadState::Loaded` or + * `RasterOverlayTile::LoadState::Failed` state. * - * The number of allowable simultaneous tile requests is provided in the - * {@link RasterOverlayOptions::maximumSimultaneousTileLoads} property of - * {@link RasterOverlay::getOptions}. + * @param tile The tile to load + * @param responsesByUrl Content responses already fetched by caller + * @param rasterCallback Loader callback to execute + * @returns RasterLoadResult indicating what happened + */ + CesiumAsync::Future loadTileThrottled( + RasterOverlayTile& tile, + const CesiumAsync::UrlResponseDataMap& responsesByUrl, + RasterProcessingCallback rasterCallback); + + /** + * @brief Gets the work needed to load a tile * - * @param tile The tile to load. - * @returns True if the tile load process is started or is already complete, - * false if the load could not be started because too many loads are already - * in progress. + * @param tile The tile to load + * @param outRequest Content request needed + * @param outCallback Loader callback to execute later */ - bool loadTileThrottled(RasterOverlayTile& tile); + void getLoadTileThrottledWork( + const RasterOverlayTile& tile, + CesiumAsync::RequestData& outRequest, + RasterProcessingCallback& outCallback); protected: /** * @brief Loads the image for a tile. * - * @param overlayTile The overlay tile for which to load the image. - * @return A future that resolves to the image or error information. + * @param overlayTile The overlay tile to load the image. + * @param responsesByUrl Content responses already fetched by caller + * @return A future containing a RasterLoadResult + */ + virtual CesiumAsync::Future loadTileImage( + const RasterOverlayTile& overlayTile, + const CesiumAsync::UrlResponseDataMap& responsesByUrl) = 0; + + /** + * @brief Get the work needed to loads the image for a tile. + * + * @param overlayTile The overlay tile to load the image. + * @param outRequest Content request needed + * @param outCallback Loader callback to execute later */ - virtual CesiumAsync::Future - loadTileImage(RasterOverlayTile& overlayTile) = 0; + virtual void getLoadTileImageWork( + const RasterOverlayTile& overlayTile, + CesiumAsync::RequestData& outRequest, + RasterProcessingCallback& outCallback) = 0; /** * @brief Loads an image from a URL and optionally some request headers. * * This is a useful helper function for implementing {@link loadTileImage}. * - * @param url The URL. - * @param headers The request headers. - * @param options Additional options for the load process. - * @return A future that resolves to the image or error information. + * @param url The URL used to fetch image data + * @param statusCode HTTP code returned from content fetch + * @param data Bytes of url content response + * @param options Additional options for the load process + * @return A future containing a RasterLoadResult */ - CesiumAsync::Future loadTileImageFromUrl( + CesiumAsync::Future loadTileImageFromUrl( const std::string& url, - const std::vector& headers = {}, + const gsl::span& data, LoadTileImageFromUrlOptions&& options = {}) const; private: - CesiumAsync::Future - doLoad(RasterOverlayTile& tile, bool isThrottledLoad); - - /** - * @brief Begins the process of loading of a tile. - * - * This method should be called at the beginning of the tile load process. - * - * @param isThrottledLoad True if the load was originally throttled. - */ - void beginTileLoad(bool isThrottledLoad) noexcept; - - /** - * @brief Finalizes loading of a tile. - * - * This method should be called at the end of the tile load process, - * no matter whether the load succeeded or failed. - * - * @param isThrottledLoad True if the load was originally throttled. - */ - void finalizeTileLoad(bool isThrottledLoad) noexcept; + CesiumAsync::Future doLoad( + RasterOverlayTile& tile, + const CesiumAsync::UrlResponseDataMap& responsesByUrl, + RasterProcessingCallback rasterCallback); private: CesiumUtility::IntrusivePointer _pOwner; @@ -414,8 +381,6 @@ class CESIUMRASTEROVERLAYS_API RasterOverlayTileProvider CesiumGeometry::Rectangle _coverageRectangle; CesiumUtility::IntrusivePointer _pPlaceholder; int64_t _tileDataBytes; - int32_t _totalTilesCurrentlyLoading; - int32_t _throttledTilesCurrentlyLoading; CESIUM_TRACE_DECLARE_TRACK_SET( _loadingSlots, "Raster Overlay Tile Loading Slot"); diff --git a/CesiumRasterOverlays/src/BingMapsRasterOverlay.cpp b/CesiumRasterOverlays/src/BingMapsRasterOverlay.cpp index fca88d623..3aa47e8dd 100644 --- a/CesiumRasterOverlays/src/BingMapsRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/BingMapsRasterOverlay.cpp @@ -145,27 +145,13 @@ class BingMapsTileProvider final : public QuadtreeRasterOverlayTileProvider { virtual ~BingMapsTileProvider() {} protected: - virtual CesiumAsync::Future loadQuadtreeTileImage( - const CesiumGeometry::QuadtreeTileID& tileID) const override { - std::string url = CesiumUtility::Uri::substituteTemplateParameters( - this->_urlTemplate, - [this, &tileID](const std::string& key) { - if (key == "quadkey") { - return BingMapsTileProvider::tileXYToQuadKey( - tileID.level, - tileID.x, - tileID.computeInvertedY(this->getTilingScheme())); - } - if (key == "subdomain") { - const size_t subdomainIndex = - (tileID.level + tileID.x + tileID.y) % this->_subdomains.size(); - return this->_subdomains[subdomainIndex]; - } - return key; - }); + virtual CesiumAsync::Future loadQuadtreeTileImage( + const CesiumGeometry::QuadtreeTileID& tileID, + const std::string& requestUrl, + uint16_t statusCode, + const gsl::span& data) const override { LoadTileImageFromUrlOptions options; - options.allowEmptyImages = true; options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); options.rectangle = this->getTilingScheme().tileToRectangle(tileID); std::vector& tileCredits = options.credits = @@ -193,7 +179,55 @@ class BingMapsTileProvider final : public QuadtreeRasterOverlayTileProvider { } } - return this->loadTileImageFromUrl(url, {}, std::move(options)); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + std::string message = "Image response code " + + std::to_string(statusCode) + " for " + requestUrl; + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {message}, + {}, + options.moreDetailAvailable}); + } + + if (data.empty()) { + // This tile is - as expected - not available + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + CesiumGltf::ImageCesium(), + options.rectangle, + std::move(options.credits), + {}, + {}, + options.moreDetailAvailable}); + } + + return this->loadTileImageFromUrl(requestUrl, data, std::move(options)); + } + + virtual bool getQuadtreeTileImageRequest( + const CesiumGeometry::QuadtreeTileID& tileID, + RequestData& requestData, + std::string&) const override { + requestData.url = CesiumUtility::Uri::substituteTemplateParameters( + this->_urlTemplate, + [this, &tileID](const std::string& key) { + if (key == "quadkey") { + return BingMapsTileProvider::tileXYToQuadKey( + tileID.level, + tileID.x, + tileID.computeInvertedY(this->getTilingScheme())); + } + if (key == "subdomain") { + const size_t subdomainIndex = + (tileID.level + tileID.x + tileID.y) % this->_subdomains.size(); + return this->_subdomains[subdomainIndex]; + } + return key; + }); + return true; } private: diff --git a/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp b/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp index 77d37b688..2c42700ee 100644 --- a/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp @@ -30,9 +30,10 @@ class DebugTileProvider : public RasterOverlayTileProvider { GeographicProjection(), GeographicProjection::computeMaximumProjectedRectangle()) {} - virtual CesiumAsync::Future - loadTileImage(RasterOverlayTile& overlayTile) override { - LoadedRasterOverlayImage result; + virtual CesiumAsync::Future loadTileImage( + const RasterOverlayTile& overlayTile, + const CesiumAsync::UrlResponseDataMap&) override { + RasterLoadResult result; // Indicate that there is no more detail available so that tiles won't get // refined on our behalf. @@ -59,6 +60,20 @@ class DebugTileProvider : public RasterOverlayTileProvider { return this->getAsyncSystem().createResolvedFuture(std::move(result)); } + + virtual void getLoadTileImageWork( + const RasterOverlayTile&, + CesiumAsync::RequestData&, + RasterProcessingCallback& outCallback) override { + // There is no content request, just processing + outCallback = [](RasterOverlayTile& overlayTile, + RasterOverlayTileProvider* provider, + const CesiumAsync::UrlResponseDataMap& responsesByUrl) { + DebugTileProvider* thisProvider = + static_cast(provider); + return thisProvider->loadTileImage(overlayTile, responsesByUrl); + }; + } }; } // namespace diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index 669716d27..634ef3af7 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -1,3 +1,5 @@ +#include "CesiumAsync/IAssetResponse.h" + #include #include #include @@ -96,13 +98,11 @@ uint32_t QuadtreeRasterOverlayTileProvider::computeLevelFromTargetScreenPixels( return imageryLevel; } -std::vector> -QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile( +void QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile( const CesiumGeometry::Rectangle& geometryRectangle, - const glm::dvec2 targetScreenPixels) { - std::vector> result; - + const glm::dvec2 targetScreenPixels, + const UrlResponseDataMap& responsesByUrl, + std::vector>& outTiles) { const QuadtreeTilingScheme& imageryTilingScheme = this->getTilingScheme(); // Use Web Mercator for our texture coordinate computations if this imagery @@ -176,9 +176,8 @@ QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile( // Because of the intersection, we should always have valid tile coordinates. // But give up if we don't. - if (!southwestTileCoordinatesOpt || !northeastTileCoordinatesOpt) { - return result; - } + if (!southwestTileCoordinatesOpt || !northeastTileCoordinatesOpt) + return; QuadtreeTileID southwestTileCoordinates = southwestTileCoordinatesOpt.value(); QuadtreeTileID northeastTileCoordinates = northeastTileCoordinatesOpt.value(); @@ -271,19 +270,19 @@ QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile( continue; } - CesiumAsync::SharedFuture pTile = - this->getQuadtreeTile(QuadtreeTileID(level, i, j)); - result.emplace_back(std::move(pTile)); + CesiumAsync::Future pTile = + this->getQuadtreeTile(QuadtreeTileID(level, i, j), responsesByUrl); + outTiles.emplace_back(std::move(pTile)); } } - - return result; } -CesiumAsync::SharedFuture< - QuadtreeRasterOverlayTileProvider::LoadedQuadtreeImage> +CesiumAsync::Future QuadtreeRasterOverlayTileProvider::getQuadtreeTile( - const CesiumGeometry::QuadtreeTileID& tileID) { + const CesiumGeometry::QuadtreeTileID& tileID, + const UrlResponseDataMap& responsesByUrl) { + + // Return any cached requests auto lookupIt = this->_tileLookup.find(tileID); if (lookupIt != this->_tileLookup.end()) { auto& cacheIt = lookupIt->second; @@ -294,7 +293,38 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( this->_tilesOldToRecent, cacheIt); - return cacheIt->future; + LoadedQuadtreeImage cacheCopy = cacheIt->loadedImage; + + return this->getAsyncSystem().createResolvedFuture( + {std::move(cacheCopy)}); + } + + // Not cached, discover request here + RequestData requestData; + UrlResponseDataMap::const_iterator foundIt; + std::string errorString; + if (this->getQuadtreeTileImageRequest(tileID, requestData, errorString)) { + // Successfully discovered a request. Find it in our responses + foundIt = responsesByUrl.find(requestData.url); + if (foundIt == responsesByUrl.end()) { + // If not there, request it and come back later + RasterLoadResult loadResult; + loadResult.missingRequests.push_back(requestData); + loadResult.state = RasterOverlayTile::LoadState::RequestRequired; + + return this->getAsyncSystem().createResolvedFuture( + {std::make_shared(std::move(loadResult))}); + ; + } + } else { + // Error occurred while discovering request + RasterLoadResult loadResult; + loadResult.errors.push_back(errorString); + loadResult.state = RasterOverlayTile::LoadState::Failed; + + return this->getAsyncSystem().createResolvedFuture( + {std::make_shared(std::move(loadResult))}); + ; } // We create this lambda here instead of where it's used below so that we @@ -302,23 +332,29 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( // create the possibility of accidentally using this pointer to a // non-thread-safe object from another thread and creating a (potentially very // subtle) race condition. - auto loadParentTile = [tileID, this]() { + auto loadParentTile = [tileID, responsesByUrl, this]() { const Rectangle rectangle = this->getTilingScheme().tileToRectangle(tileID); const QuadtreeTileID parentID( tileID.level - 1, tileID.x >> 1, tileID.y >> 1); - return this->getQuadtreeTile(parentID).thenImmediately( - [rectangle](const LoadedQuadtreeImage& loaded) { - return LoadedQuadtreeImage{loaded.pLoaded, rectangle}; + return this->getQuadtreeTile(parentID, responsesByUrl) + .thenImmediately([rectangle](const LoadedQuadtreeImage& loaded) { + return LoadedQuadtreeImage{loaded.pResult, rectangle}; }); }; + const CesiumAsync::IAssetResponse* assetResponse = foundIt->second.pResponse; + Future future = - this->loadQuadtreeTileImage(tileID) + this->loadQuadtreeTileImage( + tileID, + requestData.url, + assetResponse->statusCode(), + assetResponse->data()) .catchImmediately([](std::exception&& e) { // Turn an exception into an error. - LoadedRasterOverlayImage result; + RasterLoadResult result; result.errors.emplace_back(e.what()); return result; }) @@ -327,29 +363,39 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( minimumLevel = this->getMinimumLevel(), asyncSystem = this->getAsyncSystem(), loadParentTile = std::move(loadParentTile)]( - LoadedRasterOverlayImage&& loaded) { - if (loaded.image && loaded.errors.empty() && - loaded.image->width > 0 && loaded.image->height > 0) { + RasterLoadResult&& result) { + if (result.state == RasterOverlayTile::LoadState::RequestRequired) { + // Pass through request + return asyncSystem.createResolvedFuture(LoadedQuadtreeImage{ + std::make_shared(std::move(result)), + std::nullopt}); + } + + bool imageValid = result.image && result.errors.empty() && + result.image->width > 0 && + result.image->height > 0; + + if (imageValid) { // Successfully loaded, continue. - cachedBytes += int64_t(loaded.image->pixelData.size()); + cachedBytes += int64_t(result.image->pixelData.size()); #if SHOW_TILE_BOUNDARIES // Highlight the edges in red to show tile boundaries. gsl::span pixels = reintepretCastSpan( - loaded.image->pixelData); - for (int32_t j = 0; j < loaded.image->height; ++j) { - for (int32_t i = 0; i < loaded.image->width; ++i) { - if (i == 0 || j == 0 || i == loaded.image->width - 1 || - j == loaded.image->height - 1) { - pixels[j * loaded.image->width + i] = 0xFF0000FF; + result.image->pixelData); + for (int32_t j = 0; j < result.image->height; ++j) { + for (int32_t i = 0; i < result.image->width; ++i) { + if (i == 0 || j == 0 || i == result.image->width - 1 || + j == result.image->height - 1) { + pixels[j * result.image->width + i] = 0xFF0000FF; } } } #endif return asyncSystem.createResolvedFuture(LoadedQuadtreeImage{ - std::make_shared(std::move(loaded)), + std::make_shared(std::move(result)), std::nullopt}); } @@ -361,21 +407,36 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( } else { // No parent available, so return the original failed result. return asyncSystem.createResolvedFuture(LoadedQuadtreeImage{ - std::make_shared(std::move(loaded)), + std::make_shared(std::move(result)), std::nullopt}); } - }); + }) + .thenInMainThread([this, tileID](LoadedQuadtreeImage&& loadedImage) { + RasterLoadResult& result = *loadedImage.pResult.get(); - auto newIt = this->_tilesOldToRecent.emplace( - this->_tilesOldToRecent.end(), - CacheEntry{tileID, std::move(future).share()}); - this->_tileLookup[tileID] = newIt; + // If more requests needed, pass through + if (result.state == RasterOverlayTile::LoadState::RequestRequired) { + return std::move(loadedImage); + } - SharedFuture result = newIt->future; + // If a valid image, cache it + bool imageValid = result.image && result.errors.empty() && + result.image->width > 0 && + result.image->height > 0; + + if (imageValid) { + auto newIt = this->_tilesOldToRecent.emplace( + this->_tilesOldToRecent.end(), + CacheEntry{tileID, LoadedQuadtreeImage{loadedImage}}); + this->_tileLookup[tileID] = newIt; + } + + return std::move(loadedImage); + }); this->unloadCachedTiles(); - return result; + return future; } namespace { @@ -440,21 +501,70 @@ void blitImage( } // namespace -CesiumAsync::Future +void QuadtreeRasterOverlayTileProvider::getLoadTileImageWork( + const RasterOverlayTile&, + RequestData&, + RasterProcessingCallback& outCallback) { + // loadTileImage will control request / processing flow + outCallback = [](RasterOverlayTile& overlayTile, + RasterOverlayTileProvider* provider, + const UrlResponseDataMap& responsesByUrl) { + QuadtreeRasterOverlayTileProvider* thisProvider = + static_cast(provider); + return thisProvider->loadTileImage(overlayTile, responsesByUrl); + }; +} + +CesiumAsync::Future QuadtreeRasterOverlayTileProvider::loadTileImage( - RasterOverlayTile& overlayTile) { + const RasterOverlayTile& overlayTile, + const UrlResponseDataMap& responsesByUrl) { // Figure out which quadtree level we need, and which tiles from that level. // Load each needed tile (or pull it from cache). - std::vector> tiles = - this->mapRasterTilesToGeometryTile( - overlayTile.getRectangle(), - overlayTile.getTargetScreenPixels()); + std::vector> tiles; + this->mapRasterTilesToGeometryTile( + overlayTile.getRectangle(), + overlayTile.getTargetScreenPixels(), + responsesByUrl, + tiles); return this->getAsyncSystem() .all(std::move(tiles)) .thenInWorkerThread([projection = this->getProjection(), rectangle = overlayTile.getRectangle()]( std::vector&& images) { + // Gather any missing requests + std::vector allMissingRequests; + for (auto& image : images) { + assert(image.pResult); + if (image.pResult->state == + RasterOverlayTile::LoadState::RequestRequired) { + assert(!image.pResult->missingRequests.empty()); + + // Merge any duplicate urls + for (auto& request : image.pResult->missingRequests) { + auto foundIt = std::find_if( + allMissingRequests.begin(), + allMissingRequests.end(), + [url = request.url](const auto& val) { + return val.url == url; + }); + if (foundIt != allMissingRequests.end()) + continue; + + allMissingRequests.push_back(request); + } + } + } + + // Send all requests together + if (!allMissingRequests.empty()) { + RasterLoadResult loadResult; + loadResult.missingRequests = std::move(allMissingRequests); + loadResult.state = RasterOverlayTile::LoadState::RequestRequired; + return loadResult; + } + // This set of images is only "useful" if at least one actually has // image data, and that image data is _not_ from an ancestor. We can // identify ancestor images because they have a `subset`. @@ -462,7 +572,7 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( images.begin(), images.end(), [](const LoadedQuadtreeImage& image) { - return image.pLoaded->image.has_value() && + return image.pResult->image.has_value() && !image.subset.has_value(); }); @@ -471,7 +581,7 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( // signalling that the parent tile should be used instead. // See https://github.com/CesiumGS/cesium-native/issues/316 for an // edge case that is not yet handled. - return LoadedRasterOverlayImage{ + return RasterLoadResult{ ImageCesium(), Rectangle(), {}, @@ -499,17 +609,9 @@ void QuadtreeRasterOverlayTileProvider::unloadCachedTiles() { while (it != this->_tilesOldToRecent.end() && this->_cachedBytes > maxCacheBytes) { - const SharedFuture& future = it->future; - if (!future.isReady()) { - // Don't unload tiles that are still loading. - ++it; - continue; - } - - // Guaranteed not to block because isReady returned true. - const LoadedQuadtreeImage& image = future.wait(); + const LoadedQuadtreeImage& image = it->loadedImage; - std::shared_ptr pImage = image.pLoaded; + std::shared_ptr pImage = image.pResult; this->_tileLookup.erase(it->tileID); it = this->_tilesOldToRecent.erase(it); @@ -543,7 +645,7 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage( int32_t channels = -1; int32_t bytesPerChannel = -1; for (const LoadedQuadtreeImage& image : images) { - const LoadedRasterOverlayImage& loaded = *image.pLoaded; + const RasterLoadResult& loaded = *image.pResult; if (!loaded.image || loaded.image->width <= 0 || loaded.image->height <= 0) { continue; @@ -563,7 +665,7 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage( std::optional combinedRectangle; for (const LoadedQuadtreeImage& image : images) { - const LoadedRasterOverlayImage& loaded = *image.pLoaded; + const RasterLoadResult& loaded = *image.pResult; if (!loaded.image || loaded.image->width <= 0 || loaded.image->height <= 0) { continue; @@ -640,8 +742,7 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage( bytesPerChannel}; } -/*static*/ LoadedRasterOverlayImage -QuadtreeRasterOverlayTileProvider::combineImages( +/*static*/ RasterLoadResult QuadtreeRasterOverlayTileProvider::combineImages( const Rectangle& targetRectangle, const Projection& /* projection */, std::vector&& images) { @@ -656,7 +757,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( measurements.channels * measurements.bytesPerChannel; if (targetImageBytes <= 0) { // Target image has no pixels, so our work here is done. - return LoadedRasterOverlayImage{ + return RasterLoadResult{ std::nullopt, targetRectangle, {}, @@ -666,7 +767,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( }; } - LoadedRasterOverlayImage result; + RasterLoadResult result; result.rectangle = measurements.rectangle; result.moreDetailAvailable = false; @@ -679,7 +780,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( target.width * target.height * target.channels * target.bytesPerChannel)); for (auto it = images.begin(); it != images.end(); ++it) { - const LoadedRasterOverlayImage& loaded = *it->pLoaded; + const RasterLoadResult& loaded = *it->pResult; if (!loaded.image) { continue; } @@ -696,7 +797,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( size_t combinedCreditsCount = 0; for (auto it = images.begin(); it != images.end(); ++it) { - const LoadedRasterOverlayImage& loaded = *it->pLoaded; + const RasterLoadResult& loaded = *it->pResult; if (!loaded.image) { continue; } @@ -706,7 +807,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( result.credits.reserve(combinedCreditsCount); for (auto it = images.begin(); it != images.end(); ++it) { - const LoadedRasterOverlayImage& loaded = *it->pLoaded; + const RasterLoadResult& loaded = *it->pResult; if (!loaded.image) { continue; } diff --git a/CesiumRasterOverlays/src/RasterOverlay.cpp b/CesiumRasterOverlays/src/RasterOverlay.cpp index 7f43aa666..a5d3ba37a 100644 --- a/CesiumRasterOverlays/src/RasterOverlay.cpp +++ b/CesiumRasterOverlays/src/RasterOverlay.cpp @@ -17,10 +17,16 @@ class PlaceholderTileProvider : public RasterOverlayTileProvider { const std::shared_ptr& pAssetAccessor) noexcept : RasterOverlayTileProvider(pOwner, asyncSystem, pAssetAccessor) {} - virtual CesiumAsync::Future - loadTileImage(RasterOverlayTile& /* overlayTile */) override { - return this->getAsyncSystem() - .createResolvedFuture({}); + virtual CesiumAsync::Future + loadTileImage(const RasterOverlayTile&, const UrlResponseDataMap&) override { + return this->getAsyncSystem().createResolvedFuture({}); + } + + virtual void getLoadTileImageWork( + const RasterOverlayTile&, + RequestData&, + RasterProcessingCallback&) override { + // There is no actual work to be done } }; } // namespace diff --git a/CesiumRasterOverlays/src/RasterOverlayTile.cpp b/CesiumRasterOverlays/src/RasterOverlayTile.cpp index 722ef553b..595bf5d05 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTile.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTile.cpp @@ -43,14 +43,12 @@ RasterOverlayTile::~RasterOverlayTile() { pPrepareRendererResources = tileProvider.getPrepareRendererResources(); if (pPrepareRendererResources) { - void* pLoadThreadResult = - this->getState() == RasterOverlayTile::LoadState::Done - ? nullptr - : this->_pRendererResources; - void* pMainThreadResult = - this->getState() == RasterOverlayTile::LoadState::Done - ? this->_pRendererResources - : nullptr; + void* pLoadThreadResult = this->getState() == LoadState::Done + ? nullptr + : this->_pRendererResources; + void* pMainThreadResult = this->getState() == LoadState::Done + ? this->_pRendererResources + : nullptr; pPrepareRendererResources->freeRaster( *this, @@ -71,7 +69,7 @@ const RasterOverlay& RasterOverlayTile::getOverlay() const noexcept { } void RasterOverlayTile::loadInMainThread() { - if (this->getState() != RasterOverlayTile::LoadState::Loaded) { + if (this->getState() != LoadState::Loaded) { return; } diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 083cd7344..f3bd3e9dc 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -35,9 +35,7 @@ RasterOverlayTileProvider::RasterOverlayTileProvider( _coverageRectangle(CesiumGeospatial::GeographicProjection:: computeMaximumProjectedRectangle()), _pPlaceholder(), - _tileDataBytes(0), - _totalTilesCurrentlyLoading(0), - _throttledTilesCurrentlyLoading(0) { + _tileDataBytes(0) { this->_pPlaceholder = new RasterOverlayTile(*this); } @@ -60,9 +58,7 @@ RasterOverlayTileProvider::RasterOverlayTileProvider( _projection(projection), _coverageRectangle(coverageRectangle), _pPlaceholder(nullptr), - _tileDataBytes(0), - _totalTilesCurrentlyLoading(0), - _throttledTilesCurrentlyLoading(0) {} + _tileDataBytes(0) {} RasterOverlayTileProvider::~RasterOverlayTileProvider() noexcept { // Explicitly release the placeholder first, because RasterOverlayTiles must @@ -94,174 +90,172 @@ void RasterOverlayTileProvider::removeTile(RasterOverlayTile* pTile) noexcept { this->_tileDataBytes -= pTile->getImage().sizeBytes; } -CesiumAsync::Future -RasterOverlayTileProvider::loadTile(RasterOverlayTile& tile) { +CesiumAsync::Future RasterOverlayTileProvider::loadTile( + RasterOverlayTile& tile, + const UrlResponseDataMap& responsesByUrl, + RasterProcessingCallback rasterCallback) { if (this->_pPlaceholder) { // Refuse to load placeholders. - return this->getAsyncSystem().createResolvedFuture( - TileProviderAndTile{this, nullptr}); + return this->getAsyncSystem().createResolvedFuture(RasterLoadResult{}); } - return this->doLoad(tile, false); + // Already loading or loaded, do nothing. + if (tile.getState() != RasterOverlayTile::LoadState::Unloaded) + return this->getAsyncSystem().createResolvedFuture(RasterLoadResult{}); + + // Don't let this tile be destroyed while it's loading. + tile.setState(RasterOverlayTile::LoadState::Loading); + + // Keep the tile and tile provider alive while the async operation is in + // progress. + IntrusivePointer pTile = &tile; + IntrusivePointer thiz = this; + + return this->doLoad(tile, responsesByUrl, rasterCallback) + .thenInMainThread([thiz, pTile](RasterLoadResult&& result) noexcept { + if (result.state == RasterOverlayTile::LoadState::RequestRequired) + return thiz->_asyncSystem.createResolvedFuture( + std::move(result)); + + pTile->_rectangle = result.rectangle; + pTile->_pRendererResources = result.pRendererResources; + assert(result.image.has_value()); + pTile->_image = std::move(result.image.value()); + pTile->_tileCredits = std::move(result.credits); + pTile->_moreDetailAvailable = + result.moreDetailAvailable + ? RasterOverlayTile::MoreDetailAvailable::Yes + : RasterOverlayTile::MoreDetailAvailable::No; + pTile->setState(result.state); + + result.pTile = pTile; + + thiz->_tileDataBytes += int64_t(pTile->getImage().pixelData.size()); + + return thiz->_asyncSystem.createResolvedFuture( + std::move(result)); + }) + .catchInMainThread([thiz, pTile](const std::exception& /*e*/) { + pTile->_pRendererResources = nullptr; + pTile->_image = {}; + pTile->_tileCredits = {}; + pTile->_moreDetailAvailable = + RasterOverlayTile::MoreDetailAvailable::No; + pTile->setState(RasterOverlayTile::LoadState::Failed); + + RasterLoadResult result; + result.state = RasterOverlayTile::LoadState::Failed; + + return thiz->_asyncSystem.createResolvedFuture( + std::move(result)); + }); } -bool RasterOverlayTileProvider::loadTileThrottled(RasterOverlayTile& tile) { - if (tile.getState() != RasterOverlayTile::LoadState::Unloaded) { - return true; - } +CesiumAsync::Future +RasterOverlayTileProvider::loadTileThrottled( + RasterOverlayTile& tile, + const UrlResponseDataMap& responsesByUrl, + RasterProcessingCallback rasterCallback) { + return this->doLoad(tile, responsesByUrl, rasterCallback); +} - if (this->_throttledTilesCurrentlyLoading >= - this->getOwner().getOptions().maximumSimultaneousTileLoads) { - return false; - } +void RasterOverlayTileProvider::getLoadTileThrottledWork( + const RasterOverlayTile& tile, + RequestData& outRequest, + RasterProcessingCallback& outCallback) { + if (tile.getState() != RasterOverlayTile::LoadState::Unloaded) + return; - this->doLoad(tile, true); - return true; + getLoadTileImageWork(tile, outRequest, outCallback); } -CesiumAsync::Future +CesiumAsync::Future RasterOverlayTileProvider::loadTileImageFromUrl( const std::string& url, - const std::vector& headers, + const gsl::span& data, LoadTileImageFromUrlOptions&& options) const { - return this->getAssetAccessor() - ->get(this->getAsyncSystem(), url, headers) - .thenInWorkerThread( - [options = std::move(options), - Ktx2TranscodeTargets = - this->getOwner().getOptions().ktx2TranscodeTargets]( - std::shared_ptr&& pRequest) mutable { - CESIUM_TRACE("load image"); - const IAssetResponse* pResponse = pRequest->response(); - if (pResponse == nullptr) { - return LoadedRasterOverlayImage{ - std::nullopt, - options.rectangle, - std::move(options.credits), - {"Image request for " + pRequest->url() + " failed."}, - {}, - options.moreDetailAvailable}; - } - - if (pResponse->statusCode() != 0 && - (pResponse->statusCode() < 200 || - pResponse->statusCode() >= 300)) { - std::string message = "Image response code " + - std::to_string(pResponse->statusCode()) + - " for " + pRequest->url(); - return LoadedRasterOverlayImage{ - std::nullopt, - options.rectangle, - std::move(options.credits), - {message}, - {}, - options.moreDetailAvailable}; - } - - if (pResponse->data().empty()) { - if (options.allowEmptyImages) { - return LoadedRasterOverlayImage{ - CesiumGltf::ImageCesium(), - options.rectangle, - std::move(options.credits), - {}, - {}, - options.moreDetailAvailable}; - } - return LoadedRasterOverlayImage{ - std::nullopt, - options.rectangle, - std::move(options.credits), - {"Image response for " + pRequest->url() + " is empty."}, - {}, - options.moreDetailAvailable}; - } - - const gsl::span data = pResponse->data(); - - CesiumGltfReader::ImageReaderResult loadedImage = - RasterOverlayTileProvider::_gltfReader.readImage( - data, - Ktx2TranscodeTargets); - - if (!loadedImage.errors.empty()) { - loadedImage.errors.push_back("Image url: " + pRequest->url()); - } - if (!loadedImage.warnings.empty()) { - loadedImage.warnings.push_back("Image url: " + pRequest->url()); - } - - return LoadedRasterOverlayImage{ + return this->getAsyncSystem().runInWorkerThread( + [options = std::move(options), + url = url, + data = data, + asyncSystem = this->getAsyncSystem(), + Ktx2TranscodeTargets = + this->getOwner().getOptions().ktx2TranscodeTargets]() mutable { + CESIUM_TRACE("load image"); + + CesiumGltfReader::ImageReaderResult loadedImage = + RasterOverlayTileProvider::_gltfReader.readImage( + data, + Ktx2TranscodeTargets); + + if (!loadedImage.errors.empty()) { + loadedImage.errors.push_back("Image url: " + url); + } + if (!loadedImage.warnings.empty()) { + loadedImage.warnings.push_back("Image url: " + url); + } + + return asyncSystem.createResolvedFuture( + RasterLoadResult{ loadedImage.image, options.rectangle, std::move(options.credits), std::move(loadedImage.errors), std::move(loadedImage.warnings), - options.moreDetailAvailable}; - }); + options.moreDetailAvailable}); + }); } namespace { -struct LoadResult { - RasterOverlayTile::LoadState state = RasterOverlayTile::LoadState::Unloaded; - CesiumGltf::ImageCesium image = {}; - CesiumGeometry::Rectangle rectangle = {}; - std::vector credits = {}; - void* pRendererResources = nullptr; - bool moreDetailAvailable = true; -}; /** - * @brief Processes the given `LoadedRasterOverlayImage`, producing a - * `LoadResult`. + * @brief Processes the given `RasterLoadResult` * * This function is intended to be called on the worker thread. * - * If the given `loadedImage` contains no valid image data, then a - * `LoadResult` with the state `RasterOverlayTile::LoadState::Failed` will be - * returned. + * If the given `RasterLoadResult::image` contains no valid image data, then a + * `RasterLoadResult` with the state `RasterOverlayTile::LoadState::Failed` will + * be returned. * * Otherwise, the image data will be passed to - * `IPrepareRasterOverlayRendererResources::prepareRasterInLoadThread`, and the - * function will return a `LoadResult` with the image, the prepared renderer + * `IPrepareRendererResources::prepareRasterInLoadThread`, and the function + * will modify a `RasterLoadResult` with the image, the prepared renderer * resources, and the state `RasterOverlayTile::LoadState::Loaded`. * * @param tileId The {@link TileID} - only used for logging * @param pPrepareRendererResources The `IPrepareRasterOverlayRendererResources` * @param pLogger The logger - * @param loadedImage The `LoadedRasterOverlayImage` + * @param loadResult The `RasterLoadResult` * @param rendererOptions Renderer options - * @return The `LoadResult` */ -static LoadResult createLoadResultFromLoadedImage( +static void prepareLoadResultImage( const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, - LoadedRasterOverlayImage&& loadedImage, + RasterLoadResult& loadResult, const std::any& rendererOptions) { - if (!loadedImage.image.has_value()) { + if (!loadResult.image.has_value()) { SPDLOG_LOGGER_ERROR( pLogger, "Failed to load image for tile {}:\n- {}", "TODO", // Cesium3DTilesSelection::TileIdUtilities::createTileIdString(tileId), - CesiumUtility::joinToString(loadedImage.errors, "\n- ")); - LoadResult result; - result.state = RasterOverlayTile::LoadState::Failed; - return result; + CesiumUtility::joinToString(loadResult.errors, "\n- ")); + loadResult.state = RasterOverlayTile::LoadState::Failed; + return; } - if (!loadedImage.warnings.empty()) { + if (!loadResult.warnings.empty()) { SPDLOG_LOGGER_WARN( pLogger, "Warnings while loading image for tile {}:\n- {}", "TODO", // Cesium3DTilesSelection::TileIdUtilities::createTileIdString(tileId), - CesiumUtility::joinToString(loadedImage.warnings, "\n- ")); + CesiumUtility::joinToString(loadResult.warnings, "\n- ")); } - CesiumGltf::ImageCesium& image = loadedImage.image.value(); + CesiumGltf::ImageCesium& image = loadResult.image.value(); const int32_t bytesPerPixel = image.channels * image.bytesPerChannel; const int64_t requiredBytes = @@ -280,119 +274,45 @@ static LoadResult createLoadResultFromLoadedImage( rendererOptions); } - LoadResult result; - result.state = RasterOverlayTile::LoadState::Loaded; - result.image = std::move(image); - result.rectangle = loadedImage.rectangle; - result.credits = std::move(loadedImage.credits); - result.pRendererResources = pRendererResources; - result.moreDetailAvailable = loadedImage.moreDetailAvailable; - return result; + loadResult.state = RasterOverlayTile::LoadState::Loaded; + loadResult.pRendererResources = pRendererResources; + + return; } - LoadResult result; - result.pRendererResources = nullptr; - result.state = RasterOverlayTile::LoadState::Failed; - result.moreDetailAvailable = false; - return result; + + loadResult.pRendererResources = nullptr; + loadResult.state = RasterOverlayTile::LoadState::Failed; + loadResult.moreDetailAvailable = false; } } // namespace -CesiumAsync::Future RasterOverlayTileProvider::doLoad( +CesiumAsync::Future RasterOverlayTileProvider::doLoad( RasterOverlayTile& tile, - bool isThrottledLoad) { - if (tile.getState() != RasterOverlayTile::LoadState::Unloaded) { - // Already loading or loaded, do nothing. - return this->getAsyncSystem().createResolvedFuture( - TileProviderAndTile{this, nullptr}); - } - + const UrlResponseDataMap& responsesByUrl, + RasterProcessingCallback rasterCallback) { // CESIUM_TRACE_USE_TRACK_SET(this->_loadingSlots); - // Don't let this tile be destroyed while it's loading. - tile.setState(RasterOverlayTile::LoadState::Loading); - - this->beginTileLoad(isThrottledLoad); - - // Keep the tile and tile provider alive while the async operation is in - // progress. - IntrusivePointer pTile = &tile; - IntrusivePointer thiz = this; + assert(rasterCallback); - return this->loadTileImage(tile) + return rasterCallback(tile, this, responsesByUrl) .thenInWorkerThread( [pPrepareRendererResources = this->getPrepareRendererResources(), pLogger = this->getLogger(), rendererOptions = this->_pOwner->getOptions().rendererOptions]( - LoadedRasterOverlayImage&& loadedImage) { - return createLoadResultFromLoadedImage( - pPrepareRendererResources, - pLogger, - std::move(loadedImage), - rendererOptions); - }) - .thenInMainThread( - [thiz, pTile, isThrottledLoad](LoadResult&& result) noexcept { - pTile->_rectangle = result.rectangle; - pTile->_pRendererResources = result.pRendererResources; - pTile->_image = std::move(result.image); - pTile->_tileCredits = std::move(result.credits); - pTile->_moreDetailAvailable = - result.moreDetailAvailable - ? RasterOverlayTile::MoreDetailAvailable::Yes - : RasterOverlayTile::MoreDetailAvailable::No; - pTile->setState(result.state); - - ImageCesium& imageCesium = pTile->getImage(); - - // If the image size hasn't been overridden, store the pixelData - // size now. We'll add this number to our total memory usage now, - // and remove it when the tile is later unloaded, and we must use - // the same size in each case. - if (imageCesium.sizeBytes < 0) { - imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); + RasterLoadResult&& loadResult) { + if (loadResult.state == + RasterOverlayTile::LoadState::RequestRequired) { + // Can't prepare this image yet + } else { + prepareLoadResultImage( + pPrepareRendererResources, + pLogger, + loadResult, + rendererOptions); } - - thiz->_tileDataBytes += imageCesium.sizeBytes; - - thiz->finalizeTileLoad(isThrottledLoad); - - return TileProviderAndTile{thiz, pTile}; - }) - .catchInMainThread( - [thiz, pTile, isThrottledLoad](const std::exception& /*e*/) { - pTile->_pRendererResources = nullptr; - pTile->_image = {}; - pTile->_tileCredits = {}; - pTile->_moreDetailAvailable = - RasterOverlayTile::MoreDetailAvailable::No; - pTile->setState(RasterOverlayTile::LoadState::Failed); - - thiz->finalizeTileLoad(isThrottledLoad); - - return TileProviderAndTile{thiz, pTile}; + return std::move(loadResult); }); } -void RasterOverlayTileProvider::beginTileLoad(bool isThrottledLoad) noexcept { - ++this->_totalTilesCurrentlyLoading; - if (isThrottledLoad) { - ++this->_throttledTilesCurrentlyLoading; - } -} - -void RasterOverlayTileProvider::finalizeTileLoad( - bool isThrottledLoad) noexcept { - --this->_totalTilesCurrentlyLoading; - if (isThrottledLoad) { - --this->_throttledTilesCurrentlyLoading; - } -} - -TileProviderAndTile::~TileProviderAndTile() noexcept { - // Ensure the tile is released before the tile provider. - pTile = nullptr; - pTileProvider = nullptr; -} - } // namespace CesiumRasterOverlays diff --git a/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp b/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp index 6a46e4eba..fec72b6f3 100644 --- a/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp +++ b/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp @@ -18,7 +18,7 @@ using namespace CesiumUtility; namespace CesiumRasterOverlays { namespace { void rasterizePolygons( - LoadedRasterOverlayImage& loaded, + RasterLoadResult& loaded, const CesiumGeospatial::GlobeRectangle& rectangle, const glm::dvec2& textureSize, const std::vector& cartographicPolygons, @@ -187,8 +187,23 @@ class CESIUMRASTEROVERLAYS_API RasterizedPolygonsTileProvider final _polygons(polygons), _invertSelection(invertSelection) {} - virtual CesiumAsync::Future - loadTileImage(RasterOverlayTile& overlayTile) override { + virtual void getLoadTileImageWork( + const RasterOverlayTile&, + CesiumAsync::RequestData&, + RasterProcessingCallback& outCallback) override { + // There is no content request, just processing + outCallback = [](RasterOverlayTile& overlayTile, + RasterOverlayTileProvider* provider, + const CesiumAsync::UrlResponseDataMap& responsesByUrl) { + RasterizedPolygonsTileProvider* thisProvider = + static_cast(provider); + return thisProvider->loadTileImage(overlayTile, responsesByUrl); + }; + } + + virtual CesiumAsync::Future loadTileImage( + const RasterOverlayTile& overlayTile, + const CesiumAsync::UrlResponseDataMap&) override { // Choose the texture size according to the geometry screen size and raster // SSE, but no larger than the maximum texture size. const RasterOverlayOptions& options = this->getOwner().getOptions(); @@ -201,11 +216,11 @@ class CESIUMRASTEROVERLAYS_API RasterizedPolygonsTileProvider final invertSelection = this->_invertSelection, projection = this->getProjection(), rectangle = overlayTile.getRectangle(), - textureSize]() -> LoadedRasterOverlayImage { + textureSize]() -> RasterLoadResult { const CesiumGeospatial::GlobeRectangle tileRectangle = CesiumGeospatial::unprojectRectangleSimple(projection, rectangle); - LoadedRasterOverlayImage result; + RasterLoadResult result; result.rectangle = rectangle; rasterizePolygons( diff --git a/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp index 5175e9a1f..41dbf3eb8 100644 --- a/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp @@ -70,35 +70,60 @@ class TileMapServiceTileProvider final virtual ~TileMapServiceTileProvider() {} protected: - virtual CesiumAsync::Future loadQuadtreeTileImage( - const CesiumGeometry::QuadtreeTileID& tileID) const override { + virtual CesiumAsync::Future loadQuadtreeTileImage( + const CesiumGeometry::QuadtreeTileID& tileID, + const std::string& requestUrl, + uint16_t statusCode, + const gsl::span& data) const override { LoadTileImageFromUrlOptions options; options.rectangle = this->getTilingScheme().tileToRectangle(tileID); options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); - uint32_t level = tileID.level - this->getMinimumLevel(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + std::string message = "Image response code " + + std::to_string(statusCode) + " for " + requestUrl; + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {message}, + {}, + options.moreDetailAvailable}); + } + + if (data.empty()) { + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {"Image response for " + requestUrl + " is empty."}, + {}, + options.moreDetailAvailable}); + } + return this->loadTileImageFromUrl(requestUrl, data, std::move(options)); + } + + virtual bool getQuadtreeTileImageRequest( + const CesiumGeometry::QuadtreeTileID& tileID, + RequestData& requestData, + std::string& errorString) const override { + + uint32_t level = tileID.level - this->getMinimumLevel(); if (level < _tileSets.size()) { const TileMapServiceTileset& tileset = _tileSets[level]; - std::string url = CesiumUtility::Uri::resolve( + requestData.url = CesiumUtility::Uri::resolve( this->_url, tileset.url + "/" + std::to_string(tileID.x) + "/" + std::to_string(tileID.y) + this->_fileExtension, true); - return this->loadTileImageFromUrl( - url, - this->_headers, - std::move(options)); + return true; } else { - return this->getAsyncSystem() - .createResolvedFuture( - {std::nullopt, - options.rectangle, - {}, - {"Failed to load image from TMS."}, - {}, - options.moreDetailAvailable}); + errorString = "Failed to load image from TMS."; + return false; } } diff --git a/CesiumRasterOverlays/src/WebMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/src/WebMapServiceRasterOverlay.cpp index e972e80b9..209265eae 100644 --- a/CesiumRasterOverlays/src/WebMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/WebMapServiceRasterOverlay.cpp @@ -68,17 +68,15 @@ class WebMapServiceTileProvider final virtual ~WebMapServiceTileProvider() {} protected: - virtual CesiumAsync::Future loadQuadtreeTileImage( - const CesiumGeometry::QuadtreeTileID& tileID) const override { - - LoadTileImageFromUrlOptions options; - options.rectangle = this->getTilingScheme().tileToRectangle(tileID); - options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); + virtual bool getQuadtreeTileImageRequest( + const CesiumGeometry::QuadtreeTileID& tileID, + RequestData& requestData, + std::string&) const override { const CesiumGeospatial::GlobeRectangle tileRectangle = CesiumGeospatial::unprojectRectangleSimple( this->getProjection(), - options.rectangle); + this->getTilingScheme().tileToRectangle(tileID)); std::string queryString = "?"; @@ -109,7 +107,7 @@ class WebMapServiceTileProvider final {"width", std::to_string(this->getWidth())}, {"height", std::to_string(this->getHeight())}}; - std::string url = CesiumUtility::Uri::substituteTemplateParameters( + requestData.url = CesiumUtility::Uri::substituteTemplateParameters( urlTemplate, [&map = urlTemplateMap](const std::string& placeholder) { auto it = map.find(placeholder); @@ -117,7 +115,44 @@ class WebMapServiceTileProvider final : Uri::escape(it->second); }); - return this->loadTileImageFromUrl(url, this->_headers, std::move(options)); + return true; + } + + virtual CesiumAsync::Future loadQuadtreeTileImage( + const CesiumGeometry::QuadtreeTileID& tileID, + const std::string& requestUrl, + uint16_t statusCode, + const gsl::span& data) const override { + + LoadTileImageFromUrlOptions options; + options.rectangle = this->getTilingScheme().tileToRectangle(tileID); + options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); + + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + std::string message = "Image response code " + + std::to_string(statusCode) + " for " + requestUrl; + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {message}, + {}, + options.moreDetailAvailable}); + } + + if (data.empty()) { + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {"Image response for " + requestUrl + " is empty."}, + {}, + options.moreDetailAvailable}); + } + + return this->loadTileImageFromUrl(requestUrl, data, std::move(options)); } private: diff --git a/CesiumRasterOverlays/src/WebMapTileServiceRasterOverlay.cpp b/CesiumRasterOverlays/src/WebMapTileServiceRasterOverlay.cpp index 9f4206975..0f2d4515c 100644 --- a/CesiumRasterOverlays/src/WebMapTileServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/WebMapTileServiceRasterOverlay.cpp @@ -73,12 +73,10 @@ class WebMapTileServiceTileProvider final virtual ~WebMapTileServiceTileProvider() {} protected: - virtual CesiumAsync::Future loadQuadtreeTileImage( - const CesiumGeometry::QuadtreeTileID& tileID) const override { - - LoadTileImageFromUrlOptions options; - options.rectangle = this->getTilingScheme().tileToRectangle(tileID); - options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); + virtual bool getQuadtreeTileImageRequest( + const CesiumGeometry::QuadtreeTileID& tileID, + RequestData& requestData, + std::string&) const override { uint32_t level = tileID.level; uint32_t col = tileID.x; @@ -144,7 +142,7 @@ class WebMapTileServiceTileProvider final "tilematrix={tilematrix}&tilerow={tilerow}&tilecol={tilecol}"; } - std::string url = CesiumUtility::Uri::substituteTemplateParameters( + requestData.url = CesiumUtility::Uri::substituteTemplateParameters( urlTemplate, [&map = urlTemplateMap](const std::string& placeholder) { auto it = map.find(placeholder); @@ -152,7 +150,44 @@ class WebMapTileServiceTileProvider final : CesiumUtility::Uri::escape(it->second); }); - return this->loadTileImageFromUrl(url, this->_headers, std::move(options)); + return true; + } + + virtual CesiumAsync::Future loadQuadtreeTileImage( + const CesiumGeometry::QuadtreeTileID& tileID, + const std::string& requestUrl, + uint16_t statusCode, + const gsl::span& data) const override { + + LoadTileImageFromUrlOptions options; + options.rectangle = this->getTilingScheme().tileToRectangle(tileID); + options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); + + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + std::string message = "Image response code " + + std::to_string(statusCode) + " for " + requestUrl; + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {message}, + {}, + options.moreDetailAvailable}); + } + + if (data.empty()) { + return this->getAsyncSystem().createResolvedFuture( + RasterLoadResult{ + std::nullopt, + options.rectangle, + std::move(options.credits), + {"Image response for " + requestUrl + " is empty."}, + {}, + options.moreDetailAvailable}); + } + + return this->loadTileImageFromUrl(requestUrl, data, std::move(options)); } private: diff --git a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp index 8a231d049..03d80fe40 100644 --- a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp +++ b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp @@ -109,7 +109,10 @@ TEST_CASE("Add raster overlay to glTF") { nullptr, spdlog::default_logger(), nullptr) - .thenInMainThread([&gltf, &modelToEcef, textureCoordinateIndex]( + .thenInMainThread([&gltf, + &modelToEcef, + textureCoordinateIndex, + pMockAssetAccessor]( RasterOverlay::CreateTileProviderResult&& tileProviderResult) { REQUIRE(tileProviderResult); @@ -155,11 +158,23 @@ TEST_CASE("Add raster overlay to glTF") { pRasterTile->getRectangle()); // Go load the texture. - return pTileProvider->loadTile(*pRasterTile) + + RequestData requestData; + RasterProcessingCallback rasterCallback; + pTileProvider->getLoadTileThrottledWork( + *pRasterTile, + requestData, + rasterCallback); + + UrlResponseDataMap responseDataMap; + pMockAssetAccessor->fillResponseDataMap(responseDataMap); + + return pTileProvider + ->loadTile(*pRasterTile, responseDataMap, rasterCallback) .thenPassThrough(std::move(textureTranslationAndScale)); }) .thenInMainThread([&gltf, textureCoordinateIndex]( - std::tuple&& + std::tuple&& tuple) { auto& [textureTranslationAndScale, loadResult] = tuple; diff --git a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp index 7db8c37bb..7e91f0279 100644 --- a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp @@ -52,9 +52,20 @@ class TestTileProvider : public QuadtreeRasterOverlayTileProvider { // The tiles that will return an error from loadQuadtreeTileImage. std::vector errorTiles; - virtual CesiumAsync::Future - loadQuadtreeTileImage(const QuadtreeTileID& tileID) const { - LoadedRasterOverlayImage result; + virtual bool getQuadtreeTileImageRequest( + const CesiumGeometry::QuadtreeTileID&, + RequestData& requestData, + std::string&) const { + requestData.url = "test"; + return true; + }; + + virtual CesiumAsync::Future loadQuadtreeTileImage( + const QuadtreeTileID& tileID, + const std::string&, + uint16_t, + const gsl::span&) const { + RasterLoadResult result; result.rectangle = this->getTilingScheme().tileToRectangle(tileID); if (std::find(errorTiles.begin(), errorTiles.end(), tileID) != @@ -159,7 +170,28 @@ TEST_CASE("QuadtreeRasterOverlayTileProvider getTile") { GeographicProjection::computeMaximumProjectedRectangle(); IntrusivePointer pTile = pProvider->getTile(rectangle, glm::dvec2(256)); - pProvider->loadTile(*pTile); + + auto pMockResponse = std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + std::vector()); + auto pMockRequest = std::make_shared( + "GET", + "test", + CesiumAsync::HttpHeaders{}, + std::move(pMockResponse)); + + RequestData requestData; + RasterProcessingCallback rasterCallback; + pProvider->getLoadTileThrottledWork(*pTile, requestData, rasterCallback); + + UrlResponseDataMap responseDataMap; + responseDataMap.emplace( + "test", + ResponseData{pMockRequest.get(), pMockRequest->response()}); + + pProvider->loadTile(*pTile, responseDataMap, rasterCallback); while (pTile->getState() != RasterOverlayTile::LoadState::Loaded) { asyncSystem.dispatchMainThreadTasks(); @@ -213,7 +245,28 @@ TEST_CASE("QuadtreeRasterOverlayTileProvider getTile") { IntrusivePointer pTile = pProvider->getTile(tileRectangle, targetScreenPixels); - pProvider->loadTile(*pTile); + + RequestData requestData; + RasterProcessingCallback rasterCallback; + pProvider->getLoadTileThrottledWork(*pTile, requestData, rasterCallback); + + auto pMockResponse = std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + std::vector()); + auto pMockRequest = std::make_shared( + "GET", + "test", + CesiumAsync::HttpHeaders{}, + std::move(pMockResponse)); + + UrlResponseDataMap responseDataMap; + responseDataMap.emplace( + "test", + ResponseData{pMockRequest.get(), pMockRequest->response()}); + + pProvider->loadTile(*pTile, responseDataMap, rasterCallback); while (pTile->getState() != RasterOverlayTile::LoadState::Loaded) { asyncSystem.dispatchMainThreadTasks(); diff --git a/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp index bf131b6b8..aeef785da 100644 --- a/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp @@ -14,6 +14,7 @@ using namespace CesiumGltf; using namespace CesiumNativeTests; using namespace CesiumRasterOverlays; using namespace CesiumUtility; +using namespace CesiumAsync; TEST_CASE("TileMapServiceRasterOverlay") { // Set up some mock resources for the raster overlay. @@ -22,6 +23,7 @@ TEST_CASE("TileMapServiceRasterOverlay") { CesiumAsync::AsyncSystem asyncSystem{pMockTaskProcessor}; std::map> mapUrlToRequest; + UrlResponseDataMap responseDataMap; for (const auto& entry : std::filesystem::recursive_directory_iterator( dataDir / "Cesium_Logo_Color")) { if (!entry.is_regular_file()) @@ -37,6 +39,11 @@ TEST_CASE("TileMapServiceRasterOverlay") { url, CesiumAsync::HttpHeaders{}, std::move(pResponse)); + + responseDataMap.emplace( + url, + ResponseData{pRequest.get(), pRequest->response()}); + mapUrlToRequest[url] = std::move(pRequest); } @@ -70,7 +77,18 @@ TEST_CASE("TileMapServiceRasterOverlay") { pTileProvider->getCoverageRectangle(), glm::dvec2(256.0, 256.0)); REQUIRE(pTile); - waitForFuture(asyncSystem, pTileProvider->loadTile(*pTile)); + + RequestData requestData; + RasterProcessingCallback rasterCallback; + pTileProvider->getLoadTileThrottledWork( + *pTile, + requestData, + rasterCallback); + + auto loadFuture = + pTileProvider->loadTile(*pTile, responseDataMap, rasterCallback); + + waitForFuture(asyncSystem, std::move(loadFuture)); ImageCesium& image = pTile->getImage(); CHECK(image.width > 0); diff --git a/doc/raster-overlays.md b/doc/raster-overlays.md index ccfa18669..8ff6622e0 100644 --- a/doc/raster-overlays.md +++ b/doc/raster-overlays.md @@ -10,7 +10,7 @@ While the returned `RasterOverlayTile` is loading, the 3D Tiles engine will use `RasterOverlayTileProvider` also, in general, does not need to do any caching. The 3D Tiles engine will only call `getTile` once per geometry tile. -`getTile` internally calls the polymorphic `loadTileImage`. Derived `RasterOverlayTileProvider` classes implement this method to kick off a request, if necessary, then decode the result and provide the decoded pixels as a `LoadedRasterOverlayImage`. All the lifecycle management is handled automatically by `RasterOverlayTileProvider`, so that derived classes only need to implement this one async method. +`getTile` internally calls the polymorphic `loadTileImage`. Derived `RasterOverlayTileProvider` classes implement this method to kick off a request, if necessary, then decode the result and provide the decoded pixels as an image in `RasterLoadResult`. All the lifecycle management is handled automatically by `RasterOverlayTileProvider`, so that derived classes only need to implement this one async method. # QuadtreeRasterOverlayTileProvider