From 40806a24c777fd2a004e6403e171b389c27aee2e Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 10 Jul 2024 16:00:31 -0400 Subject: [PATCH 01/81] Add ability for images to be shared between glTFs --- Cesium3DTilesSelection/src/Tile.cpp | 2 +- .../src/TilesetContentManager.cpp | 4 +- CesiumGltf/CMakeLists.txt | 25 +- CesiumGltf/include/CesiumGltf/Image.h | 5 +- .../include/CesiumGltf/PropertyTextureView.h | 2 +- .../include/CesiumGltf/SharedAssetDepot.h | 356 ++++++++++++ CesiumGltf/include/CesiumGltf/TextureView.h | 5 +- CesiumGltf/src/PropertyTextureView.cpp | 6 +- CesiumGltf/src/SharedAssetDepot.cpp | 11 + CesiumGltf/src/TextureView.cpp | 4 +- CesiumGltf/test/TestFeatureIdTextureView.cpp | 152 ++--- CesiumGltf/test/TestPropertyTextureView.cpp | 54 +- .../include/CesiumGltfReader/GltfReader.h | 54 +- .../include/CesiumGltfReader/ImageDecoder.h | 67 +++ CesiumGltfReader/src/GltfReader.cpp | 535 +++--------------- CesiumGltfReader/src/ImageDecoder.cpp | 461 +++++++++++++++ CesiumGltfReader/test/TestGltfReader.cpp | 8 +- .../src/QuantizedMeshLoader.cpp | 12 +- .../test/TestAddRasterOverlayToGltf.cpp | 2 +- 19 files changed, 1146 insertions(+), 619 deletions(-) create mode 100644 CesiumGltf/include/CesiumGltf/SharedAssetDepot.h create mode 100644 CesiumGltf/src/SharedAssetDepot.cpp create mode 100644 CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h create mode 100644 CesiumGltfReader/src/ImageDecoder.cpp diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 932dd5b0d..a5e040691 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -175,7 +175,7 @@ int64_t Tile::computeByteSize() const noexcept { // sizeBytes is set in TilesetContentManager::ContentKindSetter, if not // sooner (e.g., by the renderer implementation). - bytes += image.cesium.sizeBytes; + bytes += image.cesium->sizeBytes; } } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 78308bc64..d46908c5d 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -53,8 +53,8 @@ struct ContentKindSetter { // size now. We'll be adding this number to our total memory usage soon, // and remove it when the tile is later unloaded, and we must use // the same size in each case. - if (image.cesium.sizeBytes < 0) { - image.cesium.sizeBytes = int64_t(image.cesium.pixelData.size()); + if (image.cesium->sizeBytes < 0) { + image.cesium->sizeBytes = int64_t(image.cesium->pixelData.size()); } } diff --git a/CesiumGltf/CMakeLists.txt b/CesiumGltf/CMakeLists.txt index 0c15ce729..4704078c7 100644 --- a/CesiumGltf/CMakeLists.txt +++ b/CesiumGltf/CMakeLists.txt @@ -22,8 +22,8 @@ cesium_glob_files(CESIUM_GLTF_TEST_HEADERS set_target_properties(CesiumGltf PROPERTIES - TEST_SOURCES "${CESIUM_GLTF_TEST_SOURCES}" - TEST_HEADERS "${CESIUM_GLTF_TEST_HEADERS}" + TEST_SOURCES "${CESIUM_GLTF_TEST_SOURCES}" + TEST_HEADERS "${CESIUM_GLTF_TEST_HEADERS}" ) set_target_properties(CesiumGltf @@ -34,27 +34,28 @@ set_target_properties(CesiumGltf target_sources( CesiumGltf PRIVATE - ${CESIUM_GLTF_SOURCES} - ${CESIUM_GLTF_HEADERS} + ${CESIUM_GLTF_SOURCES} + ${CESIUM_GLTF_HEADERS} PUBLIC - ${CESIUM_GLTF_PUBLIC_HEADERS} + ${CESIUM_GLTF_PUBLIC_HEADERS} ) target_include_directories( CesiumGltf SYSTEM PUBLIC - ${CMAKE_CURRENT_LIST_DIR}/include/ - ${CMAKE_CURRENT_LIST_DIR}/generated/include + ${CMAKE_CURRENT_LIST_DIR}/include/ + ${CMAKE_CURRENT_LIST_DIR}/generated/include PRIVATE - ${CESIUM_NATIVE_RAPIDJSON_INCLUDE_DIR} - ${CMAKE_CURRENT_LIST_DIR}/src - ${CMAKE_CURRENT_LIST_DIR}/generated/src + ${CESIUM_NATIVE_RAPIDJSON_INCLUDE_DIR} + ${CMAKE_CURRENT_LIST_DIR}/src + ${CMAKE_CURRENT_LIST_DIR}/generated/src ) target_link_libraries(CesiumGltf PUBLIC - CesiumUtility - GSL + CesiumUtility + CesiumAsync + GSL ) install(TARGETS CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index 8ddfa19be..ee50450b4 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -4,6 +4,9 @@ #include "CesiumGltf/ImageSpec.h" #include "CesiumGltf/Library.h" +#include + + namespace CesiumGltf { /** @copydoc ImageSpec */ struct CESIUMGLTF_API Image final : public ImageSpec { @@ -11,6 +14,6 @@ struct CESIUMGLTF_API Image final : public ImageSpec { * @brief Holds properties that are specific to the glTF loader rather than * part of the glTF spec. */ - ImageCesium cesium; + SharedAsset cesium; }; } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h index 2f2ac0d72..720e4d839 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h @@ -748,7 +748,7 @@ class PropertyTextureView { return PropertyTexturePropertyView(status); } - const ImageCesium& image = _pModel->images[imageIndex].cesium; + const ImageCesium& image = *_pModel->images[imageIndex].cesium; const std::vector& channels = propertyTextureProperty.channels; status = checkChannels(channels, image); diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h new file mode 100644 index 000000000..64873b2c6 --- /dev/null +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -0,0 +1,356 @@ +#pragma once + +#include "CesiumGltf/ImageCesium.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CesiumGltf { + +namespace SharedAssetDepotInternals { + +typedef size_t IdHash; +template class SharedAsset; +template class SingleAssetDepot; + +/** + * Implemented for assets that can be constructed from data fetched from a URL. + */ +template class AssetFactory { +public: + /** + * Creates a new instance of this asset out of the given data, if possible. + */ + virtual std::optional + createFrom(const gsl::span& data) const = 0; +}; + +/** + * Contains the current state of an asset within the SharedAssetDepot. + */ +template class AssetContainer { +public: + uint32_t counter; + IdHash assetId; + AssetType asset; + + // Pointer to this container's parent so we know how to clean it up. + SingleAssetDepot* parent; + + AssetContainer( + IdHash assetId_, + AssetType& asset_, + SingleAssetDepot* parent_) + : counter(0), assetId(assetId_), asset(asset_), parent(parent_) {} + SharedAsset toRef() { return SharedAsset(this); } +}; + +/** + * An asset that may or may not be stored in the SharedAssetDepot and shared + * across multiple tiles. This can either be an ImageCesium itself or a + * reference-counted pointer to an entry in the SharedAssetDepot. You should + * always treat it as if it was an ImageCesium, not a smart pointer. + * @tparam AssetType The type of the asset that we're getting a reference to. + */ +template class SharedAsset { + typedef std::variant*, AssetType> AssetContents; + +public: + SharedAsset(const AssetType& asset) : contents(asset) {} + SharedAsset(std::nullptr_t) : contents(nullptr) {} + SharedAsset() : contents(AssetType()) {} + + AssetType* get() { + struct Operation { + AssetType* operator()(AssetType& asset) { return &asset; } + + AssetType* operator()(AssetContainer* container) { + return &container->asset; + } + }; + + return std::visit(Operation{}, this->contents); + } + + const AssetType* get() const { + struct Operation { + const AssetType* operator()(const AssetType& asset) { return &asset; } + + const AssetType* operator()(const AssetContainer* container) { + return &container->asset; + } + }; + + return std::visit(Operation{}, this->contents); + } + + AssetType* operator->() { return this->get(); } + const AssetType* operator->() const { return this->get(); } + AssetType& operator*() { return *this->get(); } + const AssetType& operator*() const { return *this->get(); } + + bool operator==(const SharedAsset& other) const { + return other.get() == this->get(); + } + + bool operator==(SharedAsset& other) { + return other.get() == this->get(); + } + + bool operator!=(const SharedAsset& other) const { + return other.get() != this->get(); + } + + bool operator!=(SharedAsset& other) { + return other.get() != this->get(); + } + + /** + * Copy assignment operator for SharedAsset. + */ + void operator=(const SharedAsset& other) { + if (*this != other) { + // Decrement this reference + this->maybeChangeCounter(-1); + this->contents = other.contents; + // Increment the new reference + this->maybeChangeCounter(1); + } + } + + /** + * Move assignment operator for SharedAsset. + */ + void operator=(SharedAsset&& other) { + if (*this != other) { + this->maybeChangeCounter(-1); + this->contents = other.contents; + } + } + + ~SharedAsset() { this->maybeChangeCounter(-1); } + + /** + * Copy constructor. + */ + SharedAsset(const SharedAsset& other) { + contents = other.contents; + this->maybeChangeCounter(1); + } + + /** + * Move constructor. + */ + SharedAsset(SharedAsset&& other) noexcept { + contents = other.contents; + } + +private: + SharedAsset(AssetContainer* container) : contents(container) { + this->maybeChangeCounter(1); + } + + void maybeChangeCounter(int amt) { + struct Operation { + int amt; + + void operator()(AssetContainer* asset) { + if (asset != nullptr) { + asset->counter += this->amt; + if (asset->counter <= 0) { + asset->parent->remove(asset->assetId); + } + } + } + + void operator()(AssetType& asset) {} + }; + + std::visit(Operation{amt}, this->contents); + } + + // A SharedAssetRef might point to an asset the SharedAssetDepot, or it + // might not. If it doesn't, we own this asset now. + AssetContents contents; + + friend class SharedAssetDepot; + friend class AssetContainer; + friend class SingleAssetDepot; +}; + +/** + * Contains one or more assets of the given type. + * @tparam AssetType The type of asset stored in this depot. + */ +template class SingleAssetDepot { +public: + SingleAssetDepot(); + + /** + * Stores the AssetType in this depot and returns a reference to it, + * or returns the existing asset if present. + */ + SharedAsset store(IdHash assetId, AssetType& asset) { + auto it = this->assets.find(assetId); + if (it != this->assets.end()) { + // already stored + return it->second.toRef(); + } + + auto [newIt, ok] = + this->assets.emplace(assetId, AssetContainer(assetId, asset, this)); + CESIUM_ASSERT(ok); + + return newIt->second.toRef(); + } + + /** + * Fetches an asset that implements {@link FetchableAsset} and constructs it if possible. + * If the asset is already in this depot, it will be returned instead. + * If the asset has already started loading in this depot but hasn't finished, + * its future will be returned. + */ + template + CesiumAsync::SharedFuture>> getOrFetch( + CesiumAsync::AsyncSystem& asyncSystem, + std::shared_ptr& pAssetAccessor, + typename std::enable_if< + std::is_base_of_v, Factory>, + const Factory&>::type factory, + std::string& uri, + std::vector headers) { + IdHash idHash = std::hash{}(uri); + + auto existingIt = this->assets.find(idHash); + if (existingIt != this->assets.end()) { + // We've already loaded an asset with this ID - we can just use that. + return asyncSystem + .createResolvedFuture(std::optional>( + SharedAsset(&existingIt->second))) + .share(); + } + + auto pendingIt = this->pendingAssets.find(idHash); + if (pendingIt != this->pendingAssets.end()) { + // Return the existing future - the caller can chain off of it. + return pendingIt->second; + } + + // We haven't loaded or started to load this asset yet. + // Let's do that now. + CesiumAsync::Future>> future = + pAssetAccessor->get(asyncSystem, uri, headers) + .thenInWorkerThread( + [factory]( + std::shared_ptr&& pRequest) { + const CesiumAsync::IAssetResponse* pResponse = + pRequest->response(); + if (!pResponse) { + return std::optional(std::nullopt); + } + + return factory.createFrom(pResponse->data()); + }) + // Do this in main thread since we're messing with the collections. + .thenInMainThread([idHash, + pThiz = this, + pAssets = &this->assets, + pPendingAssets = &this->pendingAssets]( + std::optional& result) { + // Get rid of our future. + pPendingAssets->erase(idHash); + + if (result.has_value()) { + auto& [it, ok] = pAssets->emplace( + idHash, + AssetContainer(idHash, result.value(), pThiz)); + if (!ok) { + return std::optional>(); + } + + return std::optional>( + SharedAsset(&it->second)); + } + + return std::optional>(); + }); + + auto& [it, ok] = + this->pendingAssets.emplace(idHash, std::move(future).share()); + if (!ok) { + return asyncSystem + .createResolvedFuture(std::optional>()) + .share(); + } + + return it->second; + } + +private: + /** + * Removes the given asset from the depot. + * Should only be called by {@link SharedAsset}. + */ + void remove(IdHash hash) { this->assets.erase(hash); } + + // Assets that have a unique ID that can be used to de-duplicate them. + std::unordered_map> assets; + // Futures for assets that still aren't loaded yet. + std::unordered_map< + IdHash, + CesiumAsync::SharedFuture>>> + pendingAssets; + + friend class SharedAsset; +}; + +/** + * @brief Contains assets that are potentially shared across multiple tiles. + */ +class SharedAssetDepot { +public: + SharedAsset store(CesiumGltf::ImageCesium& image); + + /** + * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. + */ + template + CesiumAsync::SharedFuture>> getOrFetch( + CesiumAsync::AsyncSystem& asyncSystem, + std::shared_ptr& pAssetAccessor, + typename std::enable_if< + std::is_base_of_v, Factory>, + const Factory&>::type factory, + std::string& uri, + std::vector headers) { + return images.getOrFetch( + asyncSystem, + pAssetAccessor, + factory, + uri, + headers); + } + +private: + SingleAssetDepot images; +}; + +} // namespace SharedAssetDepotInternals + +// actually export the public types to the right namespace +using SharedAssetDepotInternals::AssetFactory; +using SharedAssetDepotInternals::SharedAsset; +using SharedAssetDepotInternals::SharedAssetDepot; + +} // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/TextureView.h b/CesiumGltf/include/CesiumGltf/TextureView.h index 9eb15af19..401149d93 100644 --- a/CesiumGltf/include/CesiumGltf/TextureView.h +++ b/CesiumGltf/include/CesiumGltf/TextureView.h @@ -3,6 +3,7 @@ #include "CesiumGltf/ImageCesium.h" #include "CesiumGltf/KhrTextureTransform.h" #include "CesiumGltf/Sampler.h" +#include "CesiumGltf/SharedAssetDepot.h" #include "CesiumGltf/TextureInfo.h" #include @@ -176,7 +177,7 @@ class TextureView { if (this->_imageCopy) { return &(this->_imageCopy.value()); } - return this->_pImage; + return &*this->_pImage; } /** @@ -210,7 +211,7 @@ class TextureView { TextureViewStatus _textureViewStatus; const Sampler* _pSampler; - const ImageCesium* _pImage; + SharedAsset _pImage; int64_t _texCoordSetIndex; bool _applyTextureTransform; diff --git a/CesiumGltf/src/PropertyTextureView.cpp b/CesiumGltf/src/PropertyTextureView.cpp index f5ac76a11..f8c3db846 100644 --- a/CesiumGltf/src/PropertyTextureView.cpp +++ b/CesiumGltf/src/PropertyTextureView.cpp @@ -80,14 +80,14 @@ PropertyTextureView::checkImage(const int32_t imageIndex) const noexcept { return PropertyTexturePropertyViewStatus::ErrorInvalidImage; } - const ImageCesium& image = + const SharedAsset& image = _pModel->images[static_cast(imageIndex)].cesium; - if (image.width < 1 || image.height < 1) { + if (image->width < 1 || image->height < 1) { return PropertyTexturePropertyViewStatus::ErrorEmptyImage; } - if (image.bytesPerChannel > 1) { + if (image->bytesPerChannel > 1) { return PropertyTexturePropertyViewStatus::ErrorInvalidBytesPerChannel; } diff --git a/CesiumGltf/src/SharedAssetDepot.cpp b/CesiumGltf/src/SharedAssetDepot.cpp new file mode 100644 index 000000000..1842c9aa7 --- /dev/null +++ b/CesiumGltf/src/SharedAssetDepot.cpp @@ -0,0 +1,11 @@ +#include "CesiumGltf/SharedAssetDepot.h" + +#include "CesiumGltf/ImageCesium.h" + +#include + +namespace CesiumGltf { + +namespace SharedAssetDepotInternals {} // namespace SharedAssetDepotInternals + +} // namespace CesiumGltf diff --git a/CesiumGltf/src/TextureView.cpp b/CesiumGltf/src/TextureView.cpp index e1b267491..1ddeba939 100644 --- a/CesiumGltf/src/TextureView.cpp +++ b/CesiumGltf/src/TextureView.cpp @@ -41,7 +41,7 @@ TextureView::TextureView( return; } - this->_pImage = &model.images[static_cast(texture.source)].cesium; + this->_pImage = model.images[static_cast(texture.source)].cesium; if (this->_pImage->width < 1 || this->_pImage->height < 1) { this->_textureViewStatus = TextureViewStatus::ErrorEmptyImage; return; @@ -85,7 +85,7 @@ TextureView::TextureView( const TextureViewOptions& options) noexcept : _textureViewStatus(TextureViewStatus::ErrorUninitialized), _pSampler(&sampler), - _pImage(&image), + _pImage(image), _texCoordSetIndex(textureCoordinateSetIndex), _applyTextureTransform(options.applyKhrTextureTransformExtension), _textureTransform(std::nullopt), diff --git a/CesiumGltf/test/TestFeatureIdTextureView.cpp b/CesiumGltf/test/TestFeatureIdTextureView.cpp index 2b1857e81..2df7bc40e 100644 --- a/CesiumGltf/test/TestFeatureIdTextureView.cpp +++ b/CesiumGltf/test/TestFeatureIdTextureView.cpp @@ -75,8 +75,8 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with empty image") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 0; - image.cesium.height = 0; + image.cesium->width = 0; + image.cesium->height = 0; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -107,9 +107,9 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with too many bytes " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; - image.cesium.bytesPerChannel = 2; + image.cesium->width = 1; + image.cesium->height = 1; + image.cesium->bytesPerChannel = 2; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -142,8 +142,8 @@ TEST_CASE( sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -174,8 +174,8 @@ TEST_CASE( sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -206,8 +206,8 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with out of range " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -237,8 +237,8 @@ TEST_CASE("Test FeatureIdTextureView on valid feature ID texture") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -269,8 +269,8 @@ TEST_CASE("Test FeatureIdTextureView with applyKhrTextureTransformExtension = " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -318,8 +318,8 @@ TEST_CASE("Test FeatureIdTextureView with applyKhrTextureTransformExtension = " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -371,13 +371,13 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; - image.cesium.pixelData.resize(featureIDs.size()); + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; + image.cesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium.pixelData.data(), + image.cesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -403,14 +403,14 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { // Clear the original image data. std::vector emptyData; - image.cesium.pixelData.swap(emptyData); + image.cesium->pixelData.swap(emptyData); const ImageCesium* pImage = view.getImage(); REQUIRE(pImage); - REQUIRE(pImage->width == image.cesium.width); - REQUIRE(pImage->height == image.cesium.height); - REQUIRE(pImage->channels == image.cesium.channels); - REQUIRE(pImage->bytesPerChannel == image.cesium.bytesPerChannel); + REQUIRE(pImage->width == image.cesium->width); + REQUIRE(pImage->height == image.cesium->height); + REQUIRE(pImage->channels == image.cesium->channels); + REQUIRE(pImage->bytesPerChannel == image.cesium->bytesPerChannel); REQUIRE(pImage->pixelData.size() == featureIDs.size()); } @@ -423,8 +423,8 @@ TEST_CASE("Test getFeatureID on invalid feature ID texture view") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 1; - image.cesium.height = 1; + image.cesium->width = 1; + image.cesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -457,13 +457,13 @@ TEST_CASE("Test getFeatureID on valid feature ID texture view") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; - image.cesium.pixelData.resize(featureIDs.size()); + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; + image.cesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium.pixelData.data(), + image.cesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -502,13 +502,13 @@ TEST_CASE("Test getFeatureID on view with applyKhrTextureTransformExtension = " std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; - image.cesium.pixelData.resize(featureIDs.size()); + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; + image.cesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium.pixelData.data(), + image.cesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -553,13 +553,13 @@ TEST_CASE( std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; - image.cesium.pixelData.resize(featureIDs.size()); + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; + image.cesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium.pixelData.data(), + image.cesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -609,13 +609,13 @@ TEST_CASE("Test getFeatureId on view with makeImageCopy = true") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; - image.cesium.pixelData.resize(featureIDs.size()); + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; + image.cesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium.pixelData.data(), + image.cesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -641,7 +641,7 @@ TEST_CASE("Test getFeatureId on view with makeImageCopy = true") { // Clear the original image data. std::vector emptyData; - image.cesium.pixelData.swap(emptyData); + image.cesium->pixelData.swap(emptyData); REQUIRE(view.getFeatureID(0, 0) == 1); REQUIRE(view.getFeatureID(1, 0) == 2); @@ -660,13 +660,13 @@ TEST_CASE("Test getFeatureID rounds to nearest pixel") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; - image.cesium.pixelData.resize(featureIDs.size()); + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; + image.cesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium.pixelData.data(), + image.cesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -704,12 +704,12 @@ TEST_CASE("Test getFeatureID clamps values") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; - auto& data = image.cesium.pixelData; + auto& data = image.cesium->pixelData; data.resize(featureIDs.size() * sizeof(uint8_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); @@ -747,12 +747,12 @@ TEST_CASE("Test getFeatureID handles multiple channels") { std::vector featureIDs{260, 512, 8, 17}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 2; - image.cesium.bytesPerChannel = 1; + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 2; + image.cesium->bytesPerChannel = 1; - auto& data = image.cesium.pixelData; + auto& data = image.cesium->pixelData; data.resize(featureIDs.size() * sizeof(uint16_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); @@ -788,12 +788,12 @@ TEST_CASE("Check FeatureIdTextureView sampling with different wrap values") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium.width = 2; - image.cesium.height = 2; - image.cesium.channels = 1; - image.cesium.bytesPerChannel = 1; + image.cesium->width = 2; + image.cesium->height = 2; + image.cesium->channels = 1; + image.cesium->bytesPerChannel = 1; - auto& data = image.cesium.pixelData; + auto& data = image.cesium->pixelData; data.resize(featureIDs.size() * sizeof(uint8_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); diff --git a/CesiumGltf/test/TestPropertyTextureView.cpp b/CesiumGltf/test/TestPropertyTextureView.cpp index a163fb0b2..8799d8e16 100644 --- a/CesiumGltf/test/TestPropertyTextureView.cpp +++ b/CesiumGltf/test/TestPropertyTextureView.cpp @@ -19,12 +19,12 @@ void addTextureToModel( int32_t channels, const std::vector& data) { Image& image = model.images.emplace_back(); - image.cesium.width = width; - image.cesium.height = height; - image.cesium.channels = channels; - image.cesium.bytesPerChannel = 1; + image.cesium->width = width; + image.cesium->height = height; + image.cesium->channels = channels; + image.cesium->bytesPerChannel = 1; - std::vector& imageData = image.cesium.pixelData; + std::vector& imageData = image.cesium->pixelData; imageData.resize(data.size()); std::memcpy(imageData.data(), data.data(), data.size()); @@ -248,7 +248,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap(emptyData); + model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -308,7 +308,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium.channels = 2; + model.images[imageIndex].cesium->channels = 2; propertyTextureProperty.channels = {0, 1}; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); @@ -336,7 +336,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].cesium.bytesPerChannel = 2; + model.images[imageIndex].cesium->bytesPerChannel = 2; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -345,7 +345,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Empty image") { - model.images[imageIndex].cesium.width = 0; + model.images[imageIndex].cesium->width = 0; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -493,7 +493,7 @@ TEST_CASE("Test scalar PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap(emptyData); + model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -556,7 +556,7 @@ TEST_CASE("Test scalar PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium.channels = 2; + model.images[imageIndex].cesium->channels = 2; propertyTextureProperty.channels = {0, 1}; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); @@ -689,7 +689,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap(emptyData); + model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -750,7 +750,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium.channels = 4; + model.images[imageIndex].cesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); @@ -769,7 +769,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].cesium.bytesPerChannel = 2; + model.images[imageIndex].cesium->bytesPerChannel = 2; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -903,7 +903,7 @@ TEST_CASE("Test vecN PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap(emptyData); + model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -973,7 +973,7 @@ TEST_CASE("Test vecN PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium.channels = 4; + model.images[imageIndex].cesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); @@ -1150,7 +1150,7 @@ TEST_CASE("Test array PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap(emptyData); + model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -1218,7 +1218,7 @@ TEST_CASE("Test array PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium.channels = 4; + model.images[imageIndex].cesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView> uint8ArrayProperty = view.getPropertyView>("TestClassProperty"); @@ -1237,7 +1237,7 @@ TEST_CASE("Test array PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].cesium.bytesPerChannel = 2; + model.images[imageIndex].cesium->bytesPerChannel = 2; PropertyTexturePropertyView> uint8ArrayProperty = view.getPropertyView>("TestClassProperty"); REQUIRE( @@ -1418,7 +1418,7 @@ TEST_CASE("Test array PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap(emptyData); + model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -1487,7 +1487,7 @@ TEST_CASE("Test array PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium.channels = 4; + model.images[imageIndex].cesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView, true> uint8ArrayProperty = @@ -2239,7 +2239,7 @@ TEST_CASE("Test callback for scalar PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap( + model.images[model.images.size() - 1].cesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2361,7 +2361,7 @@ TEST_CASE("Test callback for scalar PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap( + model.images[model.images.size() - 1].cesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2492,7 +2492,7 @@ TEST_CASE("Test callback for vecN PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap( + model.images[model.images.size() - 1].cesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2624,7 +2624,7 @@ TEST_CASE("Test callback for vecN PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap( + model.images[model.images.size() - 1].cesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2776,7 +2776,7 @@ TEST_CASE("Test callback for array PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap( + model.images[model.images.size() - 1].cesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2947,7 +2947,7 @@ TEST_CASE("Test callback for array PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium.pixelData.swap( + model.images[model.images.size() - 1].cesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index d3d47178a..527800ad2 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -1,5 +1,6 @@ #pragma once +#include "CesiumGltfReader/ImageDecoder.h" #include "CesiumGltfReader/Library.h" #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include @@ -43,30 +45,6 @@ struct CESIUMGLTFREADER_API GltfReaderResult { std::vector warnings; }; -/** - * @brief The result of reading an image with - * {@link GltfReader::readImage}. - */ -struct CESIUMGLTFREADER_API ImageReaderResult { - - /** - * @brief The {@link ImageCesium} that was read. - * - * This will be `std::nullopt` if the image could not be read. - */ - std::optional image; - - /** - * @brief Error messages that occurred while trying to read the image. - */ - std::vector errors; - - /** - * @brief Warning messages that occurred while reading the image. - */ - std::vector warnings; -}; - /** * @brief Options for how to read a glTF. */ @@ -126,6 +104,14 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * the ideal target gpu-compressed pixel format to transcode to. */ CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; + + /** + * The depot that will be used to store all of the shared assets that might + * appear in this glTF. If not present, assets will not be shared between + * glTFs, even if they're loaded from the same URL. + */ + std::variant sharedAssets = + std::monostate(); }; /** @@ -208,20 +194,14 @@ class CESIUMGLTFREADER_API GltfReader { GltfReaderResult&& result); /** - * @brief Reads an image from a buffer. - * - * The [stb_image](https://github.com/nothings/stb) library is used to decode - * images in `JPG`, `PNG`, `TGA`, `BMP`, `PSD`, `GIF`, `HDR`, or `PIC` format. - * - * @param data The buffer from which to read the image. - * @param ktx2TranscodeTargetFormat The compression format to transcode - * KTX v2 textures into. If this is std::nullopt, KTX v2 textures will be - * fully decompressed into raw pixels. - * @return The result of reading the image. + * Reads an Image from a buffer. + * @deprecated Use {@link ImageDecoder::readImage} instead. */ static ImageReaderResult readImage( const gsl::span& data, - const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets); + const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets) { + return ImageDecoder::readImage(data, ktx2TranscodeTargets); + } /** * @brief Generate mipmaps for this image. @@ -233,7 +213,9 @@ class CESIUMGLTFREADER_API GltfReader { * @return A string describing the error, if unable to generate mipmaps. */ static std::optional - generateMipMaps(CesiumGltf::ImageCesium& image); + generateMipMaps(CesiumGltf::ImageCesium& image) { + return ImageDecoder::generateMipMaps(image); + } private: CesiumJsonReader::JsonReaderOptions _context; diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h new file mode 100644 index 000000000..57b7a6ace --- /dev/null +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -0,0 +1,67 @@ +#include "CesiumGltf/ImageCesium.h" +#include "CesiumGltfReader/Library.h" + +#include + +#include +#include +#include + +namespace CesiumGltfReader { + +/** + * @brief The result of reading an image with + * {@link ImageDecoder::readImage}. + */ +struct CESIUMGLTFREADER_API ImageReaderResult { + + /** + * @brief The {@link ImageCesium} that was read. + * + * This will be `std::nullopt` if the image could not be read. + */ + std::optional image; + + /** + * @brief Error messages that occurred while trying to read the image. + */ + std::vector errors; + + /** + * @brief Warning messages that occurred while reading the image. + */ + std::vector warnings; +}; + +class ImageDecoder { +public: + /** + * @brief Reads an image from a buffer. + * + * The [stb_image](https://github.com/nothings/stb) library is used to decode + * images in `JPG`, `PNG`, `TGA`, `BMP`, `PSD`, `GIF`, `HDR`, or `PIC` format. + * + * @param data The buffer from which to read the image. + * @param ktx2TranscodeTargetFormat The compression format to transcode + * KTX v2 textures into. If this is std::nullopt, KTX v2 textures will be + * fully decompressed into raw pixels. + * @return The result of reading the image. + */ + static ImageReaderResult readImage( + const gsl::span& data, + const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets); + + /** + * @brief Generate mipmaps for this image. + * + * Does nothing if mipmaps already exist or the compressedPixelFormat is not + * GpuCompressedPixelFormat::NONE. + * + * @param image The image to generate mipmaps for. * + * @return A string describing the error, if unable to generate mipmaps. + */ + static std::optional + generateMipMaps(CesiumGltf::ImageCesium& image); +}; + +} // namespace CesiumGltfReader diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 8007eb22e..99dd4baf4 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -8,10 +8,14 @@ #include "dequantizeMeshData.h" #include "registerReaderExtensions.h" +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -25,32 +29,41 @@ #include #include #include +#include #include #include -#define STBI_FAILURE_USERMSG - -namespace Cesium { -// Use STB resize in our own namespace to avoid conflicts from other libs -#define STBIRDEF -#define STB_IMAGE_RESIZE_IMPLEMENTATION -#include -#undef STBIRDEF -}; // namespace Cesium - -#define STB_IMAGE_STATIC -#define STB_IMAGE_IMPLEMENTATION -#include -#include - using namespace CesiumAsync; using namespace CesiumGltf; using namespace CesiumGltfReader; using namespace CesiumJsonReader; using namespace CesiumUtility; -using namespace Cesium; namespace { + +/** + * Used to construct a CesiumImage. + */ +struct ImageAssetFactory : public AssetFactory { + ImageAssetFactory(const Ktx2TranscodeTargets& ktx2TranscodeTargets_) + : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} + + virtual std::optional + createFrom(const gsl::span& data) const override { + ImageReaderResult imageResult = + ImageDecoder::readImage(data, this->ktx2TranscodeTargets); + + if (imageResult.image) { + return std::optional(imageResult.image); + } + + return std::nullopt; + } + +private: + const Ktx2TranscodeTargets ktx2TranscodeTargets; +}; + #pragma pack(push, 1) struct GlbHeader { uint32_t magic; @@ -276,7 +289,7 @@ void postprocess( } // Image has already been decoded - if (!image.cesium.pixelData.empty()) { + if (!image.cesium->pixelData.empty()) { continue; } @@ -301,7 +314,7 @@ void postprocess( static_cast(bufferView.byteOffset), static_cast(bufferView.byteLength)); ImageReaderResult imageResult = - GltfReader::readImage(bufferViewSpan, options.ktx2TranscodeTargets); + ImageDecoder::readImage(bufferViewSpan, options.ktx2TranscodeTargets); readGltf.warnings.insert( readGltf.warnings.end(), imageResult.warnings.begin(), @@ -311,7 +324,7 @@ void postprocess( imageResult.errors.begin(), imageResult.errors.end()); if (imageResult.image) { - image.cesium = std::move(imageResult.image.value()); + image.cesium = SharedAsset(imageResult.image.value()); } else { if (image.mimeType) { readGltf.errors.emplace_back( @@ -536,30 +549,72 @@ void CesiumGltfReader::GltfReader::postprocessGltf( for (Image& image : pResult->model->images) { if (image.uri && image.uri->substr(0, dataPrefixLength) != dataPrefix) { - resolvedBuffers.push_back( - pAssetAccessor - ->get(asyncSystem, Uri::resolve(baseUrl, *image.uri), tHeaders) + const std::string uri = Uri::resolve(baseUrl, *image.uri); + + struct Operation { + ImageAssetFactory factory; + AsyncSystem& asyncSystem; + std::shared_ptr pAssetAccessor; + std::string uri; + std::vector headers; + + SharedFuture>> + operator()(std::monostate) { + // We don't have a depot, we have to fetch this the old way. + return pAssetAccessor->get(asyncSystem, uri, headers) .thenInWorkerThread( - [pImage = &image, - ktx2TranscodeTargets = options.ktx2TranscodeTargets]( + [uri = this->uri, pFactory = &this->factory]( std::shared_ptr&& pRequest) { const IAssetResponse* pResponse = pRequest->response(); - std::string imageUri = *pImage->uri; - if (pResponse) { - pImage->uri = std::nullopt; - - ImageReaderResult imageResult = - readImage(pResponse->data(), ktx2TranscodeTargets); - if (imageResult.image) { - pImage->cesium = std::move(*imageResult.image); - return ExternalBufferLoadResult{true, imageUri}; + gsl::span bytes = pResponse->data(); + auto asset = pFactory->createFrom(bytes); + if (asset.has_value()) { + return std::optional>( + asset.value()); } } - return ExternalBufferLoadResult{false, imageUri}; - })); + return std::optional>(); + }) + .share(); + } + + SharedFuture>> + operator()(SharedAssetDepot* depot) { + // We have a depot, this is easy! + return depot->getOrFetch( + asyncSystem, + pAssetAccessor, + factory, + uri, + headers); + } + }; + + SharedFuture>> future = std::visit( + Operation{ + ImageAssetFactory(options.ktx2TranscodeTargets), + asyncSystem, + pAssetAccessor, + uri, + tHeaders}, + options.sharedAssets); + + resolvedBuffers.push_back(future.thenInWorkerThread( + [pImage = &image]( + std::optional> maybeLoadedImage) { + std::string imageUri = *pImage->uri; + pImage->uri = std::nullopt; + + if (maybeLoadedImage.has_value()) { + pImage->cesium = std::move(maybeLoadedImage.value()); + return ExternalBufferLoadResult{true, imageUri}; + } + + return ExternalBufferLoadResult{false, imageUri}; + })); } } @@ -577,413 +632,3 @@ void CesiumGltfReader::GltfReader::postprocessGltf( return std::move(*pResult.release()); }); } - -bool isKtx(const gsl::span& data) { - const size_t ktxMagicByteLength = 12; - if (data.size() < ktxMagicByteLength) { - return false; - } - - const uint8_t ktxMagic[ktxMagicByteLength] = - {0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A}; - - return memcmp(data.data(), ktxMagic, ktxMagicByteLength) == 0; -} - -bool isWebP(const gsl::span& data) { - if (data.size() < 12) { - return false; - } - const uint32_t magic1 = *reinterpret_cast(data.data()); - const uint32_t magic2 = *reinterpret_cast(data.data() + 8); - return magic1 == 0x46464952 && magic2 == 0x50424557; -} - -/*static*/ -ImageReaderResult GltfReader::readImage( - const gsl::span& data, - const Ktx2TranscodeTargets& ktx2TranscodeTargets) { - CESIUM_TRACE("CesiumGltfReader::readImage"); - - ImageReaderResult result; - - result.image.emplace(); - ImageCesium& image = result.image.value(); - - if (isKtx(data)) { - ktxTexture2* pTexture = nullptr; - KTX_error_code errorCode; - - errorCode = ktxTexture2_CreateFromMemory( - reinterpret_cast(data.data()), - data.size(), - KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, - &pTexture); - - if (errorCode == KTX_SUCCESS) { - if (ktxTexture2_NeedsTranscoding(pTexture)) { - - CESIUM_TRACE("Transcode KTXv2"); - - image.channels = - static_cast(ktxTexture2_GetNumComponents(pTexture)); - GpuCompressedPixelFormat transcodeTargetFormat = - GpuCompressedPixelFormat::NONE; - - if (pTexture->supercompressionScheme == KTX_SS_BASIS_LZ) { - switch (image.channels) { - case 1: - transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_R; - break; - case 2: - transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_RG; - break; - case 3: - transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_RGB; - break; - // case 4: - default: - transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_RGBA; - } - } else { - switch (image.channels) { - case 1: - transcodeTargetFormat = ktx2TranscodeTargets.UASTC_R; - break; - case 2: - transcodeTargetFormat = ktx2TranscodeTargets.UASTC_RG; - break; - case 3: - transcodeTargetFormat = ktx2TranscodeTargets.UASTC_RGB; - break; - // case 4: - default: - transcodeTargetFormat = ktx2TranscodeTargets.UASTC_RGBA; - } - } - - ktx_transcode_fmt_e transcodeTargetFormat_ = KTX_TTF_RGBA32; - switch (transcodeTargetFormat) { - case GpuCompressedPixelFormat::ETC1_RGB: - transcodeTargetFormat_ = KTX_TTF_ETC1_RGB; - break; - case GpuCompressedPixelFormat::ETC2_RGBA: - transcodeTargetFormat_ = KTX_TTF_ETC2_RGBA; - break; - case GpuCompressedPixelFormat::BC1_RGB: - transcodeTargetFormat_ = KTX_TTF_BC1_RGB; - break; - case GpuCompressedPixelFormat::BC3_RGBA: - transcodeTargetFormat_ = KTX_TTF_BC3_RGBA; - break; - case GpuCompressedPixelFormat::BC4_R: - transcodeTargetFormat_ = KTX_TTF_BC4_R; - break; - case GpuCompressedPixelFormat::BC5_RG: - transcodeTargetFormat_ = KTX_TTF_BC5_RG; - break; - case GpuCompressedPixelFormat::BC7_RGBA: - transcodeTargetFormat_ = KTX_TTF_BC7_RGBA; - break; - case GpuCompressedPixelFormat::PVRTC1_4_RGB: - transcodeTargetFormat_ = KTX_TTF_PVRTC1_4_RGB; - break; - case GpuCompressedPixelFormat::PVRTC1_4_RGBA: - transcodeTargetFormat_ = KTX_TTF_PVRTC1_4_RGBA; - break; - case GpuCompressedPixelFormat::ASTC_4x4_RGBA: - transcodeTargetFormat_ = KTX_TTF_ASTC_4x4_RGBA; - break; - case GpuCompressedPixelFormat::PVRTC2_4_RGB: - transcodeTargetFormat_ = KTX_TTF_PVRTC2_4_RGB; - break; - case GpuCompressedPixelFormat::PVRTC2_4_RGBA: - transcodeTargetFormat_ = KTX_TTF_PVRTC2_4_RGBA; - break; - case GpuCompressedPixelFormat::ETC2_EAC_R11: - transcodeTargetFormat_ = KTX_TTF_ETC2_EAC_R11; - break; - case GpuCompressedPixelFormat::ETC2_EAC_RG11: - transcodeTargetFormat_ = KTX_TTF_ETC2_EAC_RG11; - break; - // case NONE: - default: - transcodeTargetFormat_ = KTX_TTF_RGBA32; - break; - }; - - errorCode = - ktxTexture2_TranscodeBasis(pTexture, transcodeTargetFormat_, 0); - if (errorCode != KTX_SUCCESS) { - transcodeTargetFormat_ = KTX_TTF_RGBA32; - transcodeTargetFormat = GpuCompressedPixelFormat::NONE; - errorCode = - ktxTexture2_TranscodeBasis(pTexture, transcodeTargetFormat_, 0); - } - if (errorCode == KTX_SUCCESS) { - image.compressedPixelFormat = transcodeTargetFormat; - image.width = static_cast(pTexture->baseWidth); - image.height = static_cast(pTexture->baseHeight); - - if (transcodeTargetFormat == GpuCompressedPixelFormat::NONE) { - // We fully decompressed the texture in this case. - image.bytesPerChannel = 1; - image.channels = 4; - } - - // In the KTX2 spec, there's a distinction between "this image has no - // mipmaps, so they should be generated at runtime" and "this - // image has no mipmaps because it makes no sense to create a mipmap - // for this type of image." It is, confusingly, encoded in the - // `levelCount` property: - // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_levelcount - // - // With `levelCount=0`, mipmaps should be generated. With - // `levelCount=1`, mipmaps make no sense. So when `levelCount=0`, we - // want to leave the `mipPositions` array _empty_. With - // `levelCount=1`, we want to populate it with a single mip level. - // - // However, this `levelCount` property is not directly exposed by the - // KTX2 loader API we're using here. Instead, there is a `numLevels` - // property, but it will _never_ have the value 0, because it - // represents the number of levels of actual pixel data we have. When - // the API sees `levelCount=0`, it will assign the value 1 to - // `numLevels`, but it will _also_ set `generateMipmaps` to true. - // - // The API docs say that `numLevels` will always be 1 when - // `generateMipmaps` is true. - // - // So, in summary, when `generateMipmaps=false`, we populate - // `mipPositions` with whatever mip levels the KTX provides and we - // don't generate any more. When it's true, we treat all the image - // data as belonging to a single base-level image and generate mipmaps - // from that if necessary. - if (!pTexture->generateMipmaps) { - // Copy over the positions of each mip within the buffer. - image.mipPositions.resize(pTexture->numLevels); - for (ktx_uint32_t level = 0; level < pTexture->numLevels; ++level) { - ktx_size_t imageOffset; - ktxTexture_GetImageOffset( - ktxTexture(pTexture), - level, - 0, - 0, - &imageOffset); - ktx_size_t imageSize = - ktxTexture_GetImageSize(ktxTexture(pTexture), level); - - image.mipPositions[level] = {imageOffset, imageSize}; - } - } else { - CESIUM_ASSERT(pTexture->numLevels == 1); - } - - // Copy over the entire buffer, including all mips. - ktx_uint8_t* pixelData = ktxTexture_GetData(ktxTexture(pTexture)); - ktx_size_t pixelDataSize = - ktxTexture_GetDataSize(ktxTexture(pTexture)); - - image.pixelData.resize(pixelDataSize); - std::uint8_t* u8Pointer = - reinterpret_cast(image.pixelData.data()); - std::copy(pixelData, pixelData + pixelDataSize, u8Pointer); - - ktxTexture_Destroy(ktxTexture(pTexture)); - - return result; - } - } - } - - result.image.reset(); - result.errors.emplace_back( - "KTX2 loading failed with error: " + - std::string(ktxErrorString(errorCode))); - - return result; - } else if (isWebP(data)) { - if (WebPGetInfo( - reinterpret_cast(data.data()), - data.size(), - &image.width, - &image.height)) { - image.channels = 4; - image.bytesPerChannel = 1; - uint8_t* pImage = NULL; - const auto bufferSize = image.width * image.height * image.channels; - image.pixelData.resize(static_cast(bufferSize)); - pImage = WebPDecodeRGBAInto( - reinterpret_cast(data.data()), - data.size(), - reinterpret_cast(image.pixelData.data()), - image.pixelData.size(), - image.width * image.channels); - if (!pImage) { - result.image.reset(); - result.errors.emplace_back("Unable to decode WebP"); - } - return result; - } - } - - { - tjhandle tjInstance = tjInitDecompress(); - int inSubsamp, inColorspace; - if (!tjDecompressHeader3( - tjInstance, - reinterpret_cast(data.data()), - static_cast(data.size()), - &image.width, - &image.height, - &inSubsamp, - &inColorspace)) { - CESIUM_TRACE("Decode JPG"); - image.bytesPerChannel = 1; - image.channels = 4; - const auto lastByte = - image.width * image.height * image.channels * image.bytesPerChannel; - image.pixelData.resize(static_cast(lastByte)); - if (tjDecompress2( - tjInstance, - reinterpret_cast(data.data()), - static_cast(data.size()), - reinterpret_cast(image.pixelData.data()), - image.width, - 0, - image.height, - TJPF_RGBA, - 0)) { - result.errors.emplace_back("Unable to decode JPEG"); - result.image.reset(); - } - } else { - CESIUM_TRACE("Decode PNG"); - image.bytesPerChannel = 1; - image.channels = 4; - - int channelsInFile; - stbi_uc* pImage = stbi_load_from_memory( - reinterpret_cast(data.data()), - static_cast(data.size()), - &image.width, - &image.height, - &channelsInFile, - image.channels); - if (pImage) { - CESIUM_TRACE( - "copy image " + std::to_string(image.width) + "x" + - std::to_string(image.height) + "x" + - std::to_string(image.channels) + "x" + - std::to_string(image.bytesPerChannel)); - // std::uint8_t is not implicitly convertible to std::byte, so we must - // use reinterpret_cast to (safely) force the conversion. - const auto lastByte = - image.width * image.height * image.channels * image.bytesPerChannel; - image.pixelData.resize(static_cast(lastByte)); - std::uint8_t* u8Pointer = - reinterpret_cast(image.pixelData.data()); - std::copy(pImage, pImage + lastByte, u8Pointer); - stbi_image_free(pImage); - } else { - result.image.reset(); - result.errors.emplace_back(stbi_failure_reason()); - } - } - tjDestroy(tjInstance); - } - return result; -} - -/*static*/ -std::optional GltfReader::generateMipMaps(ImageCesium& image) { - if (!image.mipPositions.empty() || - image.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { - // No error message needed, since this is not technically a failure. - return std::nullopt; - } - - if (image.pixelData.empty()) { - return "Unable to generate mipmaps, an empty image was provided."; - } - - CESIUM_TRACE( - "generate mipmaps " + std::to_string(image.width) + "x" + - std::to_string(image.height) + "x" + std::to_string(image.channels) + - "x" + std::to_string(image.bytesPerChannel)); - - int32_t mipWidth = image.width; - int32_t mipHeight = image.height; - int32_t totalPixelCount = mipWidth * mipHeight; - size_t mipCount = 1; - while (mipWidth > 1 || mipHeight > 1) { - ++mipCount; - - if (mipWidth > 1) { - mipWidth >>= 1; - } - - if (mipHeight > 1) { - mipHeight >>= 1; - } - - // Total pixels in the final mipmap. - totalPixelCount += mipWidth * mipHeight; - } - - // Byte size of the base image. - const size_t imageByteSize = static_cast( - image.width * image.height * image.channels * image.bytesPerChannel); - - image.mipPositions.resize(mipCount); - image.mipPositions[0].byteOffset = 0; - image.mipPositions[0].byteSize = imageByteSize; - - image.pixelData.resize(static_cast( - totalPixelCount * image.channels * image.bytesPerChannel)); - - mipWidth = image.width; - mipHeight = image.height; - size_t mipIndex = 0; - size_t byteOffset = 0; - size_t byteSize = imageByteSize; - while (mipWidth > 1 || mipHeight > 1) { - size_t lastByteOffset = byteOffset; - byteOffset += byteSize; - ++mipIndex; - - int32_t lastWidth = mipWidth; - if (mipWidth > 1) { - mipWidth >>= 1; - } - - int32_t lastHeight = mipHeight; - if (mipHeight > 1) { - mipHeight >>= 1; - } - - byteSize = static_cast( - mipWidth * mipHeight * image.channels * image.bytesPerChannel); - - image.mipPositions[mipIndex].byteOffset = byteOffset; - image.mipPositions[mipIndex].byteSize = byteSize; - - if (!stbir_resize_uint8( - reinterpret_cast( - &image.pixelData[lastByteOffset]), - lastWidth, - lastHeight, - 0, - reinterpret_cast(&image.pixelData[byteOffset]), - mipWidth, - mipHeight, - 0, - image.channels)) { - // Remove any added mipmaps. - image.mipPositions.clear(); - image.pixelData.resize(imageByteSize); - return stbi_failure_reason(); - } - } - - return std::nullopt; -} diff --git a/CesiumGltfReader/src/ImageDecoder.cpp b/CesiumGltfReader/src/ImageDecoder.cpp new file mode 100644 index 000000000..73cbf9fa3 --- /dev/null +++ b/CesiumGltfReader/src/ImageDecoder.cpp @@ -0,0 +1,461 @@ +#include "CesiumGltfReader/ImageDecoder.h" + +#include "ModelJsonHandler.h" +#include "applyKhrTextureTransform.h" +#include "decodeDataUrls.h" +#include "decodeDraco.h" +#include "decodeMeshOpt.h" +#include "dequantizeMeshData.h" +#include "registerReaderExtensions.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define STBI_FAILURE_USERMSG + +namespace Cesium { +// Use STB resize in our own namespace to avoid conflicts from other libs +#define STBIRDEF +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include +#undef STBIRDEF +}; // namespace Cesium + +#define STB_IMAGE_STATIC +#define STB_IMAGE_IMPLEMENTATION +#include +#include + +namespace CesiumGltfReader { + +using namespace CesiumGltf; +using namespace Cesium; + +namespace { + +bool isKtx(const gsl::span& data) { + const size_t ktxMagicByteLength = 12; + if (data.size() < ktxMagicByteLength) { + return false; + } + + const uint8_t ktxMagic[ktxMagicByteLength] = + {0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A}; + + return memcmp(data.data(), ktxMagic, ktxMagicByteLength) == 0; +} + +bool isWebP(const gsl::span& data) { + if (data.size() < 12) { + return false; + } + const uint32_t magic1 = *reinterpret_cast(data.data()); + const uint32_t magic2 = *reinterpret_cast(data.data() + 8); + return magic1 == 0x46464952 && magic2 == 0x50424557; +} + +} // namespace + +/*static*/ +ImageReaderResult ImageDecoder::readImage( + const gsl::span& data, + const Ktx2TranscodeTargets& ktx2TranscodeTargets) { + CESIUM_TRACE("CesiumGltfReader::readImage"); + + ImageReaderResult result; + + result.image.emplace(); + CesiumGltf::ImageCesium& image = result.image.value(); + + if (isKtx(data)) { + ktxTexture2* pTexture = nullptr; + KTX_error_code errorCode; + + errorCode = ktxTexture2_CreateFromMemory( + reinterpret_cast(data.data()), + data.size(), + KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, + &pTexture); + + if (errorCode == KTX_SUCCESS) { + if (ktxTexture2_NeedsTranscoding(pTexture)) { + + CESIUM_TRACE("Transcode KTXv2"); + + image.channels = + static_cast(ktxTexture2_GetNumComponents(pTexture)); + GpuCompressedPixelFormat transcodeTargetFormat = + GpuCompressedPixelFormat::NONE; + + if (pTexture->supercompressionScheme == KTX_SS_BASIS_LZ) { + switch (image.channels) { + case 1: + transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_R; + break; + case 2: + transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_RG; + break; + case 3: + transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_RGB; + break; + // case 4: + default: + transcodeTargetFormat = ktx2TranscodeTargets.ETC1S_RGBA; + } + } else { + switch (image.channels) { + case 1: + transcodeTargetFormat = ktx2TranscodeTargets.UASTC_R; + break; + case 2: + transcodeTargetFormat = ktx2TranscodeTargets.UASTC_RG; + break; + case 3: + transcodeTargetFormat = ktx2TranscodeTargets.UASTC_RGB; + break; + // case 4: + default: + transcodeTargetFormat = ktx2TranscodeTargets.UASTC_RGBA; + } + } + + ktx_transcode_fmt_e transcodeTargetFormat_ = KTX_TTF_RGBA32; + switch (transcodeTargetFormat) { + case GpuCompressedPixelFormat::ETC1_RGB: + transcodeTargetFormat_ = KTX_TTF_ETC1_RGB; + break; + case GpuCompressedPixelFormat::ETC2_RGBA: + transcodeTargetFormat_ = KTX_TTF_ETC2_RGBA; + break; + case GpuCompressedPixelFormat::BC1_RGB: + transcodeTargetFormat_ = KTX_TTF_BC1_RGB; + break; + case GpuCompressedPixelFormat::BC3_RGBA: + transcodeTargetFormat_ = KTX_TTF_BC3_RGBA; + break; + case GpuCompressedPixelFormat::BC4_R: + transcodeTargetFormat_ = KTX_TTF_BC4_R; + break; + case GpuCompressedPixelFormat::BC5_RG: + transcodeTargetFormat_ = KTX_TTF_BC5_RG; + break; + case GpuCompressedPixelFormat::BC7_RGBA: + transcodeTargetFormat_ = KTX_TTF_BC7_RGBA; + break; + case GpuCompressedPixelFormat::PVRTC1_4_RGB: + transcodeTargetFormat_ = KTX_TTF_PVRTC1_4_RGB; + break; + case GpuCompressedPixelFormat::PVRTC1_4_RGBA: + transcodeTargetFormat_ = KTX_TTF_PVRTC1_4_RGBA; + break; + case GpuCompressedPixelFormat::ASTC_4x4_RGBA: + transcodeTargetFormat_ = KTX_TTF_ASTC_4x4_RGBA; + break; + case GpuCompressedPixelFormat::PVRTC2_4_RGB: + transcodeTargetFormat_ = KTX_TTF_PVRTC2_4_RGB; + break; + case GpuCompressedPixelFormat::PVRTC2_4_RGBA: + transcodeTargetFormat_ = KTX_TTF_PVRTC2_4_RGBA; + break; + case GpuCompressedPixelFormat::ETC2_EAC_R11: + transcodeTargetFormat_ = KTX_TTF_ETC2_EAC_R11; + break; + case GpuCompressedPixelFormat::ETC2_EAC_RG11: + transcodeTargetFormat_ = KTX_TTF_ETC2_EAC_RG11; + break; + // case NONE: + default: + transcodeTargetFormat_ = KTX_TTF_RGBA32; + break; + }; + + errorCode = + ktxTexture2_TranscodeBasis(pTexture, transcodeTargetFormat_, 0); + if (errorCode != KTX_SUCCESS) { + transcodeTargetFormat_ = KTX_TTF_RGBA32; + transcodeTargetFormat = GpuCompressedPixelFormat::NONE; + errorCode = + ktxTexture2_TranscodeBasis(pTexture, transcodeTargetFormat_, 0); + } + if (errorCode == KTX_SUCCESS) { + image.compressedPixelFormat = transcodeTargetFormat; + image.width = static_cast(pTexture->baseWidth); + image.height = static_cast(pTexture->baseHeight); + + if (transcodeTargetFormat == GpuCompressedPixelFormat::NONE) { + // We fully decompressed the texture in this case. + image.bytesPerChannel = 1; + image.channels = 4; + } + + // In the KTX2 spec, there's a distinction between "this image has no + // mipmaps, so they should be generated at runtime" and "this + // image has no mipmaps because it makes no sense to create a mipmap + // for this type of image." It is, confusingly, encoded in the + // `levelCount` property: + // https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_levelcount + // + // With `levelCount=0`, mipmaps should be generated. With + // `levelCount=1`, mipmaps make no sense. So when `levelCount=0`, we + // want to leave the `mipPositions` array _empty_. With + // `levelCount=1`, we want to populate it with a single mip level. + // + // However, this `levelCount` property is not directly exposed by the + // KTX2 loader API we're using here. Instead, there is a `numLevels` + // property, but it will _never_ have the value 0, because it + // represents the number of levels of actual pixel data we have. When + // the API sees `levelCount=0`, it will assign the value 1 to + // `numLevels`, but it will _also_ set `generateMipmaps` to true. + // + // The API docs say that `numLevels` will always be 1 when + // `generateMipmaps` is true. + // + // So, in summary, when `generateMipmaps=false`, we populate + // `mipPositions` with whatever mip levels the KTX provides and we + // don't generate any more. When it's true, we treat all the image + // data as belonging to a single base-level image and generate mipmaps + // from that if necessary. + if (!pTexture->generateMipmaps) { + // Copy over the positions of each mip within the buffer. + image.mipPositions.resize(pTexture->numLevels); + for (ktx_uint32_t level = 0; level < pTexture->numLevels; ++level) { + ktx_size_t imageOffset; + ktxTexture_GetImageOffset( + ktxTexture(pTexture), + level, + 0, + 0, + &imageOffset); + ktx_size_t imageSize = + ktxTexture_GetImageSize(ktxTexture(pTexture), level); + + image.mipPositions[level] = {imageOffset, imageSize}; + } + } else { + CESIUM_ASSERT(pTexture->numLevels == 1); + } + + // Copy over the entire buffer, including all mips. + ktx_uint8_t* pixelData = ktxTexture_GetData(ktxTexture(pTexture)); + ktx_size_t pixelDataSize = + ktxTexture_GetDataSize(ktxTexture(pTexture)); + + image.pixelData.resize(pixelDataSize); + std::uint8_t* u8Pointer = + reinterpret_cast(image.pixelData.data()); + std::copy(pixelData, pixelData + pixelDataSize, u8Pointer); + + ktxTexture_Destroy(ktxTexture(pTexture)); + + return result; + } + } + } + + result.image.reset(); + result.errors.emplace_back( + "KTX2 loading failed with error: " + + std::string(ktxErrorString(errorCode))); + + return result; + } else if (isWebP(data)) { + if (WebPGetInfo( + reinterpret_cast(data.data()), + data.size(), + &image.width, + &image.height)) { + image.channels = 4; + image.bytesPerChannel = 1; + uint8_t* pImage = NULL; + const auto bufferSize = image.width * image.height * image.channels; + image.pixelData.resize(static_cast(bufferSize)); + pImage = WebPDecodeRGBAInto( + reinterpret_cast(data.data()), + data.size(), + reinterpret_cast(image.pixelData.data()), + image.pixelData.size(), + image.width * image.channels); + if (!pImage) { + result.image.reset(); + result.errors.emplace_back("Unable to decode WebP"); + } + return result; + } + } + + { + tjhandle tjInstance = tjInitDecompress(); + int inSubsamp, inColorspace; + if (!tjDecompressHeader3( + tjInstance, + reinterpret_cast(data.data()), + static_cast(data.size()), + &image.width, + &image.height, + &inSubsamp, + &inColorspace)) { + CESIUM_TRACE("Decode JPG"); + image.bytesPerChannel = 1; + image.channels = 4; + const auto lastByte = + image.width * image.height * image.channels * image.bytesPerChannel; + image.pixelData.resize(static_cast(lastByte)); + if (tjDecompress2( + tjInstance, + reinterpret_cast(data.data()), + static_cast(data.size()), + reinterpret_cast(image.pixelData.data()), + image.width, + 0, + image.height, + TJPF_RGBA, + 0)) { + result.errors.emplace_back("Unable to decode JPEG"); + result.image.reset(); + } + } else { + CESIUM_TRACE("Decode PNG"); + image.bytesPerChannel = 1; + image.channels = 4; + + int channelsInFile; + stbi_uc* pImage = stbi_load_from_memory( + reinterpret_cast(data.data()), + static_cast(data.size()), + &image.width, + &image.height, + &channelsInFile, + image.channels); + if (pImage) { + CESIUM_TRACE( + "copy image " + std::to_string(image.width) + "x" + + std::to_string(image.height) + "x" + + std::to_string(image.channels) + "x" + + std::to_string(image.bytesPerChannel)); + // std::uint8_t is not implicitly convertible to std::byte, so we must + // use reinterpret_cast to (safely) force the conversion. + const auto lastByte = + image.width * image.height * image.channels * image.bytesPerChannel; + image.pixelData.resize(static_cast(lastByte)); + std::uint8_t* u8Pointer = + reinterpret_cast(image.pixelData.data()); + std::copy(pImage, pImage + lastByte, u8Pointer); + stbi_image_free(pImage); + } else { + result.image.reset(); + result.errors.emplace_back(stbi_failure_reason()); + } + } + tjDestroy(tjInstance); + } + return result; +} + +/*static*/ +std::optional ImageDecoder::generateMipMaps(ImageCesium& image) { + if (!image.mipPositions.empty() || + image.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { + // No error message needed, since this is not technically a failure. + return std::nullopt; + } + + if (image.pixelData.empty()) { + return "Unable to generate mipmaps, an empty image was provided."; + } + + CESIUM_TRACE( + "generate mipmaps " + std::to_string(image.width) + "x" + + std::to_string(image.height) + "x" + std::to_string(image.channels) + + "x" + std::to_string(image.bytesPerChannel)); + + int32_t mipWidth = image.width; + int32_t mipHeight = image.height; + int32_t totalPixelCount = mipWidth * mipHeight; + size_t mipCount = 1; + while (mipWidth > 1 || mipHeight > 1) { + ++mipCount; + + if (mipWidth > 1) { + mipWidth >>= 1; + } + + if (mipHeight > 1) { + mipHeight >>= 1; + } + + // Total pixels in the final mipmap. + totalPixelCount += mipWidth * mipHeight; + } + + // Byte size of the base image. + const size_t imageByteSize = static_cast( + image.width * image.height * image.channels * image.bytesPerChannel); + + image.mipPositions.resize(mipCount); + image.mipPositions[0].byteOffset = 0; + image.mipPositions[0].byteSize = imageByteSize; + + image.pixelData.resize(static_cast( + totalPixelCount * image.channels * image.bytesPerChannel)); + + mipWidth = image.width; + mipHeight = image.height; + size_t mipIndex = 0; + size_t byteOffset = 0; + size_t byteSize = imageByteSize; + while (mipWidth > 1 || mipHeight > 1) { + size_t lastByteOffset = byteOffset; + byteOffset += byteSize; + ++mipIndex; + + int32_t lastWidth = mipWidth; + if (mipWidth > 1) { + mipWidth >>= 1; + } + + int32_t lastHeight = mipHeight; + if (mipHeight > 1) { + mipHeight >>= 1; + } + + byteSize = static_cast( + mipWidth * mipHeight * image.channels * image.bytesPerChannel); + + image.mipPositions[mipIndex].byteOffset = byteOffset; + image.mipPositions[mipIndex].byteSize = byteSize; + + if (!stbir_resize_uint8( + reinterpret_cast( + &image.pixelData[lastByteOffset]), + lastWidth, + lastHeight, + 0, + reinterpret_cast(&image.pixelData[byteOffset]), + mipWidth, + mipHeight, + 0, + image.channels)) { + // Remove any added mipmaps. + image.mipPositions.clear(); + image.pixelData.resize(imageByteSize); + return stbi_failure_reason(); + } + } + + return std::nullopt; +} + +} // namespace CesiumGltfReader diff --git a/CesiumGltfReader/test/TestGltfReader.cpp b/CesiumGltfReader/test/TestGltfReader.cpp index 746377be7..f5d9e55d3 100644 --- a/CesiumGltfReader/test/TestGltfReader.cpp +++ b/CesiumGltfReader/test/TestGltfReader.cpp @@ -703,7 +703,7 @@ TEST_CASE("Decodes images with data uris") { REQUIRE(model.images.size() == 1); - const ImageCesium& image = model.images.front().cesium; + const ImageCesium& image = *model.images.front().cesium; CHECK(image.width == 256); CHECK(image.height == 256); @@ -795,9 +795,9 @@ TEST_CASE("GltfReader::loadGltf") { REQUIRE(result.model->images.size() == 1); const CesiumGltf::Image& image = result.model->images[0]; - CHECK(image.cesium.width == 2048); - CHECK(image.cesium.height == 2048); - CHECK(image.cesium.pixelData.size() == 2048 * 2048 * 4); + CHECK(image.cesium->width == 2048); + CHECK(image.cesium->height == 2048); + CHECK(image.cesium->pixelData.size() == 2048 * 2048 * 4); CHECK(!result.model->buffers.empty()); for (const CesiumGltf::Buffer& buffer : result.model->buffers) { diff --git a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp index 327c26a69..9d889be99 100644 --- a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp +++ b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp @@ -1072,13 +1072,13 @@ static std::vector generateNormals( const size_t waterMaskImageId = model.images.size(); model.images.emplace_back(); CesiumGltf::Image& waterMaskImage = model.images[waterMaskImageId]; - waterMaskImage.cesium.width = 256; - waterMaskImage.cesium.height = 256; - waterMaskImage.cesium.channels = 1; - waterMaskImage.cesium.bytesPerChannel = 1; - waterMaskImage.cesium.pixelData.resize(65536); + waterMaskImage.cesium->width = 256; + waterMaskImage.cesium->height = 256; + waterMaskImage.cesium->channels = 1; + waterMaskImage.cesium->bytesPerChannel = 1; + waterMaskImage.cesium->pixelData.resize(65536); std::memcpy( - waterMaskImage.cesium.pixelData.data(), + waterMaskImage.cesium->pixelData.data(), meshView->waterMaskBuffer.data(), 65536); diff --git a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp index c442dbfa0..d0664d233 100644 --- a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp +++ b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp @@ -266,7 +266,7 @@ TEST_CASE("Add raster overlay to glTF") { const Model& gltfBack = *resultBack.model; REQUIRE(gltfBack.images.size() == 1); - CHECK(!gltfBack.images[0].cesium.pixelData.empty()); + CHECK(!gltfBack.images[0].cesium->pixelData.empty()); REQUIRE(!gltfBack.meshes.empty()); REQUIRE(!gltfBack.meshes[0].primitives.empty()); From a0954e3fbcece44e316479df53e87877a5f3f168 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 23 Jul 2024 14:03:06 -0400 Subject: [PATCH 02/81] Add test for shared images --- .../src/TileContentLoadInfo.cpp | 3 + .../src/TileContentLoadInfo.h | 3 + .../src/TilesetContentManager.cpp | 17 + .../src/TilesetContentManager.h | 7 + .../src/TilesetJsonLoader.cpp | 1 + .../src/TilesetJsonLoader.h | 2 + .../test/TestTilesetContentManager.cpp | 63 + .../test/TestTilesetJsonLoader.cpp | 139 +- .../test/TestTilesetJsonLoader.h | 28 + .../test/data/SharedImages/_Sandcastle.js | 33 + .../test/data/SharedImages/plane-0-0.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-0-1.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-0-2.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-0-3.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-0-4.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-0-5.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-0-6.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-0-7.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-0-8.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-0-9.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-1-0.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-1-1.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-1-2.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-1-3.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-1-4.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-1-5.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-1-6.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-1-7.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-1-8.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-1-9.glb | Bin 0 -> 1504 bytes .../test/data/SharedImages/plane-2-0.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-2-1.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-2-2.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-2-3.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-2-4.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-2-5.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-2-6.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-2-7.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-2-8.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-2-9.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-0.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-1.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-2.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-3.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-3-4.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-5.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-6.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-3-7.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-3-8.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-3-9.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-4-0.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-4-1.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-4-2.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-4-3.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-4-4.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-4-5.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-4-6.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-4-7.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-4-8.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-4-9.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-5-0.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-5-1.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-5-2.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-5-3.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-5-4.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-5-5.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-5-6.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-5-7.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-5-8.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-5-9.glb | Bin 0 -> 1504 bytes .../test/data/SharedImages/plane-6-0.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-6-1.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-6-2.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-6-3.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-6-4.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-6-5.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-6-6.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-6-7.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-6-8.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-6-9.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-7-0.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-7-1.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-7-2.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-7-3.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-7-4.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-7-5.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-7-6.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-7-7.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-7-8.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-7-9.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-8-0.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-8-1.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-8-2.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-8-3.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-8-4.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-8-5.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-8-6.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-8-7.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-8-8.glb | Bin 0 -> 1500 bytes .../test/data/SharedImages/plane-8-9.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-9-0.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-9-1.glb | Bin 0 -> 1504 bytes .../test/data/SharedImages/plane-9-2.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-9-3.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-9-4.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-9-5.glb | Bin 0 -> 1504 bytes .../test/data/SharedImages/plane-9-6.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-9-7.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/plane-9-8.glb | Bin 0 -> 1508 bytes .../test/data/SharedImages/plane-9-9.glb | Bin 0 -> 1512 bytes .../test/data/SharedImages/texture0.png | Bin 0 -> 58902 bytes .../test/data/SharedImages/texture1.png | Bin 0 -> 57706 bytes .../test/data/SharedImages/tileset.json | 2228 +++++++++++++++++ .../include/CesiumGltf/SharedAssetDepot.h | 21 +- .../include/CesiumGltfReader/GltfReader.h | 4 +- CesiumGltfReader/src/GltfReader.cpp | 2 +- 116 files changed, 2475 insertions(+), 76 deletions(-) create mode 100644 Cesium3DTilesSelection/test/TestTilesetJsonLoader.h create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/_Sandcastle.js create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-0-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-1-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-2-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-3-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-4-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-5-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-6-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-7-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-8-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-0.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-1.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-2.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-3.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-4.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-5.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-6.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-7.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-8.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/plane-9-9.glb create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/texture0.png create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/texture1.png create mode 100644 Cesium3DTilesSelection/test/data/SharedImages/tileset.json diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp index b492e134c..31796498f 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp @@ -9,6 +9,8 @@ TileContentLoadInfo::TileContentLoadInfo( const std::shared_ptr& pPrepareRendererResources_, const std::shared_ptr& pLogger_, + const std::optional> + maybeAssetDepot_, const TilesetContentOptions& contentOptions_, const Tile& tile) : asyncSystem(asyncSystem_), @@ -18,6 +20,7 @@ TileContentLoadInfo::TileContentLoadInfo( tileID(tile.getTileID()), tileBoundingVolume(tile.getBoundingVolume()), tileContentBoundingVolume(tile.getContentBoundingVolume()), + maybeAssetDepot{maybeAssetDepot_}, tileRefine(tile.getRefine()), tileGeometricError(tile.getGeometricError()), tileTransform(tile.getTransform()), diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.h b/Cesium3DTilesSelection/src/TileContentLoadInfo.h index 85ee52187..0245cc033 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.h +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.h @@ -24,6 +24,8 @@ struct TileContentLoadInfo { const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, + const std::optional> + maybeAssetDepot, const TilesetContentOptions& contentOptions, const Tile& tile); @@ -40,6 +42,7 @@ struct TileContentLoadInfo { BoundingVolume tileBoundingVolume; std::optional tileContentBoundingVolume; + std::optional> maybeAssetDepot; TileRefine tileRefine; diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index d46908c5d..861193dcf 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -53,6 +53,11 @@ struct ContentKindSetter { // size now. We'll be adding this number to our total memory usage soon, // and remove it when the tile is later unloaded, and we must use // the same size in each case. + + // TODO: this number will be wrong once we deduplicate images! it'll log + // the image's size for each time it's used, instead of only once as it + // should. if you are reading this comment in a code review, i forgot to + // fix this and you should remind me. if (image.cesium->sizeBytes < 0) { image.cesium->sizeBytes = int64_t(image.cesium->pixelData.size()); } @@ -562,6 +567,9 @@ postProcessContentInWorkerThread( tileLoadInfo.contentOptions.ktx2TranscodeTargets; gltfOptions.applyTextureTransform = tileLoadInfo.contentOptions.applyTextureTransform; + if (tileLoadInfo.maybeAssetDepot.has_value()) { + gltfOptions.sharedAssets = *tileLoadInfo.maybeAssetDepot; + } auto asyncSystem = tileLoadInfo.asyncSystem; auto pAssetAccessor = tileLoadInfo.pAssetAccessor; @@ -655,6 +663,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, + _assetDepot(std::make_shared()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -684,6 +693,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, + _assetDepot(std::make_shared()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -835,6 +845,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, + _assetDepot(std::make_shared()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -980,6 +991,7 @@ void TilesetContentManager::loadTileContent( this->_externals.pAssetAccessor, this->_externals.pPrepareRendererResources, this->_externals.pLogger, + std::optional(this->_assetDepot), tilesetOptions.contentOptions, tile}; @@ -1209,6 +1221,11 @@ TilesetContentManager::getTilesetCredits() const noexcept { return this->_tilesetCredits; } +const std::shared_ptr& +TilesetContentManager::getSharedAssetDepot() const noexcept { + return this->_assetDepot; +} + int32_t TilesetContentManager::getNumberOfTilesLoading() const noexcept { return this->_tileLoadsInProgress; } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index d750b2207..fb03571de 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -94,6 +95,9 @@ class TilesetContentManager const std::vector& getTilesetCredits() const noexcept; + const std::shared_ptr& + getSharedAssetDepot() const noexcept; + int32_t getNumberOfTilesLoading() const noexcept; int32_t getNumberOfTilesLoaded() const noexcept; @@ -146,6 +150,9 @@ class TilesetContentManager int32_t _loadedTilesCount; int64_t _tilesDataUsed; + // Stores assets that might be shared between tiles. + std::shared_ptr _assetDepot; + CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp index f557d79f8..26d217ef8 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp @@ -1004,4 +1004,5 @@ void TilesetJsonLoader::addChildLoader( std::unique_ptr pLoader) { this->_children.emplace_back(std::move(pLoader)); } + } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index 13d21cb94..c9a673dc1 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -55,6 +56,7 @@ class TilesetJsonLoader : public TilesetContentLoader { private: std::string _baseUrl; CesiumGeospatial::Ellipsoid _ellipsoid; + std::optional> _maybeAssetDepot; /** * @brief The axis that was declared as the "up-axis" for glTF content. diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index f476922f8..90e822c20 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1,5 +1,7 @@ #include "SimplePrepareRendererResource.h" +#include "TestTilesetJsonLoader.h" #include "TilesetContentManager.h" +#include "TilesetJsonLoader.h" #include #include @@ -1235,4 +1237,65 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { pManager->unloadTileContent(tile); } + + SECTION("Resolve external images, with deduplication") { + std::filesystem::path dirPath(testDataPath / "SharedImages"); + + // mock the requests for all files + for (const auto& entry : std::filesystem::directory_iterator(dirPath)) { + pMockedAssetAccessor->mockCompletedRequests.insert( + {entry.path().filename().string(), createMockRequest(entry.path())}); + } + + std::filesystem::path tilesetPath(dirPath / "tileset.json"); + auto pExternals = createMockJsonTilesetExternals( + tilesetPath.string(), + pMockedAssetAccessor); + + auto pJsonLoaderFuture = + TilesetJsonLoader::createLoader(pExternals, tilesetPath.string(), {}); + + externals.asyncSystem.dispatchMainThreadTasks(); + + auto loaderResult = pJsonLoaderFuture.wait(); + + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + + auto& rootTile = *loaderResult.pRootTile; + auto& containerTile = rootTile.getChildren()[0]; + + REQUIRE(containerTile.getChildren().size() == 100); + + // create manager + Tile::LoadedLinkedList loadedTiles; + IntrusivePointer pManager = + new TilesetContentManager{ + externals, + {}, + RasterOverlayCollection{loadedTiles, externals}, + {}, + std::move(loaderResult.pLoader), + std::move(loaderResult.pRootTile)}; + + for (auto& child : containerTile.getChildren()) { + pManager->loadTileContent(child, {}); + externals.asyncSystem.dispatchMainThreadTasks(); + pManager->waitUntilIdle(); + + CHECK(child.getState() == TileLoadState::ContentLoaded); + CHECK(child.isRenderContent()); + + const auto& renderContent = child.getContent().getRenderContent(); + const auto& images = renderContent->getModel().images; + CHECK(images.size() == 1); + } + + CHECK(pManager->getSharedAssetDepot()->getImagesCount() == 2); + + // unload the tile content + for (auto& child : containerTile.getChildren()) { + pManager->unloadTileContent(child); + } + } } diff --git a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp index b2dc6e382..ccad9aa18 100644 --- a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp @@ -1,8 +1,11 @@ +#include "TestTilesetJsonLoader.h" + #include "ImplicitQuadtreeLoader.h" #include "SimplePrepareRendererResource.h" #include "TilesetJsonLoader.h" #include +#include #include #include #include @@ -12,6 +15,7 @@ #include #include +#include #include #include @@ -23,52 +27,6 @@ using namespace CesiumUtility; namespace { std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; -TilesetExternals createMockTilesetExternals(const std::string& tilesetPath) { - auto tilesetContent = readFile(tilesetPath); - auto pMockCompletedResponse = std::make_unique( - static_cast(200), - "doesn't matter", - CesiumAsync::HttpHeaders{}, - std::move(tilesetContent)); - - auto pMockCompletedRequest = std::make_shared( - "GET", - "tileset.json", - CesiumAsync::HttpHeaders{}, - std::move(pMockCompletedResponse)); - - std::map> - mockCompletedRequests; - mockCompletedRequests.insert({tilesetPath, std::move(pMockCompletedRequest)}); - - std::shared_ptr pMockAssetAccessor = - std::make_shared(std::move(mockCompletedRequests)); - - auto pMockPrepareRendererResource = - std::make_shared(); - - auto pMockCreditSystem = std::make_shared(); - - AsyncSystem asyncSystem{std::make_shared()}; - - return TilesetExternals{ - std::move(pMockAssetAccessor), - std::move(pMockPrepareRendererResource), - std::move(asyncSystem), - std::move(pMockCreditSystem)}; -} - -TilesetContentLoaderResult -createLoader(const std::filesystem::path& tilesetPath) { - std::string tilesetPathStr = tilesetPath.string(); - auto externals = createMockTilesetExternals(tilesetPathStr); - auto loaderResultFuture = - TilesetJsonLoader::createLoader(externals, tilesetPathStr, {}); - externals.asyncSystem.dispatchMainThreadTasks(); - - return loaderResultFuture.wait(); -} - TileLoadResult loadTileContent( const std::filesystem::path& tilePath, TilesetContentLoader& loader, @@ -114,8 +72,8 @@ TEST_CASE("Test creating tileset json loader") { Cesium3DTilesContent::registerAllTileContentTypes(); SECTION("Create valid tileset json with REPLACE refinement") { - auto loaderResult = - createLoader(testDataPath / "ReplaceTileset" / "tileset.json"); + auto loaderResult = createTilesetJsonLoader( + testDataPath / "ReplaceTileset" / "tileset.json"); CHECK(!loaderResult.errors.hasErrors()); @@ -182,7 +140,7 @@ TEST_CASE("Test creating tileset json loader") { SECTION("Create valid tileset json with ADD refinement") { auto loaderResult = - createLoader(testDataPath / "AddTileset" / "tileset2.json"); + createTilesetJsonLoader(testDataPath / "AddTileset" / "tileset2.json"); CHECK(!loaderResult.errors.hasErrors()); @@ -229,7 +187,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset has tile with sphere bounding volume") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "SphereBoundingVolumeTileset.json"); @@ -247,7 +205,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset has tile with box bounding volume") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "BoxBoundingVolumeTileset.json"); @@ -266,7 +224,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset has tile with no bounding volume field") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "NoBoundingVolumeTileset.json"); @@ -281,7 +239,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset has tile with no geometric error field") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "NoGeometricErrorTileset.json"); @@ -300,7 +258,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset has tile with no capitalized Refinement field") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "NoCapitalizedRefineTileset.json"); @@ -321,7 +279,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Scale geometric error along with tile transform") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "ScaleGeometricErrorTileset.json"); @@ -340,7 +298,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset with empty tile") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "EmptyTileTileset.json"); CHECK(!loaderResult.errors.hasErrors()); REQUIRE(loaderResult.pRootTile); @@ -357,7 +315,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset with quadtree implicit tile") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "QuadtreeImplicitTileset.json"); CHECK(!loaderResult.errors.hasErrors()); @@ -380,7 +338,7 @@ TEST_CASE("Test creating tileset json loader") { } SECTION("Tileset with octree implicit tile") { - auto loaderResult = createLoader( + auto loaderResult = createTilesetJsonLoader( testDataPath / "MultipleKindsOfTilesets" / "OctreeImplicitTileset.json"); CHECK(!loaderResult.errors.hasErrors()); @@ -404,7 +362,7 @@ TEST_CASE("Test creating tileset json loader") { SECTION("Tileset with metadata") { auto loaderResult = - createLoader(testDataPath / "WithMetadata" / "tileset.json"); + createTilesetJsonLoader(testDataPath / "WithMetadata" / "tileset.json"); CHECK(!loaderResult.errors.hasErrors()); REQUIRE(loaderResult.pLoader); @@ -425,8 +383,8 @@ TEST_CASE("Test loading individual tile of tileset json") { Cesium3DTilesContent::registerAllTileContentTypes(); SECTION("Load tile that has render content") { - auto loaderResult = - createLoader(testDataPath / "ReplaceTileset" / "tileset.json"); + auto loaderResult = createTilesetJsonLoader( + testDataPath / "ReplaceTileset" / "tileset.json"); REQUIRE(loaderResult.pRootTile); REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); @@ -450,7 +408,7 @@ TEST_CASE("Test loading individual tile of tileset json") { SECTION("Load tile that has external content") { auto loaderResult = - createLoader(testDataPath / "AddTileset" / "tileset.json"); + createTilesetJsonLoader(testDataPath / "AddTileset" / "tileset.json"); REQUIRE(loaderResult.pRootTile); REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); @@ -501,8 +459,8 @@ TEST_CASE("Test loading individual tile of tileset json") { } SECTION("Load tile that has external content with implicit tiling") { - auto loaderResult = - createLoader(testDataPath / "ImplicitTileset" / "tileset_1.1.json"); + auto loaderResult = createTilesetJsonLoader( + testDataPath / "ImplicitTileset" / "tileset_1.1.json"); REQUIRE(loaderResult.pRootTile); CHECK(loaderResult.pRootTile->isExternalContent()); @@ -600,8 +558,8 @@ TEST_CASE("Test loading individual tile of tileset json") { } SECTION("Check that tile with legacy implicit tiling extension still works") { - auto loaderResult = - createLoader(testDataPath / "ImplicitTileset" / "tileset_1.0.json"); + auto loaderResult = createTilesetJsonLoader( + testDataPath / "ImplicitTileset" / "tileset_1.0.json"); REQUIRE(loaderResult.pRootTile); CHECK(loaderResult.pRootTile->isExternalContent()); @@ -622,3 +580,52 @@ TEST_CASE("Test loading individual tile of tileset json") { CHECK(pLoader->getAvailableLevels() == 2); } } +Cesium3DTilesSelection::TilesetContentLoaderResult +Cesium3DTilesSelection::createTilesetJsonLoader( + const std::filesystem::path& tilesetPath) { + std::string tilesetPathStr = tilesetPath.string(); + auto pAccessor = std::make_shared( + std::map>()); + auto externals = createMockJsonTilesetExternals(tilesetPathStr, pAccessor); + auto loaderResultFuture = + TilesetJsonLoader::createLoader(externals, tilesetPathStr, {}); + externals.asyncSystem.dispatchMainThreadTasks(); + + return loaderResultFuture.wait(); +} +Cesium3DTilesSelection::TilesetExternals +Cesium3DTilesSelection::createMockJsonTilesetExternals( + const std::string& tilesetPath, + std::shared_ptr& pAssetAccessor) { + auto tilesetContent = readFile(tilesetPath); + auto pMockCompletedResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + std::move(tilesetContent)); + + auto pMockCompletedRequest = + std::make_shared( + "GET", + "tileset.json", + CesiumAsync::HttpHeaders{}, + std::move(pMockCompletedResponse)); + + pAssetAccessor->mockCompletedRequests.insert( + {tilesetPath, std::move(pMockCompletedRequest)}); + + auto pMockPrepareRendererResource = + std::make_shared(); + + auto pMockCreditSystem = std::make_shared(); + + CesiumAsync::AsyncSystem asyncSystem{ + std::make_shared()}; + + return TilesetExternals{ + std::move(pAssetAccessor), + std::move(pMockPrepareRendererResource), + std::move(asyncSystem), + std::move(pMockCreditSystem)}; +} diff --git a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.h b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.h new file mode 100644 index 000000000..ed3a1b34a --- /dev/null +++ b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.h @@ -0,0 +1,28 @@ +#pragma once + +#include "SimplePrepareRendererResource.h" +#include "TilesetJsonLoader.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace Cesium3DTilesSelection { + +Cesium3DTilesSelection::TilesetExternals createMockJsonTilesetExternals( + const std::string& tilesetPath, + std::shared_ptr& pAssetAccessor); + +TilesetContentLoaderResult +createTilesetJsonLoader(const std::filesystem::path& tilesetPath); + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/test/data/SharedImages/_Sandcastle.js b/Cesium3DTilesSelection/test/data/SharedImages/_Sandcastle.js new file mode 100644 index 000000000..2e33de316 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/SharedImages/_Sandcastle.js @@ -0,0 +1,33 @@ +const viewer = new Cesium.Viewer("cesiumContainer", { + globe: false +}); + +// Create the tileset in the viewer +const tileset = viewer.scene.primitives.add( + await Cesium.Cesium3DTileset.fromUrl( + "http://localhost:8003/tileset.json", { + debugShowBoundingVolume: true, + }) +); + +// Move the tileset to a certain position on the globe, +// and scale it up +const transform = Cesium.Transforms.eastNorthUpToFixedFrame( + Cesium.Cartesian3.fromDegrees(-75.152408, 39.946975, 20) +); +const scale = 15.0; +const modelMatrix = Cesium.Matrix4.multiplyByUniformScale( + transform, + scale, + new Cesium.Matrix4() +); +tileset.modelMatrix = modelMatrix; + +// Zoom to the tileset, with a small offset so that +// it is fully visible +const offset = new Cesium.HeadingPitchRange( + Cesium.Math.toRadians(-22.5), + Cesium.Math.toRadians(-22.5), + 60.0 +); +viewer.zoomTo(tileset, offset); \ No newline at end of file diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-0-0.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-0-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..b3ac6b99d5b04e561dd1493cad7d5d7fae3e5415 GIT binary patch literal 1500 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 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-0-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-0-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..b81e52b3c9da06b1f6fc10699d0b4dab12b3084d GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE=A8edjw@#p(R!O4GETZ#2z?bvyGX8-&ANYo#b`9h@j zX{4uw%PnYdsj9R=G06$Xf^p=b+Zbwo9dVXjCqfjZ*XhJmBvoKQDLdcr8pmlzT>_3Z z;U#5IC}bNZ+JeKX8Jfm`P8gFs776k!*OZL=UODWYOMN3F-+>|h0`pk5l;ghZIr~hV zL9M1#E!KT|jJ1D*?KSQDFIdY2qC=;h6Q^w%Kh1DV>d-4rA#H*@iHZ};DDqg=jAo=B zl=^B%swFVPf^dq{8e0VXjtHElG@P<3PI4GW;Vw>#1mCe0%r)}w`=34!A%qpHf|ShYimZ?qfyYGo zPs*eTyx=sWg03483&Ckn2`O&Sy+0exNB#slMn@BWdeCC)ei``w^y60(S^%iQ zV+xOuDu0`sT2St|Th-t>2zACX;8*8@k_}VDRM!yrCaP6ZpQ;u3)`}@A2>Z`^&P_zjP)Mhi8}e IE&lxM7gtxa`2YX_ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-0-6.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-0-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..0927e2f83f5a3700e80fa79ec3b52ec82328e3d3 GIT binary patch literal 1508 zcmb_cZHn745Oufo9znm2_Iw` z?0j!qN)5I(lHR=0%p1k&?E1z=-BB zvKL$Pn8NuB3UFLgbaoW?gTA*Ta4g%DuWi|TDs8K|Q&RVTkOG->E{V@b<=iPLBkTyXcMa7A26?rUc zMl+%tMQ?4(X$j1*Ae`bu_H zduEzi;%L4k(0BG0h-$x$2Ps&@8!I)?qOh&z1bW(T0kqvsL=e!O#iIfM2Z(j`K3bLWy?XwYs)^I;t<_WgvqsaaN=t zY#VN8aMd!wO?+kwL$;@ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-0-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-0-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..42f64e1c345b96d513ebfb5a46ff912d86822503 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$AjzCF^PTTpCraiwSDL2%e5Gmc;JuxBlMTW_K#GF#0=bI~^2;zJ{GO5r$U}Xl|T^$LfMYDcy)SDt5rT7tyI!jp53|8iH zs~=NXe?b9;Yl_Z};(pL~b_AAT>hd)Wb5B*tN_R?X{|{0ilg1_S8L8Brl0u^Xn9OG) zwNE2GC0uSng9}xq4T?xcI2Mc}7u`ls^H#`N+KPq93%A{lsEAA7fKs-<;WdtuwmJkH zYr+f4pisy*Otb}uRWsC$0i7@+StMfQS{+j|?tA61cP{mfjC==%^b5=**-}pDZP(dn z>I`btrE0L9H^*4}H`s37zW;)?OdvXR+BtFBmhsaRN2Ch9w62@Ar~?^AAIS1-t^;F6IuYM z!D9-SkSc$Zn@UjbbhfI&GZ3nbrNFQD1;<&DV4=Epacx|i@^RE)%G*FXTi`TLptI%m zrsM$TL!k?~sw_6PD@a|(YOBUq2v+hyg2AI!>j&JWABGe7zR<4VeF@*==llE1veZAf OClQBxm(~sbeD4>AHMMX6 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-0-8.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-0-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..307a4df5d40b7bc3026355f61227d9f19fc50ddb GIT binary patch literal 1500 zcmb_cTZ-E-6m_R`A3@*7@v9;APf61ZWJuy^k`9FsitH%1iftKL&dd-3T|k%E#q>(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ?_YuS#DLRMz5-O-Uyt7z8mje1k0qZB`ZQ)dY?n!(Im z?(|a%>n|w4aZTawDINxWXHQ@mrY?tRm^_WvLSGU;3rUy#bRb5bbOAB*`y zruKQJ=Y-20tl&a*X^SF~5sn4p$U}D#^t`p^EN#U?a7@t~Ds?OfB1%zO`q@(au(DJjQ&*KrP& zx`0}Bs~W7^J;mC;!FFo-{tMPJf#^7B=PYPj=1)@`kt+3)Qz)AtPom_+H1aHxJ)JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2`K~ED zgo~kYg`8Es8m|?kZeX=l<0}L!c__gUOX~-`p&y15_`cF^V7!L!>F@jd>$Y5fX-^`K L_pZ$w{N>&+N5Hev literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-0-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-0-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..e351c781242929e76e0185d40602d68e0c1697dc GIT binary patch literal 1508 zcmb_c>x!E|6t-LX9$|iq@sfr{3MEamkR^%RBwY$2l+lbjMI9N(be9n51N1TbAU!i` z?%S49L7j7EzVn^yM9JdzM$@#PuQcr~ymxbNx@KTz@wxu%Z% zCqcY0EQnBIz|x#$Bom9LoFMnLWji_wSee0gS4To=(R|P!_h(2)DSiZ_&I(pEgO$13 z>8BLdUr>PInxeC(co+_xJ%MGIx_nK;JWy4#(w&pq|AQ3Bq;W}nK`M3Uq>!jTCi9s{ z?ej>_30FJN;6hbtiz1Q{js@e$MRyU@ycKemwqhaj!fm%BD&o>Npp@;ec!Q&)tquXl zhVX(iC={{{6YaoZ)eLoGKqrhz7Ks?SR>zc#hh91Eoojs~Bj1A|{Q~nywv^N9b)7?| zE}&Ljss`J6bBeWpgYDMs`!86_1fpZ7oinFx89z;NM5@qBP9bfAJc^PN%gD1x){Lg4 z8kFj2TdE~6#e#5(lM0*rd_n|H5*o}{8O0flBXVI*a6twvVf+>cs+!BD(d)k~L33tN zQA-@n)&%Cx+yPN-x3M6Bh&LohS~!c7JjN5YhPg)GegEU9A%w7IrJs;FU6VC(L-3d= z|49+of#;m2RM1T=Vj(#7OCiNAy7%Uz#mJjN$LMJ4%_jXZav_t8;pc(p%|3iFp#^{% zJf?67sq#0usRZRtXQvuG1EI=T3jAtcaGVth7OHEPJ)>vBW2w26mx1)Pz-gX9W6Rr3 z$pMUq0vB>s8Ei~fkh+Z3R*kO^tmJ_NgF~%W>j&JW??+SkzR+&qeF@*w=li?sveZAf OClSYc*VYaGa_<+70JUNO literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-0.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..53f8c61939e17278517655b238cb899b2652d9de GIT binary patch literal 1500 zcmb_c+lt#T5OufoKNdZk#J7glFC|U0kR=;$lXNMBP-I88Rcy=1a=J?h^aJ{d{g{p< zJNIo%X~2#~(wQ@tGfuMk&6QyoKVKP!2hZ&+m~0S^B2rb1SIA#%P*}$?;SZF2L%wCA z@JWy$jw>RR8L_-%1u4Y*sU*mMZ9A@sB32i0+%u6-yJ$A(kNZ<(q8vYhQ+Ej~TENO$ zZp~u~`!6WKaYNzlC?19bcSm4%EK@#K$J$eATkV~aI{$+d$fR>gd`2qwPD!Cqe=O!R znL4MLo)Rv%u!Aerr433*K{ytSBOl!+(DUnOo~LL$hSR9GVZlT|Z+q^( zQfE-RX;q8$-W+3{-(Y)c))t5wzpV!eM7$+A*oC_|D^q;ORxsBnxbJ`bG=vmZtPV3WqbssPehd*4 z6+fxcCh?NfoC>;bOe_SaVJ)P&LHEIIG#>>MP>hKt!St>_Mm|(>KKwigg6W4Z7Dxc7 zAz}(2A=Uo2Hri0(cv~g#0t{WT9Qf6_;JBzVER<>&dK_&%^H7`$=Vb<(mLE15b``;7 zD4bepU#(Y|Sh&)3P#-mTjbJs8Bp71(0pHN~qY1oU7+3JTg!l39`@74w+<)#&A`Z_k Jty}#0*)LCbv+)1` literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..655d287f64c970ee9693547e4a96e00af035ded7 GIT binary patch literal 1500 zcmb_c+lt#T5OufoKZ2f3;!7G@zmzo1LY8d2P12>Xgd#hNtzuh7lG9y6pdZjr?8kH@ z*|~39N&|K@lFppDoNQ5WK~c3@5q?j}HxxKF zik>71S1UoJHe;5TtRRJ)Ka~UpuU*f#QOv3Wj(awedKb+G{c(SaY?R|iaOy8$MGIIt zi>-Z3;r;~$IIbzWJBs_^z~2$LZO2xR({}b$#@2hMq~8A^1v2Ga5ucH&y;D*s)E|ra zOs3vxrl*99E$m>SyR<sB?`?wez*_Zw`lmhZn{T?dE`gZ58?_Ei2f#|bg1SDZrGBzcq-Cr-O864f)B z6Ei60YDaA=V2&l>6lVroM*NmYoMkkgvMNan7)KPqoM1r)EMxo{$GV#jjnM0V)q>{E z+)yhVEtUl4&e;M{?YHru1dDh>((HmexQnwg#kXt;bB)5g{>M*4NMXsUC?hkvBuf-X zUJ)HW5ow)w$!Sg{UDqa-lGCV?%G{v4a5kEc!U+h*Mw4)Q+aIF<3OOHs9)#ia!xsk> z0L&0Dg>R7Vep4D_=xDlIt?vR1Q?MNP^||D@5E+(Qv``%vrqMMc5490EFEfy|`mU+4 zMGS+Xae8HZHC|z0;Y!^=b5!FEf>k_LV2Gvl1HPf}M-zCzu&&^F3Gd_I_ji|Vwg23i NLL8o5S~vLfvtK*tv&H}b literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-2.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..b5047f4619f4d6687c104577a3971d9153b3225b GIT binary patch literal 1500 zcmb_c+lt#T5OufoKNdY3$Cos;ekp01g)G^4o1{x2gd#h#tzuh7lG9y6pdZjr?8kH@ z+qrLBN&|K@lFppDoN|i(`1#5(y71i2g2@KqC?Y~IE|9<2ps-j2YrY*Rh9W$&qKTkoBcy8nX|$dq$Md`7DFPD!Cqe=O!R znYyQ$o)Rv%u!Du}(gr1@ARJ4^k&kW@==pWbS$>^LSqi_?NvKS#(1KQWzT!1bvW~t4 z9BaY_WzZ<(S~l83z*-EAFhB`oQY11(zSFf8<6bL=+PS0~mH7?~<(HTzN>X0;ZO_|R z>I~{Mt!lB|H^*4_H`rby-+#e6HV_>K?VSYes{Cn=6H=#MaSCOV@5&Aep?R;h>MP>hKt!St>_Mm|(>KKwigg6W4ZHb?-d zAz}(2A>IDAHnpL{>29^a3oz=6<-o7cCC7!xu+*wuxRwj!=+rY0#Su6!Gtjj9u&J;` z1e2k0dR6;sy~4!8m8OIBQG>4$tm2UZLo7ew8~T1Uf%gmJ3Z9qnKK^}wciC3^&z&j6 M;n}5ii$6d61yf_Q^#A|> literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..1c796fb9019f726c22d4563ca4cac0c1276d57fb GIT binary patch literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZ4Qp zeWlKzPTi^o+j(=0b$^5H*7E%qtYZVwVbI=5(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aiqFAZ-id|%N8_u zW|~^!Xuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_CT}%ry$`dY?WIAcZBX!jw$uk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&61=Hbd7>q$MCK?BmQSTP{P{`TfOFsxEAHUk5 z0APlIDSU%e_nXpaLq*fsDt+f*=z?Xyug(R>d6{CNL_7B^&z6r1GwJBThYFQAD^d`) z{JP2D%Lv9pq18%zY<$Dm!j-y>`l!Zh1gm%?!4Ofa)%pS7(htKi{Jt=*;C%_d$Itio Tmu0JOM_w$y!B`_5gdtJ|i(`1#5(Ja}$r!DNGQ6cHg97sy|1P*}w=;SZF2L%waI z@JW&&js=n0j96Z>f)sN8R1)OBcHFLsB32b}+%u8XyJ$A(kNZ<(q8vYh)9w;hw1Abp z+?vM}&R-5r5r*`|7I%idGfw%$7>b^iw`kSXVi_>5HTosvSK{#eXs zGIdWgJtbUjVFwG{r433*K{%F-BOl!+(DUn=WF378 zIM##<%AirmwQRJ7fVCJJVSp0Gq)23ne8;mD<6bL=+PS0~mH7?~<(HTzN>W|#ZLhnp z)EU%iTGe8`H^*4_H`rby-+#e6HV_>K-8~7~Rr%8#C!|ij;uOjz$)l_|ajddPRL^Km z>OrZmcGR{4=2#L=aaLo?klzuBvy4VlRwZcx7eU>EM|q!%8Z1gYJXbXg&%ipcoTPg6Um+Y>)s@ zL&Ow5Lc0BJZE8b@<88IT3oz=6<-o7cCC7!xu+*wuc$Np_=+rY0#Su6!Gtjj9u&J;` z1e2k0dR6;sy~4!8m8OIBQG>4$tm2UZLo7ew8~T1Uf%gmJ3Z9qnKK^}wciC3^&z&j6 M;n}5ii$6d61y@hA`2YX_ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..fed618ea974ede9b0bd7c06fca5a1bf5ead9ba57 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|n48k(PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-gv1~_2AuBUD?&?UWT{P?WM!hN0QHr0ysk4L?&0u9N zcls%X^%oT2xTbLT6c2;GvnQ|&Qa7@t~Ds?OfB1%zO`q@(au(DJjQ&*L4n+ zx`0}Bs~W8P_7rRX2HUOW`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE;>dBrqQZK9%@tIG*3X%^1CL( zEwJ_x=5KTkgMf OCK1PH*VYaG^6VEw$g|D> literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-6.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..783d80d0007864368467dff5e4434c185df6c79b GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dA^o*V<9~XwwswW>xRN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%a}wQ&Fd literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..d143bd58e73a56fd7dc7299f2eb7f08e5aee1422 GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|I5F03L!nsff{U81+NB^;CUH!rB$+UoxVwnZ2k2w=L3(GB z_`Pi@4MgY8%(>@&oSQJ7-CP-l@$;2oyo2|4>Ww#u1Od%+E^_3~H^?tmD=O|8eLyZY zk^dyAw_4>?YBS(T##5Tg*;7W5``T_fCJK0w!g0q$QtzT^uRH8ckckrV2u_^^tXK*w zyx5w@6!u?Gfa8jyy`#AA_naMpZDCWr*us0N($;&Yq^0ifm3;pNYhxff4B9yfx~1}G2?=SLdLbB;P0~kM2#T#N4OP!r zLd!uZuQt`T0wzRK!AM+UGhd9TByr4w2`|DZg>ghK%n8Y9kH=ixkU)2HUJJeYS1qXT z%t~s7W9gE@+~F+{Reoy^O0Y=OB=s)1gS#ZoA~NDjm}}(Sbw7RXLkde?_%WTbC0!y{ zih}C!X&zO1XM!b6vUOz=DFyQjsmu+!^QME@z#D^LOf>c;qwWy7P{>*TOV9HrAHQNy z05C(q48B3S`*mqbLr2rzYJI0*lm$zGU!O}s(mW+7fXtZ9H9&6t)ws56tV|i5JO9U@OpuiAOqtW;Q-_j3*G5o$TuHbzMzsJw_ U_m^$8|J<2E9G+cT*ZA|ZUva6mMF0Q* literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-8.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..c94389505158c66c3f81a64625c5672c076d41d8 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6w!Qt6OIMr$V0a=^!z&FEW1vGC`zx>iK$4cz<^eEzTq{F(~i0X z9BaZ$%AirmHcYgIfRz{;VSp0GB#%XcJj*pD<6bL=+PS0~nfVS3ZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&J);NXXw@?h#VK)Cq@Zc}VUuB( zAxwtCsa5T(^$HUUSDFsiM-9G4u!@Hg46*!xZ|H~71m3T-D|lYR`}p_${bgJ3zjP)M Mhi8}8E&lxM7hfl|0RR91 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-1-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-1-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..3f2257e8a1d8191d1c1da07f6faeee83c945defe GIT binary patch literal 1504 zcmb_c>uTFD6!tpy9!0;6<4YDA{~1}9LP{EUNmd3S4B1g^4cju3oVA3&9$*i-N7<2V z=iY6M8Jy@y`p$PQ-*KGIZm$f(`1!^#Zs5I}2ICFFQAC7bTp)kGL1DRC5&lTYH{{zU z3ZEqjRx3fIHY1i5EGM~~Jr@M|Zynb&QN+p|jyoojdKXRmy-{z1OqAg#aOy2!MRQo$ zi>-M~;rs;!IIbz$JBr6a-`f#5mTjunw(LDsW$V3DQulw50-18Ih|fsX-YF>*>W{^I zAyfA>(^JC57Iv`EUD}|StQWnB*wPGrhGPIzTt*>~E_QEeue?h~%+Mkngl@#kkkXp>{6mMrFPOL-{4bl2a&~Bu}#B#I}k&Rz0H` zsRpIG+EUvJm|;mc#c72tLViyqPE#69SQ#fdj3e@4POu<-mNI^eBi+q;BlP-TwV=5( ztEd%@=1T%|XK#V1_S<++fu4@xZ$!S}?~RZTg`5pO_k&>a;foCl z0A>i7!Z%2FzbQ>+=xEwot?wL+s$d!L>vPF*E>bMDXy;w4Ys2HfNIKQrLxBX&iWGFM zK5i;@5y5n5uwGRT8`m(kaHZ~IbyVXk1S@%@z!1<6_>R6Gj^X!}aRu*d_&t8Uzq@R! R{g=)Z;_&R!y1}2H{Q^|?w66dF literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-0.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..582ddad0f62c63992412408c7b1cdbbff783612c GIT binary patch literal 1500 zcmb_c+lt#T5OufoKZ2f(<4YP^zmzo1LY5@nCh1ZLp~#M6tJs#2i!Q>AXCm2@dc^cJ12!g{jr$O zWa^%0dQQ09!44L>OIs9^oNz1|M?SiXq35kNXIU$evJif|9aEW^wVaav)EkdKMPX-cChE8`@GaYR1M2^OT!QpRs_q`SFjgkJxv7BqL} z6}7_Ad_`dH>>Uu*ej5);u!uJ#%`UiuyErWpJZ3AHYZTn~K7JZN3M*EIDVfm~Ss`EY zlIZY>Nb0-`PBSX$rZ%yZoQ9=T<`&%tv*COgOh7Oungr8vZ-jg(W1eu#!g#46(F+z&G^$a02fa#tl3#;eGo1{_eW1_Mbab Nh~u+s>jr;$_6s}Uv&H}b literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..21e6499216d916c745904fbf11f379f5bc5666a5 GIT binary patch literal 1500 zcmb_c+lt#T5OufoKNdY3$Cos;ekp01g)B+DP12>Xgd#h#tzuh7lG9y6pdZjr?8kH@ z+qrLBN&|K@lFppDoNb^iw`kSXVi_>5HTosvSK{#eXs zGIdWgJtbUjVFwG{r433*K{%F-BOl!+(DUn=WF378 zIM##<%AirmwQRJ7fVCJJVSp0Gq)23ne5Y$G#=TY!wR1@~D)Su}$}cfbl%%}w+n%?t z)EU%iTGe8^Z;r9OrZmcGR{4=2#L=aaLo?kWYxjSw^E7tCF;UaYR1M2^M6)GRALkq`Udh3cdMPEokp7 zYHEd}#fre(*;^oL{I(tx5b>7eU>EMhMD!EDkWBOfZc7=9iE!R*5q8zcbK z5HW?1kZyllo7&LfbhldI1sHY3a^TnJlH)>TSZdWmc^uQV+ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-2.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..661b2c5dc047ffa113379cf11fb25a1d2d357646 GIT binary patch literal 1500 zcmb_cTaME(6b*yeN9FG*&7)M1A5=<5>Zomowv3Rf3OT8p7$tFJJ7q>yB^JOESq#@s z()R#DRC4NT``mLM$Bxta?Tuj=KVKO}2gcnjm~0V_A|eFi0{M$A3d{AH@CQo1A>TGp z_#{cNUJD|%8L_NjImzYxsUXOI?YN$aB39KV;Q zby2FLZMCd`8J2`ooL1N(>$~ej4IwBwL2^OT!QpRs_q`SFjgkJxv7Bpw( z6}7_Ad_~~i**hSr{WczyU=eRfnpJQIXK`92c+6ICuTgN{`}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLXc!PBJo6=NuTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&OOVs<>Q*JD?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcHvo`=~`|z@=zOrvmymatM8f$ zTSPDz8mCv4uf{73EL^D@SRK{)3c*SqDKNy+`T^h2_rnRiUl=#=yoC4Z@B6#!w%UL0 OOd*cXuB{vV<=HPoz_ZQ( literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..3651e61c49c03f274df4739630870374af22d247 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&U8AephF#A*6sN>lk%Fe>hfRiE zhA<(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQSc?{tsB?`&YNSb^BZipmhZn{Z45+*LAxhGJ1T#cl89ERmx4jrBz=^npxDZ@NcD`R zv>KG^YFlk9U`iwvj3gB{55$N{k|Zpg@G^=s7)Rv6oREU{dBVjt33WH;jnM0V)q>{E ztfE#pmMtmF9o_;_?YHru1dBvN((HmexJ!~eCL_Ltxkmn7@6+c2q_E^=kkBbx(k1ew zD5(yg7IB?-E?CMWTh}I$Qm~+u%G{tke>$8E{V@o}L}Pz4>fIs_3OO5m>HGfV<5vs{ z0A>i7!8b^EzbQ>+=x91yt?vwss$ePb>vJhcRwP7f(ayV87ptdZI+jxnK2)e6X`X7jlxdMgRZ+ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..b4a800a3be438c4d548546a3a6f9d345029751c6 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7uUwMDIZ6-4ZE6rC{cmaJOO3P z&zlUs2w*-GTCFONjc=G+xKj7AI;!y%f|WdwV2G&I`T^h455o!kzR<4VeF?wE&-eG2 SZMpy4nM53(U0OHz^Rr)y8MSl( literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-8.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..aa370f486fdf1f14b3a66a3d04fb93f0915a7c6d GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXF1kinw+*`*d8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lgptIHh literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-2-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-2-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..e7b47423138bb6cddbaa3d402cd3bf03e0bd438b GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mQ=(T15 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-0.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..b7052e366bde9dfb5141a8e19608a2d0d6392613 GIT binary patch literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&gD3ImS@Yy)sqh;Dsfh%pltbh zlfjn}%!fj&mG;>9hPj0+bszOnjn@cP@koLpqE_n%d`mwJ$ME~YxPtd3{2o8w-(R-n R{&Qy%ad>uV-Qds9egS;lwPyeT literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..9d072930811898df4fe326b11696917848dd5b9c GIT binary patch literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZMNmd3W4B1g^4cjuZoVJ9(9$=5T2icKi z=iY6M8Jy@y`p$PQ-*KGIudfWl`1#5(-obk_3nm+cqllCx<0bO%Hz=%DE5h$6c|g8x zqVP$OV6`fVP-eukg5@L^^QVF!|Fz?KCW=^T2-N?*$U?{)9JeHF3I&Zt) zzEWpUr*2h)?Yue0y1&78Yx({Q*0F);Flg^2XjkS>GaM70dc`S}O^`=XabjCV9?PE5 zjOanpSKD%10y8WKr#RKvBIIKtaGKI+%BncYVH}YUbAn6KXDQ>?I8xobZ-id|%N8_u z=9*gKXuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_Ca0%ry#bd!IfJAcZBX!j#PDk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&62D9OO7)(GgCYl7(ac_itDCB(br5^;-k6&$2 z05C(q6uv>K`%P)Ip`z(*mA-Q@bip#re}G!d|W;FP@xiMMGC@} zUpEuTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&RxqjJ9uPB literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..888ce036664489bc196958e792820e62efca084c GIT binary patch literal 1512 zcmb_c>u%F96b?hYN9Es9noFr5f1s33>a;eaEfZ2zAt!Ycqa=3x6@k&^h>{?2zUpB<;O>np=Be!en{_we3KgYgF8C?aLac!~V^28GpXMfg1>56HJo z6g~+OtX3rv%8Xc6u$<&#_EZq$zjj>DL=mfUSnirgs97}a_ilR=WTFf|f>Un+Bbvj= zUTn={3g<5e2?qBqtmT#*vS1V(58m#aY%$geXeC-Hxe9s?dT~w!h&uj?=c< z1RQI^OUj^8$hB;=g@Bb9YGHs9Zb=@C1o=+KmW+F?9BSv1Ze->=FqB_l9!p7iowr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8IFlgz2X$gCdi|xII*oFk7dtj zM)X0^TibG40y8WKr#RKvBIF|?aGKI+!m2pQ;W#26&Iv9_pQVgn<4ARL-Uz+^%N8_y zW|~^!Xuc$H?(8iP)qWcfQm}|OB+V?igS|K{5wWq>fE1Rj3R5zrOR_}1 z;1yBflQOCEE;!AopzGSiLU0;ZLYf(T?l+~;hKi=MRr=1s&;`qYU#$y{^D@OkiFWQ;o-H5O+>hjyAVV&3R-_x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dAxQ44chGWXdt|uQ#RN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%UfwQm3b literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..d9ac63cbf573e4342135140ac6082ab832ebf22d GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|I7w96P$<^A;G$-$b}5LINgU&pBoihRyNd{YfIem)q<1EX z-`kebKy>cRoO|xaxryT0?Tuj=KVKQfTX^rL{&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&C+P34GDC9*7#~l+%y^E&3?yx&SCQ8U7ICU4WVkxZf zVrQOG*ndF*jw_1Rp5me3bN2+cg-!Kh3m>RTTkoBdI{$+d$dq$Md_k)A&PkzAe=O!R znL6j0o)a#1utRd)r7enRN(GTzAP?O|(DTMh@T9SpGRwVYGh%XG1QxWi`IT%)6gTxH z;Mh=+GY*YHj)l<<0@h-vgaJwz(lnB5B(7!O)G*3LEEsLc0ZD8D2kQj&68Z#wRw zQWsFWYE_MGy*|Y{zrl7Y`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04wDY#rHeJib>T$}!hYIB+$zl+; z`nsv$^AN^EqxGuvSo?;tg)3DX%cBZkB6uM}1%`+kjm8i7mcAd1;rE4c1Mf@tJ$=5v UyKbxf=gt)3`0U!c#$TTO0&sk_L;wH) literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-6.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..2779bf34712a29ae2800aaf1bdadb4026c2bc172 GIT binary patch literal 1512 zcmb_c+m6#P5Dkm?kCmUJG&d^93zX88x@w!HEi0s|LQd)=MoAnwPIp&TB|d-;5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^M3q372TXW4ZkL{WO3PE18o1qQUT^9`?YoOaYD z;8+u0QU;Aewqc?z1gyl+2m_QbB6%zl*WzT3v z>OrZmcI37MW>^qTaav=GfRBm5X-dN>tKuYwaYP=>2`)*WrHtR;P<35b>7eU>EMpP_SQVsXMptBoya*yD zDt=NXP2vTo85MNhm{L|g5$hQu~4dAbd9blA6It_N3HC7{-IDM&WaTD zEkANHg))RGQ9jhF_S-VR^um>0lZeB!OY0VYe)bEOUbbHV literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..b3912f5656640be1c409c0e96f5cfd856ffe203b GIT binary patch literal 1512 zcmb_c+m6~W5KXu0e;_|cAs5QFd1)zSC00PTKv$Izs+?egt0azWhusxI>Id{g`xzZO z3HNQQs*wU7+h@*P&LmEzH&=#X{Cs5?@8P|j1fvbYQAC7bTp)k8L1D365`Is~1M+PX zg-?+J{}%Qn?(TlSu+wDsO8srx@jflN79#Al>x@01h@^~Yj9 zlc{@}=_%oS3p-foE^SauGQzQB9Qo)rhMqT;oTZJGl)3Pm&6vtn5n9m7<~O{?anjV6 zfMZR#pbQ#?T+2pV2w01u5(X$?K(bh_kngl?#kkkXp>{6mMrFPOL-{4&-FN{SCHV$@gEdjtxYIL3<}byDER0;+T}F7o0-bBzcqtC$^PmvFaI3 zNjWIx)u!53z!XcuDNagk9`YfPI7w(UW<|WpU>uPTbAkowvV`#)9O-V(YN1#Ess;6( zX-TbcG+PjuJ9`U6mEYQf5-j31Nxcj1;4V(`6&|t$%ry$`I-frGAcX}h!h}rdf-I0P zc|mmeM69a3b52t#>AEtpl$?fzROSZV1(W`?AB;dSCK?6fVP}ARDCD&Fr5gm}k6&$2 z05C(q6uv>a`*mqbLr2rvYJF#5lm$zHU!O~kGm&7aMLWB;u5I;rre}G2<(A_QbrLwu z6Ogz1#;FuU1Vf^I=vC>rW`N;^D^)YgqY7UlSivI&hPWC(;CuR^KZ4&E#udCT;rICY V{_e7^_Mbabh{Ll>>l%N4_6vcawle?# literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-8.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..f4ac1a0480c3f7654b06afdcccae72dc8b1c56ce GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Qw~3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq678Qj-(b77eE$V&nLu(-2Zvv(it%?qUK>}v9%LTezF}_RO5MlmsK!?aR`NiCA);362YgH4kEZbZLc4+YCH$T~-``!g S<^FSL5^;QXZQbB6&wc@o4YhXw literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-3-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-3-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..73333b2175f1e3bd3baa36d9bb94df456638efc5 GIT binary patch literal 1512 zcmb_c?T*qw6s>D~4>G@-GHsDa|0p7xumQRPZcGR<(=wDXZKs(|aW{m-2k=3C4DU=^ zzE_R0Nn!5HoO|xaxsBr4?Tuj=KVKQfJ9zJ={&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&CUfYgqqL3FU9Jfs*^)8zBy2I`SnJ6KT;M85filwl^ zi=BB&VgCgMIIbvKdy0pC&)pN)7BiiEX$}IPq&4|f$5m?a5=2x;IQQXv* zfMY{N&Nws*ITl7c2w01u5(X$?NYhBJk!QECVmxT&SUcBrqcY!vq5P7FNJ+|Vb=vNs zQWsFWYE_MGy*b4?zrnUE`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04wDXSD!SL9oYq@HHopSo2OgTxi z7}TvkaVmp6gc;E;^s01Q6Ts}km8zHJQH3uNybz%RLs*T*5BQ$GAB^Gmg>eJ#OZYu~ WzQ4V0tNrKB6yo^o+PcPHp8W!djkYBK literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-0.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..17b6f8b5e8bf2f2e30eea240041d383dfbda60c6 GIT binary patch literal 1500 zcmb_c?T(W`6vZ{ZM>D?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcA=8 N5XWcN)(!sh>=!@4v&sMf literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..69b3a0cc968c9af542ae3cac6890766754727527 GIT binary patch literal 1500 zcmb_c?QWYe6lI0JOM_w$y!B`_5gdtJsU*mM?YLbNMXV~|xMw1%chP*%ANOa-L^*y0r`;v2XaOsG zxiya|oWGy|#|?$IqqrXqx;p~LvQ72amc6H{ZM}C&>i!Q>AXCm2@foSwJ0*oe{jr$O zWa^$~dP=z5!VVU?OB|q!%8Z1gYJU)XfX<=pcoTPgW04%Mm|(>G5kCTg4u^JHb?-d zAz}(2A>IDAHnpL{@wQsv1sHY3a^TnJlH)>TSZdWmc^uQTyn5!LI0ENo2AWnMHWjvr zU@|mLuWDbdSD0A1(sZysYVb9JRXkE)h~)=-L*I|4@P1)j!SfQ{$G`9IF57DVxif`0 MJiD}R@#km109QA&`2YX_ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-2.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..3666813a657a2c1670e48e5c7d05ea3d6c7255ba GIT binary patch literal 1500 zcmb_c?T(W`6vZ{ZM>D?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcHvsC=~-Si@=zOrvmymatM8f$ zTSPDz8mCv4uf{73EL^D@SRK{)3c*SqDKNy+`T^h2_rnRiUl=#=yoC4Z@B6#!w%UL0 OOd*cXuB{vV<=HPou(Qqp literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..81484dcb5c90b81a4b52576268e3f5de7ecb0582 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dAIEG`&$JSlLttTH!RN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%UTwQm3b literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-4.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..0e156d7f198d315c5ee44e5b4c61433c86cacfa1 GIT binary patch literal 1500 zcmb_c?W)r-7_LXWkCES{Yr8tMe`L-b*kN0b+dK$kNZMs>q-{!)&T|+8FThLmVtkXf z`#vh-AYI-h&-=U|PZFo|+Z)3$e!emc560arm~0V_A|eFi0{M$A3d{AH@CQo1A>TGp z_#{cNUJD|%8L_NjImzYxsUXOI?YJEiMXbzWyK5q;SJABB8}+8hL>Ybrr=2CtXbv-b zxie2GoWGy|$2Enwr+66jJ9`4hvQ0H?%RW$5wq83Yb^iw`kSXVi_<~feos&YL{#eXs zGIh^0Jtti5U#~WzZ<(S~l82z*-EoFhB_-lE*SZzT?@7@t~Ds?OfB1%6t!o@=MHPC8>`0w%a*W z>H_N2t!l8|n^Ua&8*I0h@4sLj8;FjB?wkefs{Cn&V^XDFatdXW^wVaav)EkdKMPX-cChE8`@G>xg`~Cs>d^OBuh#k?!WA5qkZvTF{)C zSJVne^A&-6XYYWh_S<++fZfX-t$!SQw{YDs4xZ2iWDTR-ZgDU za4|Gas9EK!@mfLZ23A`&zCy5)M+ywFw0^)F`hGZp?+fDw#!L8~{=UDvZmad@_7viH L@7lbuTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&U8AeJhFi}(6sN>lk%Fe>hfRiE zhA<(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQScuPLb3zK*=Lr|rB-GuUH$t!fRSTLs zvx-{bShl1vcX$g#wco~r5-bu8NwW*?;4Vq>n2h)m<{J5Ty-%M9kiwFeK|-f&Nteiz zqNF-}TEunUxnL=iY+ai~O2L9sDszMG{ONEu^v56=6OH}JsCSDzDCBJLrSJQbk6$q; z0GJ_Q2Hzmv{iZaPp`&rPTHhHMRl!o=*XL4@tVoE|qMdiGE>@3gx|UlFK2)e6X`X7j}BJN&o-= literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..e1d4befddbb889c33bee1545db0938bb327038b5 GIT binary patch literal 1508 zcmb_c?TVW~6t!FW9$|iq(P*=+6iS+AAxjdsNxBq5D5Dv5iaIim*db{d}cq@8G?id6NyoK|qRv@dCMv4f4w{B>bL|2jrSM z@}C6p!muDhnE^|4mXS=%pK^lS*H*{YQNYR!j=MS%Y8TD=y-{z9bd=&paB44MMKf5L z%dLJ)Vf_UKIIbz29mV~iZ|?{!!_?(#8s?s=ven)xY3F~C0-1C!iO)#o-YF>*>W{^I zCezMorl*9KG>4LcmH4wJ<;lBa%fTMy}3GygPPAntOBH1&V zl4?+@t8KY0fhiV*Q=C-T+~;E=aFWnq%E~CtU>uPPbAk)fX9?psI8fbOG(xZcWeb`+ z^NL#HXtp9Scjgv|YQK#K2}HahIoO4}ILTu?W-FL$x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXF1kincMZ21d8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lkfV0{F literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-4-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-4-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..b4369264d79c146b7bb3feb055fa49e08a601d8d GIT binary patch literal 1508 zcmb_c?TVW~6t!FW9$|iqQDZ|Rg_5RO$dbfuk}ic1%4kNNqK=GXx=RT30s5GIklqOJq9M=@ip5kFR=qjven)>sr^4lflNA=#22J;@0=70^~Yj9 zlc{~4={ezQ2Rpb>UD~3EWQ1eEIC9Zl1U+wsoTaT;h`eyy?TCuF^bKfb`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`OtMJC$h90(G9I*YtetDRk(uwoP=0}VBqh~xdfm>U zQWsFGZdHSI-kf6X-(b77eE$V&nLuCe#u2$NC%7O3mN0&c1J%uCBlP-TwxGGQ zsHi25W@`d-XYPQg_S<-nK*Sr8qg}X*lRU-~wuZSz-hKb$ry-=UW~HByIbD-Aazlui zsQ5_{*NNwxrc}^PZDJuf^-CemExPySqs7RZf?{+u^=6a)7`af%#qjgM^JX8um>>b5 zhKMPAgjD;R+Ej)L$Jr@?XJAwnOMzdV3y!lQ!9uBa*)w`3JeKYlPBr*Yp#rCQ0>YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mU(6wp+ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-5-0.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-5-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..c95fcb4cffbebdc8db0dcc30fbcd8d2523e404d6 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&avZxxx1M<@PKmQ31x?Ejn+&@Q zVKNj>t!iJbSD0A1(sZysYVb9JRXmhnh~)=-LqCir@P4IT!Sfp4$G`9IFWYker89{* MJiD}R@#km109P-w`2YX_ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-5-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-5-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..16c81779b71e36a80f91e67bd31e0bfe891fea33 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9znm2<4Y1Y{~1}9LP`>MNmd3W4B1g^4cju3oVJ9(9$=5ShuM*A zH`i`sEMP}R(s#ad`HtgsaeZYN#?Lo~(She?9!$3gM-dT%ae@5%Eegx^n(#YH9*}RF zD14G6Sg!?<+KgCMu$<&_@l+7xcOBO=QN+p|j(aAOdKb+H{c(SWOqAhAaOy2#MRQo$ z%bj^l;rs;!IIbx=&lGpVf%i<{ShlGi+p_mmm96(qN!|ZJ3S`Q;B0eKkd#9vOs6Q6- zg-qSkOiu}yJJ`WOcWH}ak`s<4_QEeue?nB=icknePC#kkkXp>{6mMrHmChVo0yVQ*(_&f8c2)i~!!fB+FFA#>N%AO5PHd~lW7RX7 zk!nz?t8KNdfEkvAQ=C@VBIFYyahlR-#>zO!VH}YUbAkmKu$1v@9O-V}H$t!fRSTLs zi;7y|XucvaclHj5YQK#KC0N88l4cj&!Cjmd37)VO%ry#b`=34!A%zty!<5YFimZ?? zc}aBmL?m_I1*aL6bW@vHN>0O4Dszi&gZXGN3Z@_!6HSBJq(4SJ6ml{AG6;g%$FDXh z0GJ_Q3f~~z{iZaPp`+>Sw7zpNs)A*}ug@jNxk$0pqJ`=>X4mRgBM-F^I4e?+wEC{8 zutfxep>cXu`D(nvz`~Wffz?rsuMn){kpe?3tsn3W{VuTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&wqfgCqg&5B6sN>lk%Fe>hfRiE zhAcRoO|xaxryT0?Tuj=KVKQfTX^rL{&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&C+P34GDC9*7#~l+%y^E&3?yx&SCQ8U7ICU4WVkxZf zVrQOG*ndF*jw_1Rp5me3bN2+cg-!Kh3m>RTTkoBdI{$+d$dq$Md_k)A&PkzAe=O!R znL6j0o)a#1utRd)r7enRN(GTzAP?O|(DTMh@T9SpGRwVYGh%XG1QxWi`IT%)6gTxH z;Mh=+GY*YHj)l<<0@h-vgaJwz(lnB5B(7!O)G*3LEEsLc0ZD8D2kQj&68Z#wRw zQWsFWYE_MGy*|Y{zrl7Y`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04v~$;Tv3eY{ZMDn6hYIB+$zl+; z`nsv$^AN^EqxGuvSo?;tg)3DX%cBZkB6uM}1%`+kjm8i7mcAd1;rE4c1Mf@tJ$=5v UyKbxf=gt)3`0U!c#$TTO0&rZlL;wH) literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-5-4.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-5-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..f85acea0917ecd50f4d2b422fa66a50c4b4bee9b GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&uHoukqg&5B6sN>lk%Fe>hfRiE zhA(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ^LShl01kd+y1cXcGxDw_3squvzhD8*0U)LFueW-v3C zJN=Zx`U?thTvNDviibhp*%MfXsmoy+=7FlR)!I3!{Xa;7Ogfju7o>9SoD>T6$6~&a zsePX5IpJ~#E4WZy+MJT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2<_SnzzH7=3 z;bJIUA!n7Z#%l$s8(3}C_zJ;F9!fC8()s~!=!fA1zOS?!7_Z@b`uqO=x-HjV+LMUm Ly=(IZf4TPyONg`9 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-5-6.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-5-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..c33df9153a2f8959d33c6428253e2f61bb4d82bd GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Smq3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdPzu*RmZQ1+2{AxThnbcF}y$ANOZSM=5>;r_KsiG=r77 z+Uchh)?ZM7zgr+63+oIQbMn7VvT!#q$`w%R)(-2Zvv(it%?tB`R>5C!lQk zd6U5x0nCR&t5xN(@eOkeSL!}iM>W1eu#yK73=y?jKj2&Xel&&O7upTHFX8v}`Tp*@ SE%%>0lZfN9YwHGodG-sA<+XVL literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-5-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-5-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..b5542077807f81832e30683d6bfee9261830caf6 GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|I5FzBp-`-K!9~qh?NSgalQ@Fho0s5GIklvXj z>*uzmG!UITGv}WBac;tResg6Q#?M!V@fP0OnK#)Y5(G5QxyX^b*do7Juc^3a^c!-q ziTp=Nz4bb$Qkwx!GM>^@&L1<1+>YIHOcd}Uh2yS?q~1ldes9#9A`>O#0h~HZSg{mV zc)2r=DeS+X0LK+Y`-$Ry(085)Yzv#}#TMREmA2kHC2jo=QXo^#74aFV+B+qMLjAFr z&t%#<&GeLTxq}^&>n?3kNK-0^Lg<2LyI|x{dp%MltVMNnVM##0>STXLka;Tk4x>1=wfua18h)_w&X}{?@ z`%0Zb?W$EZw*C4T+xiW*Tgmrdur>yw!=Rm$pj#?`mXMH^sTYDl*(80Cg`n8V(opq` zCA1ur@@i9UD_}w-6^z6sHuJ@pN)pE`nDQcwQW!_%!kmzt_Ib?34GDBN7q!r|vg&)%yThSGA zr6{NlpXO1OcP3cEB->Obky0?fkjmVmJ8w3e54{Np#zYfuI_`~-3x%8yKKDIu`r!)( z1pqSy%-|cOyI+^4G;}oWoz`~>Mp>`~`1QFIB+X+YwP+XD)-_g-V|J`gIrvbaoFrKc z!d7256?`7Rcxbdx$Yy6i&DFJ(l?$VlFB+e~Ps(xTx8xT?!&)5+})&BoihRyNd{YfIea$re`LJ z_ian5kT_>%zVn^ScM>IwyBkf@e!kJPHaz!pZ@NV|2uM*dULbe5MSdBEgg;X94Y{U{ z{AWSDFf52rX28;%Wh4`e=bRw7W7&?50#;^l+|`j#yJ$Y>kNY#EqZB`ZQ)dM$n!(Ck z?etR$>n|w4aZSgd_gMr&PkzAe=Oz; zncC->o)fNiu!9TLr7em`MmQFXBNyFA(DU1nv-CC=A}`!lE21JUeFIwA`ieI=N?Pg? zaBK)KD1$~J+c41%0#;(Eg#k(!lPnT3a;>&084p@H*3LEE$jtX(D8Ilwl9F=TZ@bQ+ zQWsFGZdHSA_fE0)Z?N53zW;)?OdvWA+Bpl_mif~ZN2E%<cavDHU{6n^*`={ZdGCiyplBXfg7ppcoxZz1gHcMlMuxG5kF6yxE5@CP)CN zAz}(2A=Un-HkF~mY44Q4Gcc-(rNFPw1;<&DV4+mI>=`}1V|1#ShvF1C%@feH{IJQe zivT7=;nb?~)p&)8g)4OjtD_oUAy~-+35HmHz&G^$XbSIF+6_Ff;eGo1{_eUh_g^}b Nh~u+s>jr;$_6ug;vuTFD6!tpy9!0;6V>=fb{~1}9LP{EUNmd3S4B1g^4cjuZoVJ9(9$*i-N7<2N z=iY6M8Jy@y`p$PQ-*J@8Zm$f(`1#H-Zs5I}`r{44VMvOC@dA1C4GPNDittBDo{(po zD0mUXU#$uvlo_%#XBo-F>?J42Yddb!L?J6PIPRE8s9iMe_eQ-5GEs`3!D({=E1JQ| zUTn={3g<5)uj!JNJcmoj3W=-MbPu>6=&&nEJR*-jYdR8Tm}}jvhfYCag;RF zCE!>SUQh;&Lat?_Ed;E@PzeK+Fd|tbV&pk3TQcsoa;Tk4x{;ahz)*gHc_by(Y;`-$ zeWlKzPSvU!+qyZ%y1&78D*65k*0F);FzDt<(5}p%rZ^%x^^#L4n;_4kG2`i&GgKq+FxSX`=zaP;fE1Rj3=%S>OR_|s z;3ZMvlOnG2&N)q~pzF%SLU0@FMZ#ieEe#I z0)QC;rtl3?-LFfd4HZpmtMr|Lp$nD*zd9EjXGMaA679Teb!~VYvu(BY+(Us1oaPDW zT7KMQ>_rIEp}=aT9oDX4YT-)NMSWD^HG-8qlwb(x2Yg3A49D>M#<+s_E&Luo-``)h S<^F4D5^;ETXuTFD6!tpy9znm2<4YDcGZ4Qp zeWlKzPTi^o+j(=0b$^5H*7E%qtYZVwVbI=5(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aiqFAZ-id|%N8_u zW|~^!Xuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_CT}%ry$`dY?WIAcZBX!jw$uk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&61=Hbd7>q$MCK?BmQSTP{P{`TfOFsxEAHUk5 z0APlIDSU%e_nXpaLq*fsDt+f*=z?Xyug(R>d6{CNL_3$sb*-)~pN<}Us8ETsA_ZZ~ zubT|Mj9@$zTCKFl#y5;DT&dfrk7~R|u!=_#3=y?jtsn3${V*KE?+fD!-k0!u{Ct0Z T*_Qjyok_&u*`;-ZKR^2gZbr32 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..2c03127adf883478932512a62de7c3901aaffed3 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYrES)>F2Yh|l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(l`byKZpRY9S9lSSlf4V_93`tosULx;)gMuoG2*0D`0ePm5 zf+s=zC@P6iX2`OFn7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=?VNgj&?d6sKR#=TY!wR1@~GV>i6$}cdFrKBA9ZO_?P z>I`Z%t!lCEn`5l~8*HzU@4sLz6NnCjc20t}W&Sk7F{x9pIEAta@+c}!EThO{*)y7v zdQj@CUAZlR85V?7oYvSP;1eQnn$mE_syNAE9FYfef=e=BDdX2TRNcI9h2H$j7PNO3 zHMPXid_`dH%qyPj0(DLOe_SaK_#TQLAU;Vv>5qQP>hbI{%q18BM&OM7=9V}{_Nvd6C?oC z5HW?1kZONho7zy}xLYOg9E`eR8Stxf!Es)uSSZy(c`UtW^i27<_2ff|N}LrbC|iEs zWbkDO^P$jcReNlG!`#A^rjPYegRc>+;-LgXM4iqL_?CVcP2u;2b_MTC_&t8UzrSqD R{pZdk;_&R!y2YQL{Q`!~wQ&Fd literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-2.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..d41d5b589e5caf6801b9d2dab35c045befd86181 GIT binary patch literal 1508 zcmb_c?P}XF6!kjx9znm2BimWn%wS|$3Moz8C0QASFl0xuHEhesa@rCCdw@OW9%NUt zo$qdA%-}><(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQSc?{tsB?`&YNSb^BZipmhZn{Z45+*LAxhGJ1T#cl89ERmx4jrBz=^npxDZ@NcD`R zv>KG^YFlk9U`iwvj3gB{55$N{k|Zpg@G^=s7)Rv6oREU{dBVjt33WH;jnM0V)q>{E ztfE#pmMtmF9o_;_?YHru1dBvN((HmexJ!~eCL_Ltxkmn7@6+c2q_E^=kkBbx(k1ew zD5(yg7IB?-E?CMWTh}I$Qm~+u%G{tke>$8E{V@o}L}Pz4>fIs_3OO5m>HGfV<5vs{ z0A>i7!8b^EzbQ>+=x91yt?vwss$ePb>vJhcRwP7f(as&qF}qe5tEW>9K2)e6X`X7jjm$MgRZ+ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..91e683543dcf883c76eb7327245c6c0413489bf9 GIT binary patch literal 1512 zcmb_c+m6#P5Dkm?kCmUJG&d^93zX88x@w!HEi0s|LQd)=MoAnwPIp&TB|d-;5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^M3q372TXW4ZkL{WO3PE18o1qQUT^9`?YoOaYD z;8+u0QU;Aewqc?z1gyl+2m_QbB6%zl*WzT3v z>OrZmcI37MW>^qTaav=GfRBm5X-dN>tKuYwaYP=>2`)*WrHtR;P<35b>7eU>EMpP_SQVsXMptBoya*yD zDt=NXP2vTo85MNhm{L|g5$hQu~4dAIEG`&$JV<>SFPN7{-IDM&WaTD zEkANHg))RGQ9jhF_S-VR^um>0lZeB!OY0VYe)bEO4z^za literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-4.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..7336fd6535a6ed885281ababd7c3292d5e9c42a0 GIT binary patch literal 1508 zcmb_c?P}XF6!kjx9znm2BRgK$%wS|$3Moz8C0QASFl0xuHEhesa@rCCdw@OW9%NUt zo$qdA%-}><(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQScuPLb3zK*=Lr|rB-GuUH$t!fRSTLs zvx-{bShl1vcX$g#wco~r5-bu8NwW*?;4Vq>n2h)m<{J5Ty-%M9kiwFeK|-f&Nteiz zqNF-}TEunUxnL=iY+ai~O2L9sDszMG{ONEu^v56=6OH}JsCSDzDCBJLrSJQbk6$q; z0GJ_Q2Hzmv{iZaPp`&rPTHhHMRl!o=*XL4@tVoE|qMf^zYj&+JR*zc^K2)e6X`X7j|Z~N&o-= literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..5283d5b7c43f57378feb3665a551e40b91ae0bf0 GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Smq3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdPzu*RmZQ1+2{AxThnbcF}y$ANOZSM=5>;r_KsiG=r77 z+Uchh)?ZM7zgr+63+oIQbMn7VvT!#q$`w%R)(-2Zvv(it%?5C!lQk zd6U5x0nCR&t5xN(@eOkeSL!}iM>W1eu#yK73=y?jKj2&Xel&&O7upTHFX8v}`Tp*@ SE%%>0lZfN9YwHGodG-sA<+XVL literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-6.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..ac18670568591b3027f4cdbd632c0b9b00560afe GIT binary patch literal 1512 zcmb_c>u%F96b?hYN9Es9noFr5f1s33>a;eaEfZ2zAt!Ycqa=3x6@k&^h>{?2zUpB<;O>np=Be!en{_we3KgYgF8C?aLac!~V^28GpXMfg1>56HJo z6g~+OtX3rv%8Xc6u$<&#_EZq$zjj>DL=mfUSnirgs97}a_ilR=WTFf|f>Un+Bbvj= zUTn={3g<5e2?qBqtmT#*vS1V(58m#aY%$geXeC-Hxe9s?dT~w!h&uj?=c< z1RQI^OUj^8$hB;=g@Bb9YGHs9Zb=@C1o=+KmW+F?9BSv1Ze->=FqB_l9!p7iowr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8IFlgz2X$gCdi|xII*oFk7dtj zM)X0^TibG40y8WKr#RKvBIF|?aGKI+!m2pQ;W#26&Iv9_pQVgn<4ARL-Uz+^%N8_y zW|~^!Xuc$H?(8iP)qWcfQm}|OB+V?igS|K{5wWq>fE1Rj3R5zrOR_}1 z;1yBflQOCEE;!AopzGSiLU0;ZLYf(T?l+~;hKi=MRr=1s&;`qYU#$y{^D@OkiFV$#y0&~e=6)ov1Q~LPvmym~ z%R5fl5gZcbqm(1Z#%lztcqG9PSL+Adryqu6_`Wc%;C%_-u%F96b?hY$K>Bpnj0PD50uhLo7!fyWkRYd*WzT3v z>OrZmcI37MW>^qTaav=GfRBj4X-dNhtKuYwaYP=>2`)*WrHo(WP<35b>5|Zx`<3v`FxXEn%*af7ko;c>pOaSrw#YN|$7bya*yD zDt=NXP2vTo85MNZm{gJN_v_9vs>5P49^+2Bjx_a`5}njit7 zhKMPAgjD<6+SG;$$K5D_=U~(o%Ya{<3y$+L#X_leeq~&l@^SR8(N!zAo_{D*iL)XF zeanxWOrZ>6N|X<^s{OVMFuic4>1KV@;A;e{cqqXT*AMufez+OK?+fh$-k0!u_ephmW6 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-8.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..2a1b3ae5e9acf0b6794054820f6ae5710761b8dd GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|m|Mk$Lb28b7d2b8OF^Ve;+RZHGGQ{YyNJ*S=wtRldS;S% z-?o$nqH|{EJKwo{Cs8uLz0ox7=PONn3-8^`pKK8hLsArs7sy*|QBbbegg;R74SA-H zf+s=z^|~NJnITJamXS=%pK^k{*Ou+*C}d>@#~mFBwTou`-l#W4I!f^)ICYk=q8Y5r zOJq9M=@?p5kH9clHF9Ve0ZV4f8-%*=p~c)czl&Kqj3_;tNu_cTNh0`eQMl z$<#j2^qg?HgB@I`E^SdnGQzQ79C_$2f}Xe5oTaT;h`jLH?TCuF3=C*x`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`M6yW4$g^BiG9I*YtetDRk(uwoP=0}VBqimzZ#vGQ zQWsFGZdHSIyQf(DH`q=s-+#ecCJ-G5?VJT|%lv7IBT}VaatdV=Y+j3h1Q!EIlIH|CCz{f=3B%$Gyl~J6*I3f?`1Q(>w62@JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob%N-}k2`)1xtZnoePe$BEdq5cF{GuddKLP@^P!dhYA%q%@Yu| z{JP2Dix9>`q1CGL*!YIAg)4O%tD_oUAy~;n35JMTt=13tmcAcO;P-`g1Mf@tJ$=5v UyKc+<=guVJ`0U!c!C#*J0)B?IQUCw| literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-6-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-6-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..975cf35dcce6f3b63f8a644e39a22f963942f6b5 GIT binary patch literal 1512 zcmb_c>yFYu6s~K04>G@-(hCylA4OynHb7UvjR_%UTBhxow$n_fxEn&^1NfjmhG(WN z_f=zTQkXL{-}%nvJB^aX?Tw~sKVNCuJ9zKr-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?Sk6)HNHZyk_QqDVYPn1_w@Z}3coM38+c#B@9FdX U?R8u3KX)b($7k2p4gT`%7pFx$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7jhi0jcZdrPBr;Zq5`LR0?L-3 zHyL~pzuTFD6!tpy9znm2<4cxqW-ziWg_b1llB^6$7_y_-8n$I*Ic*7nJ-{Aw53(c4 z&b`|hGdR(a^qucqzT-HZU0oQ4@$;2oyo2{<8jRNnM-eGY#!KYiuTfYnmxSL@@_>BX zMB$Sl!E#v=q0ES71Zes9>DAQNTy5uAE+SkW9- z_IzU=QaFD>0gh{m&X(eC(D$|kj%AzjwJm!`rERr$OzQp*QXrGgCGiQV+&dQiT?@vi%LOaGbW) zCE!>QUQz~)Lat?_4Fs&jPzwW;FeG^_668A_TQcsnvag+Ux{;Z0!BBpIc`PO6b>4Qp zU8PQ-PTi^o+j(<{b$^5H*7E%qtYZVwe$d`g(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aHP6<-w3__mn~@S z%rv#c(R@K*?(7W^)qWcfQm}|OB+V|kgS$8_5Avg^yAtGCmG0`}fjCw=lLm_8_Fa02xeEe#I z0)QC;rtl3?-ET^x4HZpiqx7AFp$nD)zd9Ej=Vgk85-n86F)yu4TRxs1e5g=~vmymy z%deXZzKmcz6k4sc$Hq5|EnKPFsE=yAMzD%U5)2WwTCE@OE&XskhTj*)1-vie_wf1t U{=6;spF5L?{j+oH27h|?3vpGoMF0Q* literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-7-2.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-7-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..4277e4d44c9356deb4077a77ec817b0de31f6a7b GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7q(&R*T%IeAE%mpC{cmaJOO3P z&zlUs2w*-GTCFONjc=G+xKj7AI;!y%f|WdwV2G&I`T^h455o!kzR<4VeF?wE&-eG2 SZMpy4nM53(U0OHz^Rr)x*|l^4 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-7-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-7-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..a16497fd90219fcb5389bcf970dcf4cd62ff4844 GIT binary patch literal 1512 zcmb_c+m6~W5KXu0e;_|cAs5QFd1)zSC00PTKv$Izs+?egt0azWhusxI>Id{g`xzZO z3HNQQs*wU7+h@*P&LmEzH&=#X{Cs5?@8P|j1fvbYQAC7bTp)k8L1D365`Is~1M+PX zg-?+J{}%Qn?(TlSu+wDsO8srx@jflN79#Al>x@01h@^~Yj9 zlc{@}=_%oS3p-foE^SauGQzQB9Qo)rhMqT;oTZJGl)3Pm&6vtn5n9m7<~O{?anjV6 zfMZR#pbQ#?T+2pV2w01u5(X$?K(bh_kngl?#kkkXp>{6mMrFPOL-{4&-FN{SCHV$@gEdjtxYIL3<}byDER0;+T}F7o0-bBzcqtC$^PmvFaI3 zNjWIx)u!53z!XcuDNagk9`YfPI7w(UW<|WpU>uPTbAkowvV`#)9O-V(YN1#Ess;6( zX-TbcG+PjuJ9`U6mEYQf5-j31Nxcj1;4V(`6&|t$%ry$`I-frGAcX}h!h}rdf-I0P zc|mmeM69a3b52t#>AEtpl$?fzROSZV1(W`?AB;dSCK?6fVP}ARDCD&Fr5gm}k6&$2 z05C(q6uv>a`*mqbLr2rvYJF#5lm$zHU!O~kGm&7aMLY8>&sL9XUR&3C<(1rW`N;^D^)YgqY7UlSivI&hPWC(;CuR^KZ4&E#udCT;rICY V{_e7^_Mbabh{Ll>>l%N4_6vbVwle?# literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-7-4.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-7-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..df1f59836c150937f33d2530494a1f532a1bfb44 GIT binary patch literal 1508 zcmb_c?TVW~6t!FW9$|iq(P*=+6iS+AAxjdsNxBq5D5Dv5iaIim*db{d}cq@8G?id6NyoK|qRv@dCMv4f4w{B>bL|2jrSM z@}C6p!muDhnE^|4mXS=%pK^lS*H*{YQNYR!j=MS%Y8TD=y-{z9bd=&paB44MMKf5L z%dLJ)Vf_UKIIbz29mV~iZ|?{!!_?(#8s?s=ven)xY3F~C0-1C!iO)#o-YF>*>W{^I zCezMorl*9KG>4LcmH4wJ<;lBa%fTMy}3GygPPAntOBH1&V zl4?+@t8KY0fhiV*Q=C-T+~;E=aFWnq%E~CtU>uPPbAk)fX9?psI8fbOG(xZcWeb`+ z^NL#HXtp9Scjgv|YQK#K2}HahIoO4}ILTu?W-FL$@Fho0s5GIklvXj z>*uzmG!UITGv}WBac;tResg6Q#?M!V@fP0OnK#)Y5(G5QxyX^b*do7Juc^3a^c!-q ziTp=Nz4bb$Qkwx!GM>^@&L1<1+>YIHOcd}Uh2yS?q~1ldes9#9A`>O#0h~HZSg{mV zc)2r=DeS+X0LK+Y`-$Ry(085)Yzv#}#TMREmA2kHC2jo=QXo^#74aFV+B+qMLjAFr z&t%#<&GeLTxq}^&>n?3kNK-0^Lg<2LyI|x{dp%MltVMNnVM##0>STXLka;Tk4x>1=wfua18h)_w&X}{?@ z`%0Zb?W$EZw*C4T+xiW*Tgmrdur>yw!=Rm$pj#?`mXMH^sTYDl*(80Cg`n8V(opq` zCA1ur@@i9UD_}w-6^z6sHuJ@pN)pE`nDQcwQW!_%!kmzt_Ib?34GDBN7q!r|vg&)%yThSGA zr6{NlpXO1OcP3cEB->Obky0?fkjmVmJ8w3e54{Np#zYfuI_`~-3x%8yKKDIu`r!)( z1pqSy%-|cOyI+^4G;}oWoz`~>Mp>`~`1QFIB+X+YwP+U|t7BeU*H}GHIrvbaoFrKc z!d7256?`7Rcxbdu%F96b?hY$K>Bpnj0PD50uhLo7!fyWkRYd*WzT3v z>OrZmcI37MW>^qTaav=GfRBj4X-dNhtKuYwaYP=>2`)*WrHo(WP<35b>5|Zx`<3v`FxXEn%*af7ko;c>pOaSrw#YN|$7bya*yD zDt=NXP2vTo85MNZm{gJN_v_9vs>5P49^+2Bjx_a`5}njit7 zhKMPAgjD<6+SG;$$K5D_=U~(o%Ya{<3y$+L#X_le-Zi?Wd|ds?xKb;po_{D*iL)XF zeanxWOrZ>6N|X<^s{OVMFuic4>1KV@;A;e{cqqXT*AMufez+OK?+fh$-k0!u_epggu{ literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-7-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-7-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..00de2f419649b4ce14431bdeecfca344225e6826 GIT binary patch literal 1512 zcmb_c?QYXB6b(bXN9Es9>NKT;{DD$BsZ-mGwoFJ>g`Ct)jFLEVoNiN9i3i|8c?_H&=#X{Cs5?@8P|jdgBcuK|u4IiyXQ04f2cCii&$iACQYp zkhjUWTJ#Tf>UP!BbLGl zFSh0}h5Z*4;JBh_?+wDsI6Y3qNG0-18Ih|fsX+$kv(>W{^I zCezkwrl*99EzFQycWHw{no>a|7sy4oA@sbl5OMCo8QQqgmF`E z0**BmIpfeM)Uq(zLcm%Kl`udFLz;#%LayD$igB-%L+xDBjmmrnhVn}yLM17u{kG%m zD|H67t5((6_M2mD>o?d=CEtI++8BrqgLY1WZmIlPLPA=mUI+$dlk`y*f?_L6L)9~u z(DI;^TbpWH0TUvrU?eWFnJ-3Ek~n6;gco6y!f`|{oD-7M9*?=WA%X7ZycT-(S1qXb z%t~s7W9gE@xx-r^s{Ga-lwgslN$Oc}2YX4JMP$U6aITSe*ZuUl4=F5p;m35!mUM|+ zDGI8?r+HN6oe7pO$<~!gq!i3Aq%t??&YKQq18)q1G11tYjJiYQLLq1UFFntjeEf<* z0l*9aGq{6v_v_M>hK{Da)%s4sC<~SVzh0Muq77R2`c2ABv}md zR(G7X12`nwN3KSt-RLv}xDtw9Hg$NWF;%fYW`}D(L4Br>V6}&Iud;ENVciC3+ Q&#fuM;o7BfjXz)e1&W8ZJOBUy literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-7-8.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-7-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..5d110b0a72afb7f166191f2500c13843254be4e4 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82p05Tw>FbZMKlt&32KOwvg*l4QbUQgjTxi{S+90a5&7%z~!+##0_NJlAt0H@9hRy2c^ zx!UQ+6xLr*fa98?^F(n!95_z|mSO7hH4Sr5RoQCql+^woq(CN}OX4$9xpztmh5BPL zpUKod&GeLTwSyg8s4i_$L^8s$U>v#VHiDkFLeA1wEJR+o?RG>(T>1vIvi%iraFn#w zCE(Z)UQh;&LbhR|9R#ezPzwW;FeX_fV&qyKQ!?(ga;Tk4x{;YbfuZ~Y^GHg{>AdMW z`%0Zbt-4hWw$nSt+P}eeYx({Q)-r+UFlgr_Xj|q_Qyh^h^^#L4n;;LOJT!IQ2^*%`Lj~=A*^Pn}T9=H1%ea{usGX$;I&V!1HDwzL+2Z zpoWMke1uf{o7z-{3a7JE0?)vxDwYDjIu{&gMS_J=?XqX|^lRhVl#f$QK9s1yX`XyTXUj#583awU^$Hq6zEnKPlSRK{)3c*SqNH9dyYW;w3>HE{8A*Hqjy`VG0* zME;|s-g=!=sm*{V8Bb{{=Z_gh?rXc{m?+>y3dbE2Nxh3^{obfIMJ7tf12}b-uwp5! z@N#D!Q`mn&0gfw*_7lbZpzk~p*cLX`i!HpTDs8=YO4|A#q(G*eE8;U!wRcJih5BPL zpUJd!n&~OwatAvk*InA8kfu}+$pv!JZ3sPYtOZXRk(62PHk%=nQQ=$A%H~(HAz|Fq zmw;nKMb0=h3bibZb`Y=@LnRDQ!ic7!jF4-$v0~h7&F5uuWl)9!Yh zeWlKzcGapH+kSJ5ZT$w@spR`FSQ`V;VbIP=&@GieOGrq|)C<9&Y?406LQrgFX{dU} z5?T&Qd9|sw6)+)^3P$1*oB3i)C5dAeOnDJTDU2g>VNOU+`#k32h6K8si(2T_ziL5! zXI@e(97|Ue<___B5 zQWR8&PxGkCI}Jz6j$OD)W?Lx0gw>1IGE?lX4SsqpR62S`*C@_T8X#9Zh>HFaXeqR_@@Vx$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE@X0Dqg#zU)TY2`o`9s~cTI*} zgfJKir&g7(#w!dgT&Wva9o6^>!Ac%VFvQaO0pHLM!wI}!X*ck^hWF|3`}^y*+<)mz NB970ltsDI1*)KuTFD6!tpy9!0;6kNY#EqYOWRQ)dY)n#0Om zZuMgd>n|w4aYNzmDDH;?XGdTerY;}TF!xlot@chy?f*dvWYW1LJ|mTTr=(D*KNj*ukah(gwvOCmajLk%w+$==pWTS$3TWQIuY%6H}2?fdQ@Te8X!TryX?( zIM#%hltH7AZJ1~a0V^>y!T=?VNgj&?d6sKR#=TY!wR1@~GV>i6$}cdFrKBA9UDw%H z>I`Z%t!lAu?-*ZqRW~16p*R1s1?`58n77eT~C z#ZStlNxa}Rqk^s*6AQsl$q}t!srZ!YK?p6sr2cxc72K?$=aGaMZ7D}~H9!u{Ty?W-MI3>=C6f`YAY%=UJ zgvn4iwW@u!USVS4O4GsmsKM6=R`F1RA(kKT4gD~h!uyqW1m(C>O M@a)pM#h;)30$%U50RR91 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-8-2.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-8-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..996dcf6ea7039612203326b0d5c612b438d9a532 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE^NcryGFMfd8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lgaI@9` literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-8-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-8-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..c01f821a031a193fb260cb7b123ad7b58de769ab GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Qw~3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq678Qj-(b77eE$V&nLu(-2Zvv(it%?TezF}_RO5MlmsK!?aR`NiCA);362YgH4kEZbZLc4+YCH$T~-``!g S<^FSL5^;QXZQbB6&wc@nv9)&q literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-8-4.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-8-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..7c325c9d370634b6fb762edee6f31173813eac16 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE?mRayGFMfd8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lkV6)l) literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-8-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-8-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..8916d5b2a57d2a1b5270e12acc065b34352fb112 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ(l?$VlFB+e~Ps(xTx8xT?!&)5+})&BoihRyNd{YfIea$re`LJ z_ian5kT_>%zVn^ScM>IwyBkf@e!kJPHaz!pZ@NV|2uM*dULbe5MSdBEgg;X94Y{U{ z{AWSDFf52rX28;%Wh4`e=bRw7W7&?50#;^l+|`j#yJ$Y>kNY#EqZB`ZQ)dM$n!(Ck z?etR$>n|w4aZSgd_gMr&PkzAe=Oz; zncC->o)fNiu!9TLr7em`MmQFXBNyFA(DU1nv-CC=A}`!lE21JUeFIwA`ieI=N?Pg? zaBK)KD1$~J+c41%0#;(Eg#k(!lPnT3a;>&084p@H*3LEE$jtX(D8Ilwl9F=TZ@bQ+ zQWsFGZdHSA_fE0)Z?N53zW;)?OdvWA+Bpl_mif~ZN2E%<cavDHU{6n^*`={ZdGCiyplBXfg7ppcoxZz1gHcMlMuxG5kF6yxE5@CP)CN zAz}(2A=Un-HkF~mY44Q4Gcc-(rNFPw1;<&DV4+mI>=+%rXY{I>hvF1C%@feH{IJQe zivT7=;nb?~)p&)8g)4OjtD_oUAy~-+35HmHz&G^$XbSIF+6_Ff;eGo1{_eUh_g^}b Nh~u+s>jr;$_6ugmvx$Yy6i&DFJ!F1|m|Mk$Lb28b7d2b8OF^Ve;+RZHGGQ{YyNJ*S=wtRldS;S% z-?o$nqH|{EJKwo{Cs8uLz0ox7=PONn3-8^`pKK8hLsArs7sy*|QBbbegg;R74SA-H zf+s=z^|~NJnITJamXS=%pK^k{*Ou+*C}d>@#~mFBwTou`-l#W4I!f^)ICYk=q8Y5r zOJq9M=@?p5kH9clHF9Ve0ZV4f8-%*=p~c)czl&Kqj3_;tNu_cTNh0`eQMl z$<#j2^qg?HgB@I`E^SdnGQzQ79C_$2f}Xe5oTaT;h`jLH?TCuF3=C*x`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`M6yW4$g^BiG9I*YtetDRk(uwoP=0}VBqimzZ#vGQ zQWsFGZdHSIyQf(DH`q=s-+#ecCJ-G5?VJT|%lv7IBT}VaatdV=Y+j3h1Q!EIlIH|CCz{f=3B%$Gyl~J6*I3f?`1Q(>w62@JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob%N-}k2`)1xtZnoePe$BEdq5cF{39rhHtzYjmr@hYA%q%@Yu| z{JP2Dix9>`q1CGL*!YIAg)4O%tD_oUAy~;n35JMTt=13tmcAcO;P-`g1Mf@tJ$=5v UyKc+<=guVJ`0U!c!C#*J0)9%hQUCw| literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-8-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-8-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..1b60bb7f7c9f36e5d4e3b2037bc507b808074840 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82p05Tw>FbZMKlt&32KOwvg*l4QbUQgjTxi{S+90a5&7%z~!+##0_NJlAt0H@9hRy2c^ zx!UQ+6xLr*fa98?^F(n!95_z|mSO7hH4Sr5RoQCql+^woq(CN}OX4$9xpztmh5BPL zpUKod&GeLTwSyg8s4i_$L^8s$U>v#VHiDkFLeA1wEJR+o?RG>(T>1vIvi%iraFn#w zCE(Z)UQh;&LbhR|9R#ezPzwW;FeX_fV&qyKQ!?(ga;Tk4x{;YbfuZ~Y^GHg{>AdMW z`%0Zbt-4hWw$nSt+P}eeYx({Q)-r+UFlgr_Xj|q_Qyh^h^^#L4n;;LOJT!IQ2^*%`Lj~=A*^Pn}T9=H1%ea{usGX$;I&V!1HDwzL+2Z zpoWMke1uf{o7z-{3a7JE0?)vxDwYDjIu{&gMS_J=?ef~VHs#~!J)>7mK9s1yX`XyTXUj#583awU^$Hq6zEnKPlSRK{)3c*SqNH9dyYW;w3>HE(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ?_YuS#DLRMz5-O-Uyt7z8mje1k0qZB`ZQ)dY?n!(Im z?(|a%>n|w4aZTawDINxWXHQ@mrY?tRm^_WvLSGU;3rUy#bRb5bbOAB*`y zruKQJ=Y-20tl&a*X^SF~5sn4p$U}D#^t`p^EN#U?a7@t~Ds?OfB1%zO`q@(au(DJjQ&*KrP& zx`0}Bs~W7^J;mC;!FFo-{tMPJf#^7B=PYPj=1)@`kt+3)Qz)AtPom_+H1aHxJ)JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2<_SnzzH7=3 z;bJIUA!n7Z#%l$s8(3}C_zJ;F9!fC8()s~!=!fA1zOS?!7_Z@b`uqO=x-HjV+LMUm Ly=(IZf4TPyS3tAk literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-8-9.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-8-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..469cb3393075aa6e206313ef7b611c2e513e8faa GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|m_#cY3dLF%T-0pUE(MV?iIZeXk_nTEyNd{YfIem)q<1EX z-`kebKy>cRoO|xaxe4RN?Tw~sKi_EDJ9zKr?sSW=?~^=dJV(xQi@YKT2!EjD5jm!g zyk|k&AjpYOrq7a$r6d)L=Zqldt!1}#OJq99IW{^I zAyfN2({sYr4t8*^y0k?hNeRb-apa)85PIGSI7=Fl5Lxatn;{iZ;Th1%<~O{-Vcb-g zfMY{=P8l=`*@lUB5U>(MB@9r)n53bIkYlw?$#~Gpv39QMMrOVTL-_^fp_Ejs-Rrat zmAZghRjX=jyL*bYe}nB*^8FXAWdhN0(5_Ek5Rwa}}7*@F7c zqNJ8Mnyv}Vow);|%5UvK0uirCj&|WLjn3B*eHae~R;$us?Ha}wu2gL-k1BkLUx$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?ge|{r zGWH^X@las3svI`1VQk?_-Nx#u##abd@<4(ipjNB(1HPs2M^pHHq20jy5`Isg@9(bL Sa{swAi8wyHwr=p3XTJc9mbGF4 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-9-1.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-9-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..9ea21e3509d3cdea2abbf97aa592d9f5b01117fa GIT binary patch literal 1504 zcmb_c>uTFD6!tpy9!0;6<4YDA{~1}9LP`>MNmd3W4B1g^4cju3oVA3&9$*i-N7<2V z=iY6M8Jy@y`p$PQ-*KGIZ?6o)`1!^#Zs5I}1(OZJQAC7bTp<5(gTivPBK(1pZ^*Yz z6h2E5tX6_ZZAL6BSWa>|e=Z2}-#V^mqKK6_9Cu74^)8zAd!ybInJB|g;M7~disrDg z7hCg~!ubmda9mTgcN7nUzPBTAEZbDCZP|OO%GP_Qr0)M91v2Ga5ucH&y;D*s)E|ra zLZSyR<XIfeRGU;e}nDR^8FXAV*}A)(B4VVuF9WgI3`u zQVmLVwWYQdFvF5?iqi^PgnUdSPE#69Ss5oej3e@4POu<-mNI^eBi+r%M(FjwYC&^n zUQsI?&6fn`&fWr1?YHru1dDh>((HmexQo*w!DF_BxkkZ#@8hQdq_AXVn35S?k|pvb zFNqGHh@{TD;54I>u4@xZ$!S^B^||CY7b%unv``(#>{?wL9;cdnD3HKek%F$( z$4$j9BA5;h)~m{4;~J(GuGC$uj%s{`U?qx$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mQqP1oK literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-9-3.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-9-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..5f40c7b6e365db2f31169f8b1c45e43b392f27a6 GIT binary patch literal 1512 zcmb_c?T*qw6s>D~4>G@-GHsDa|0p7xumQRPZcGR<(=wDXZKs(|aW{m-2k=3C4DU=^ zzE_R0Nn!5HoO|xaxsBr4?Tuj=KVKQfJ9zJ={&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&CUfYgqqL3FU9Jfs*^)8zBy2I`SnJ6KT;M85filwl^ zi=BB&VgCgMIIbvKdy0pC&)pN)7BiiEX$}IPq&4|f$5m?a5=2x;IQQXv* zfMY{N&Nws*ITl7c2w01u5(X$?NYhBJk!QECVmxT&SUcBrqcY!vq5P7FNJ+|Vb=vNs zQWsFWYE_MGy*b4?zrnUE`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04v~$;Tv3eY{V|CQRE~g*Ll#?Wj zLEY*Tr!vSxm=WzluS&Ny0n9F3sd`x+RrnIY3lSOJq9M=@ip5kFR=qjven)>sr^4lflNA=#22J;@0=70^~Yj9 zlc{~4={ezQ2Rpb>UD~3EWQ1eEIC9Zl1U+wsoTaT;h`eyy?TCuF^bKfb`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`OtMJC$h90(G9I*YtetDRk(uwoP=0}VBqh~xdfm>U zQWsFGZdHSI-kf6X-(b77eE$V&nLuCe#u2$NC%7O3mN0&c1J%uCBlP-TwxGGQ zsHi25W@`d-XYPQg_S<-nK*Sr8qg}X*lRU-~wuZSz-hKb$ry-=UW~HByIbD-Aazlui zsQ5_{*NNwxrc}^PZDJuf^-CemExPySqs7RZf?{+u^=6a)7`af%#qjgM^JX8um>>b5 zhKMPAgjD;R+Ej)L$Jr@?XJAwnOMzdV3y!lQ!9uBa=@^dQGkPXGRyFugp#rCQ0>YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mUuC;0a literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-9-5.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-9-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..22909852b90854c08f146b388ed637922a7defdb GIT binary patch literal 1504 zcmb_c>uTFD6!tpy9!0;6V>=fb{~1}9LP{EUNmd3S4B1g^4cjuZoVJ9(9$*i-N7<2N z=iY6M8Jy@y`p$PQ-*J@8Zm$f(`1#H-Zs5I}`r{44VMvOC@dA1C4GPNDittBDo{(po zD0mUXU#$uvlo_%#XBo-F>?J42Yddb!L?J6PIPRE8s9iMe_eQ-5GEs`3!D({=E1JQ| zUTn={3g<5)uj!JNJcmoj3W=-MbPu>6=&&nEJR*-jYdR8Tm}}jvhfYCag;RF zCE!>SUQh;&Lat?_Ed;E@PzeK+Fd|tbV&pk3TQcsoa;Tk4x{;ahz)*gHc_by(Y;`-$ zeWlKzPSvU!+qyZ%y1&78D*65k*0F);FzDt<(5}p%rZ^%x^^#L4n;_4kG2`i&GgKq+FxSX`=zaP;fE1Rj3=%S>OR_|s z;3ZMvlOnG2&N)q~pzF%SLU0@FMZ#ieEe#I z0)QC;rtl3?-LFfd4HZpmtMr|Lp$nD*zd9EjXGMaA679Tgwau>8wc&B}+(Us1oaPDW zT7KMQ>_rIEp}=aT9oDX4YT-)NMSWD^HG-8qlwb(x2Yg3A49D>M#<+s_E&Luo-``)h S<^F4D5^;ETXyFYu6s~K04>G@-(hCylA4OynHb7UvjR_%UTBhxow$n_fxEn&^1NfjmhG(WN z_f=zTQkXL{-}%nvJB^aX?Tw~sKVNCuJ9zKr-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?z-gX< zyyZ7eW>5q$B+7+aRc;#s7+$ziH?umb@fCuVJdj`rtMvoEr|(Bo_!21$@PoM8^ UuiJ9}xig74KD)MV@Rw)50H-0gS^xk5 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/plane-9-7.glb b/Cesium3DTilesSelection/test/data/SharedImages/plane-9-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..77b088bfe57108c448c7760fed648c1872a09bc6 GIT binary patch literal 1512 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 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/texture1.png b/Cesium3DTilesSelection/test/data/SharedImages/texture1.png new file mode 100644 index 0000000000000000000000000000000000000000..9594525097353c3fefa248b0cc9732700e301425 GIT binary patch literal 57706 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 Y$@7zpj^cR82v(Y@vBf8uMvkGs0ipHazyJUM literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/SharedImages/tileset.json b/Cesium3DTilesSelection/test/data/SharedImages/tileset.json new file mode 100644 index 000000000..b7569c61b --- /dev/null +++ b/Cesium3DTilesSelection/test/data/SharedImages/tileset.json @@ -0,0 +1,2228 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4096, + "root": { + "boundingVolume": { + "box": [ + 5.4500005, + 0, + 5.4500005, + 5.450000286102295, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5.450000286102295 + ] + }, + "geometricError": 1024, + "children": [ + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-9.glb" + } + } + ], + "refine": "ADD" + } +} \ No newline at end of file diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 64873b2c6..a0f058d21 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -132,10 +132,11 @@ template class SharedAsset { /** * Move assignment operator for SharedAsset. */ - void operator=(SharedAsset&& other) { + void operator=(SharedAsset&& other) noexcept { if (*this != other) { this->maybeChangeCounter(-1); - this->contents = other.contents; + this->contents = std::move(other.contents); + other.contents = nullptr; } } @@ -153,7 +154,8 @@ template class SharedAsset { * Move constructor. */ SharedAsset(SharedAsset&& other) noexcept { - contents = other.contents; + contents = std::move(other.contents); + other.contents = nullptr; } private: @@ -195,7 +197,7 @@ template class SharedAsset { */ template class SingleAssetDepot { public: - SingleAssetDepot(); + SingleAssetDepot() {} /** * Stores the AssetType in this depot and returns a reference to it, @@ -216,7 +218,7 @@ template class SingleAssetDepot { } /** - * Fetches an asset that implements {@link FetchableAsset} and constructs it if possible. + * Fetches an asset that has a {@link AssetFactory} defined and constructs it if possible. * If the asset is already in this depot, it will be returned instead. * If the asset has already started loading in this depot but hasn't finished, * its future will be returned. @@ -280,7 +282,7 @@ template class SingleAssetDepot { } return std::optional>( - SharedAsset(&it->second)); + std::move(SharedAsset(&it->second))); } return std::optional>(); @@ -297,6 +299,8 @@ template class SingleAssetDepot { return it->second; } + size_t getDistinctCount() const { return this->assets.size(); } + private: /** * Removes the given asset from the depot. @@ -320,7 +324,7 @@ template class SingleAssetDepot { */ class SharedAssetDepot { public: - SharedAsset store(CesiumGltf::ImageCesium& image); + SharedAssetDepot() {} /** * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. @@ -342,6 +346,8 @@ class SharedAssetDepot { headers); } + size_t getImagesCount() const { return this->images.getDistinctCount(); } + private: SingleAssetDepot images; }; @@ -349,6 +355,7 @@ class SharedAssetDepot { } // namespace SharedAssetDepotInternals // actually export the public types to the right namespace +// fairly sure this is anti-pattern actually but i'll fix it later using SharedAssetDepotInternals::AssetFactory; using SharedAssetDepotInternals::SharedAsset; using SharedAssetDepotInternals::SharedAssetDepot; diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index 527800ad2..28dc2394e 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -110,8 +110,8 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * appear in this glTF. If not present, assets will not be shared between * glTFs, even if they're loaded from the same URL. */ - std::variant sharedAssets = - std::monostate(); + std::variant> + sharedAssets = std::monostate(); }; /** diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 99dd4baf4..a31c1bba9 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -582,7 +582,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( } SharedFuture>> - operator()(SharedAssetDepot* depot) { + operator()(std::shared_ptr depot) { // We have a depot, this is easy! return depot->getOrFetch( asyncSystem, From 8d2c5e1e6100887b9ddeea6361a094a2784e868a Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 23 Jul 2024 14:15:39 -0400 Subject: [PATCH 03/81] clang-format --- CesiumGltf/include/CesiumGltf/Image.h | 1 - 1 file changed, 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index ee50450b4..0e755d365 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -6,7 +6,6 @@ #include - namespace CesiumGltf { /** @copydoc ImageSpec */ struct CESIUMGLTF_API Image final : public ImageSpec { From 9bf3ac430762e326b0eb9b9635603bc9b2cb279c Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 23 Jul 2024 14:24:00 -0400 Subject: [PATCH 04/81] Fix unnecessary reference --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index a0f058d21..d8d365a2d 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -274,7 +274,7 @@ template class SingleAssetDepot { pPendingAssets->erase(idHash); if (result.has_value()) { - auto& [it, ok] = pAssets->emplace( + auto [it, ok] = pAssets->emplace( idHash, AssetContainer(idHash, result.value(), pThiz)); if (!ok) { From 2f622d78926086bb542b394a8a27b96055f9eba5 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 23 Jul 2024 14:25:53 -0400 Subject: [PATCH 05/81] Unused param --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index d8d365a2d..5020eb8f4 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -176,7 +176,7 @@ template class SharedAsset { } } - void operator()(AssetType& asset) {} + void operator()([[maybe_unused]] AssetType& asset) {} }; std::visit(Operation{amt}, this->contents); From 4e5bff4dfb30d28f0de2265d4550a04c075f3608 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 16 Aug 2024 14:10:08 -0400 Subject: [PATCH 06/81] Changes for Unreal implementation --- .../include/Cesium3DTilesSelection/Tileset.h | 8 ++++++++ Cesium3DTilesSelection/src/Tileset.cpp | 9 +++++++++ CesiumGltf/include/CesiumGltf/ImageCesium.h | 3 ++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index fea035987..306a440a8 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -177,6 +177,14 @@ class CESIUM3DTILESSELECTION_API Tileset final { /** @copydoc Tileset::getOverlays() */ const RasterOverlayCollection& getOverlays() const noexcept; + /** + * @brief Returns the {@link SharedAssetDepot} of this tileset. + */ + CesiumGltf::SharedAssetDepot& getSharedAssetDepot() noexcept; + + /** @copydoc Tileset::getSharedAssetDepot() */ + const CesiumGltf::SharedAssetDepot& getSharedAssetDepot() const noexcept; + /** * @brief Updates this view but waits for all tiles that meet sse to finish * loading and ready to be rendered before returning the function. This method diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 05954d672..ccc7729c7 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -134,6 +134,15 @@ const RasterOverlayCollection& Tileset::getOverlays() const noexcept { return this->_pTilesetContentManager->getRasterOverlayCollection(); } +CesiumGltf::SharedAssetDepot& Tileset::getSharedAssetDepot() noexcept { + return *this->_pTilesetContentManager->getSharedAssetDepot(); +} + +const CesiumGltf::SharedAssetDepot& +Tileset::getSharedAssetDepot() const noexcept { + return *this->_pTilesetContentManager->getSharedAssetDepot(); +} + static bool operator<(const FogDensityAtHeight& fogDensity, double height) noexcept { return fogDensity.cameraHeight < height; diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index 939ada0ae..1cfca8f70 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -2,6 +2,7 @@ #include "CesiumGltf/Ktx2TranscodeTargets.h" #include "CesiumGltf/Library.h" +#include "CesiumUtility/ExtensibleObject.h" #include #include @@ -29,7 +30,7 @@ struct CESIUMGLTF_API ImageCesiumMipPosition { * @brief Holds {@link Image} properties that are specific to the glTF loader * rather than part of the glTF spec. */ -struct CESIUMGLTF_API ImageCesium final { +struct CESIUMGLTF_API ImageCesium final : public CesiumUtility::ExtensibleObject { /** * @brief The width of the image in pixels. */ From 34ec18de941218a0ce457feef747573e57deed51 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 16 Aug 2024 14:40:18 -0400 Subject: [PATCH 07/81] Revert adding extern folder back --- extern/CSPRNG | 1 - extern/Catch2 | 1 - extern/GSL | 1 - extern/KTX-Software | 1 - extern/PicoSHA2 | 1 - extern/asyncplusplus | 1 - extern/cmake-modules | 1 - extern/cpp-httplib | 1 - extern/draco | 1 - extern/earcut | 1 - extern/expected-lite | 1 - extern/glm | 1 - extern/include/catch2/catch.hpp | 3 --- extern/libjpeg-turbo | 1 - extern/libmorton | 1 - extern/libwebp | 1 - extern/meshoptimizer | 1 - extern/rapidjson | 1 - extern/s2geometry | 1 - extern/spdlog | 1 - extern/stb | 1 - extern/tinyxml2 | 1 - extern/uriparser | 1 - extern/zlib | 1 - 24 files changed, 26 deletions(-) delete mode 160000 extern/CSPRNG delete mode 160000 extern/Catch2 delete mode 160000 extern/GSL delete mode 160000 extern/KTX-Software delete mode 160000 extern/PicoSHA2 delete mode 160000 extern/asyncplusplus delete mode 160000 extern/cmake-modules delete mode 160000 extern/cpp-httplib delete mode 160000 extern/draco delete mode 160000 extern/earcut delete mode 160000 extern/expected-lite delete mode 160000 extern/glm delete mode 100644 extern/include/catch2/catch.hpp delete mode 160000 extern/libjpeg-turbo delete mode 160000 extern/libmorton delete mode 160000 extern/libwebp delete mode 160000 extern/meshoptimizer delete mode 160000 extern/rapidjson delete mode 160000 extern/s2geometry delete mode 160000 extern/spdlog delete mode 160000 extern/stb delete mode 160000 extern/tinyxml2 delete mode 160000 extern/uriparser delete mode 160000 extern/zlib diff --git a/extern/CSPRNG b/extern/CSPRNG deleted file mode 160000 index 8768a94b4..000000000 --- a/extern/CSPRNG +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8768a94b4b04213c0798b80824a04ae4990e9847 diff --git a/extern/Catch2 b/extern/Catch2 deleted file mode 160000 index 62fd66058..000000000 --- a/extern/Catch2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 62fd660583d3ae7a7886930b413c3c570e89786c diff --git a/extern/GSL b/extern/GSL deleted file mode 160000 index ef0ffefe5..000000000 --- a/extern/GSL +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ef0ffefe525a6219ff245d19a832ce06f3fd3504 diff --git a/extern/KTX-Software b/extern/KTX-Software deleted file mode 160000 index dd389bdfd..000000000 --- a/extern/KTX-Software +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dd389bdfd05343ddcd374614c71de66849bd3df4 diff --git a/extern/PicoSHA2 b/extern/PicoSHA2 deleted file mode 160000 index 1677374f2..000000000 --- a/extern/PicoSHA2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1677374f23352716fc52183255a40c1b8e1d53eb diff --git a/extern/asyncplusplus b/extern/asyncplusplus deleted file mode 160000 index 756893feb..000000000 --- a/extern/asyncplusplus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 756893feb9e69c098225d5a454a668a2c139e4be diff --git a/extern/cmake-modules b/extern/cmake-modules deleted file mode 160000 index d6d1a778e..000000000 --- a/extern/cmake-modules +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d6d1a778e41cb114e5cdf279b8a659fa0ce9a0d4 diff --git a/extern/cpp-httplib b/extern/cpp-httplib deleted file mode 160000 index d73395e1d..000000000 --- a/extern/cpp-httplib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d73395e1dc652465fa9524266cd26ad57365491f diff --git a/extern/draco b/extern/draco deleted file mode 160000 index 878674008..000000000 --- a/extern/draco +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8786740086a9f4d83f44aa83badfbea4dce7a1b5 diff --git a/extern/earcut b/extern/earcut deleted file mode 160000 index b28acde13..000000000 --- a/extern/earcut +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b28acde132cdb8e0ef536a96ca7ada8a651f9169 diff --git a/extern/expected-lite b/extern/expected-lite deleted file mode 160000 index 95b9cb015..000000000 --- a/extern/expected-lite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 95b9cb015fa17baa749c2b396b335906e1596a9e diff --git a/extern/glm b/extern/glm deleted file mode 160000 index bf71a8349..000000000 --- a/extern/glm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bf71a834948186f4097caa076cd2663c69a10e1e diff --git a/extern/include/catch2/catch.hpp b/extern/include/catch2/catch.hpp deleted file mode 100644 index a8539ae49..000000000 --- a/extern/include/catch2/catch.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once -#include -using Catch::Approx; diff --git a/extern/libjpeg-turbo b/extern/libjpeg-turbo deleted file mode 160000 index 68103a1f2..000000000 --- a/extern/libjpeg-turbo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 68103a1f2830b839772d2fffcb6d3daead3355be diff --git a/extern/libmorton b/extern/libmorton deleted file mode 160000 index 234a443ca..000000000 --- a/extern/libmorton +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 234a443ca8e2c64f6385f1a9d6ee10a70d08a3fa diff --git a/extern/libwebp b/extern/libwebp deleted file mode 160000 index 0d1f12546..000000000 --- a/extern/libwebp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0d1f12546bd803099a60c070517a552483f3790e diff --git a/extern/meshoptimizer b/extern/meshoptimizer deleted file mode 160000 index fbe26ff91..000000000 --- a/extern/meshoptimizer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fbe26ff91317bd89c67c3890e038965d55f3e354 diff --git a/extern/rapidjson b/extern/rapidjson deleted file mode 160000 index fcb23c2db..000000000 --- a/extern/rapidjson +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fcb23c2dbf561ec0798529be4f66394d3e4996d8 diff --git a/extern/s2geometry b/extern/s2geometry deleted file mode 160000 index 4ad31e9b9..000000000 --- a/extern/s2geometry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4ad31e9b9d4b25f0ddffa2061ac5a7d14edf7195 diff --git a/extern/spdlog b/extern/spdlog deleted file mode 160000 index 76fb40d95..000000000 --- a/extern/spdlog +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 76fb40d95455f249bd70824ecfcae7a8f0930fa3 diff --git a/extern/stb b/extern/stb deleted file mode 160000 index af1a5bc35..000000000 --- a/extern/stb +++ /dev/null @@ -1 +0,0 @@ -Subproject commit af1a5bc352164740c1cc1354942b1c6b72eacb8a diff --git a/extern/tinyxml2 b/extern/tinyxml2 deleted file mode 160000 index e05956094..000000000 --- a/extern/tinyxml2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e05956094c27117f989d22f25b75633123d72a83 diff --git a/extern/uriparser b/extern/uriparser deleted file mode 160000 index 24df44b74..000000000 --- a/extern/uriparser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 24df44b74753017acfaec4b3a30097a8a2ae1ae1 diff --git a/extern/zlib b/extern/zlib deleted file mode 160000 index 04f42ceca..000000000 --- a/extern/zlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 04f42ceca40f73e2978b50e93806c2a18c1281fc From 8ae21507f72045bdb46efb278926d52c3f9cb5e2 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 16 Aug 2024 14:41:04 -0400 Subject: [PATCH 08/81] Narrowing in on the correct set of extern files --- extern/cmake-modules | 1 + extern/include/catch2/catch.hpp | 3 +++ 2 files changed, 4 insertions(+) create mode 160000 extern/cmake-modules create mode 100644 extern/include/catch2/catch.hpp diff --git a/extern/cmake-modules b/extern/cmake-modules new file mode 160000 index 000000000..d6d1a778e --- /dev/null +++ b/extern/cmake-modules @@ -0,0 +1 @@ +Subproject commit d6d1a778e41cb114e5cdf279b8a659fa0ce9a0d4 diff --git a/extern/include/catch2/catch.hpp b/extern/include/catch2/catch.hpp new file mode 100644 index 000000000..a8539ae49 --- /dev/null +++ b/extern/include/catch2/catch.hpp @@ -0,0 +1,3 @@ +#pragma once +#include +using Catch::Approx; From 0018493071c837977ad9ecbf5ed06cf7958608a4 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 16 Aug 2024 15:07:30 -0400 Subject: [PATCH 09/81] Fix GltfReader I broke in merge --- CesiumGltfReader/src/GltfReader.cpp | 80 +++++++++++++++-------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 22a0ff9eb..04ab00cbe 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -560,29 +560,31 @@ void CesiumGltfReader::GltfReader::postprocessGltf( std::string uri; std::vector headers; - SharedFuture>> - operator()(std::monostate) { - // We don't have a depot, we have to fetch this the old way. - return pAssetAccessor->get(asyncSystem, uri, headers) - .thenInWorkerThread( - [uri = this->uri, pFactory = &this->factory]( - std::shared_ptr&& pRequest) { - const IAssetResponse* pResponse = pRequest->response(); - - if (pResponse) { - gsl::span bytes = pResponse->data(); - auto asset = pFactory->createFrom(bytes); - if (asset.has_value()) { - return std::optional>(asset.value()); - } - - return std::optional>(); - }) - .share(); - } - - SharedFuture>> - operator()(std::shared_ptr depot) { + SharedFuture>> + operator()(std::monostate) { + // We don't have a depot, we have to fetch this the old way. + return pAssetAccessor->get(asyncSystem, uri, headers) + .thenInWorkerThread( + [uri = this->uri, pFactory = &this->factory]( + std::shared_ptr&& pRequest) { + const IAssetResponse* pResponse = pRequest->response(); + + if (pResponse) { + gsl::span bytes = pResponse->data(); + auto asset = pFactory->createFrom(bytes); + if (asset.has_value()) { + return std::optional>( + asset.value()); + } + } + + return std::optional>(); + }) + .share(); + } + + SharedFuture>> + operator()(std::shared_ptr depot) { // We have a depot, this is easy! return depot->getOrFetch( asyncSystem, @@ -590,8 +592,8 @@ void CesiumGltfReader::GltfReader::postprocessGltf( factory, uri, headers); - } - }; + } + }; SharedFuture>> future = std::visit( @@ -616,21 +618,21 @@ void CesiumGltfReader::GltfReader::postprocessGltf( return ExternalBufferLoadResult{false, imageUri}; })); - } } } + } - return asyncSystem.all(std::move(resolvedBuffers)) - .thenInWorkerThread( - [pResult = std::move(pResult)]( - std::vector&& loadResults) mutable { - for (auto& bufferResult : loadResults) { - if (!bufferResult.success) { - pResult->warnings.push_back( - "Could not load the external gltf buffer: " + - bufferResult.bufferUri); - } + return asyncSystem.all(std::move(resolvedBuffers)) + .thenInWorkerThread( + [pResult = std::move(pResult)]( + std::vector&& loadResults) mutable { + for (auto& bufferResult : loadResults) { + if (!bufferResult.success) { + pResult->warnings.push_back( + "Could not load the external gltf buffer: " + + bufferResult.bufferUri); } - return std::move(*pResult.release()); - }); - } + } + return std::move(*pResult.release()); + }); +} From 7e36d6c45f6c9bacccb73e079f38ecc2dbfd32cc Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 23 Aug 2024 14:31:08 -0400 Subject: [PATCH 10/81] Refactor to use IntrusivePointer for AssetContainer --- .../src/TilesetContentManager.cpp | 4 +- .../CesiumGltf/PropertyTexturePropertyView.h | 4 +- .../include/CesiumGltf/PropertyTextureView.h | 6 +- .../include/CesiumGltf/SharedAssetDepot.h | 95 ++++++++----------- CesiumGltf/include/CesiumGltf/TextureView.h | 2 +- CesiumGltf/src/TextureView.cpp | 2 +- CesiumGltfReader/src/decodeDataUrls.cpp | 3 +- 7 files changed, 51 insertions(+), 65 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 42d26ab44..3d58b6738 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -608,12 +608,12 @@ postProcessContentInWorkerThread( "Warning when resolving external gltf buffers from " "{}:\n- {}", result.pCompletedRequest->url(), - CesiumUtility::joinToString(gltfResult.errors, "\n- ")); + CesiumUtility::joinToString(gltfResult.warnings, "\n- ")); } else { SPDLOG_LOGGER_ERROR( tileLoadInfo.pLogger, "Warning resolving external glTF buffers:\n- {}", - CesiumUtility::joinToString(gltfResult.errors, "\n- ")); + CesiumUtility::joinToString(gltfResult.warnings, "\n- ")); } } diff --git a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h index f3dcd168e..fd96b3602 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h @@ -298,7 +298,7 @@ class PropertyTexturePropertyView const PropertyTextureProperty& property, const ClassProperty& classProperty, const Sampler& sampler, - const ImageCesium& image, + const SharedAsset& image, const TextureViewOptions& options = TextureViewOptions()) noexcept : PropertyView(classProperty, property), TextureView( @@ -531,7 +531,7 @@ class PropertyTexturePropertyView const PropertyTextureProperty& property, const ClassProperty& classProperty, const Sampler& sampler, - const ImageCesium& image, + const SharedAsset& image, const TextureViewOptions& options = TextureViewOptions()) noexcept : PropertyView(classProperty, property), TextureView( diff --git a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h index 720e4d839..d0b01c656 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h @@ -748,15 +748,15 @@ class PropertyTextureView { return PropertyTexturePropertyView(status); } - const ImageCesium& image = *_pModel->images[imageIndex].cesium; + const SharedAsset& image = _pModel->images[imageIndex].cesium; const std::vector& channels = propertyTextureProperty.channels; - status = checkChannels(channels, image); + status = checkChannels(channels, *image); if (status != PropertyTexturePropertyViewStatus::Valid) { return PropertyTexturePropertyView(status); } - if (channels.size() * image.bytesPerChannel != elementSize) { + if (channels.size() * image->bytesPerChannel != elementSize) { return PropertyTexturePropertyViewStatus::ErrorChannelsAndTypeMismatch; } diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 5020eb8f4..0ce548c40 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include @@ -39,7 +41,9 @@ template class AssetFactory { /** * Contains the current state of an asset within the SharedAssetDepot. */ -template class AssetContainer { +template +class AssetContainer + : public CesiumUtility::ReferenceCounted, true> { public: uint32_t counter; IdHash assetId; @@ -64,36 +68,16 @@ template class AssetContainer { * @tparam AssetType The type of the asset that we're getting a reference to. */ template class SharedAsset { - typedef std::variant*, AssetType> AssetContents; - public: - SharedAsset(const AssetType& asset) : contents(asset) {} + SharedAsset(AssetType& asset) + : contents(new AssetContainer(0, asset, nullptr)) {} SharedAsset(std::nullptr_t) : contents(nullptr) {} - SharedAsset() : contents(AssetType()) {} - - AssetType* get() { - struct Operation { - AssetType* operator()(AssetType& asset) { return &asset; } - - AssetType* operator()(AssetContainer* container) { - return &container->asset; - } - }; + SharedAsset() + : contents(new AssetContainer(0, AssetType(), nullptr)) {} - return std::visit(Operation{}, this->contents); - } - - const AssetType* get() const { - struct Operation { - const AssetType* operator()(const AssetType& asset) { return &asset; } + AssetType* get() { return &this->contents->asset; } - const AssetType* operator()(const AssetContainer* container) { - return &container->asset; - } - }; - - return std::visit(Operation{}, this->contents); - } + const AssetType* get() const { return &this->contents->asset; } AssetType* operator->() { return this->get(); } const AssetType* operator->() const { return this->get(); } @@ -122,10 +106,10 @@ template class SharedAsset { void operator=(const SharedAsset& other) { if (*this != other) { // Decrement this reference - this->maybeChangeCounter(-1); + this->changeCounter(-1); this->contents = other.contents; // Increment the new reference - this->maybeChangeCounter(1); + this->changeCounter(1); } } @@ -134,20 +118,20 @@ template class SharedAsset { */ void operator=(SharedAsset&& other) noexcept { if (*this != other) { - this->maybeChangeCounter(-1); + this->changeCounter(-1); this->contents = std::move(other.contents); other.contents = nullptr; } } - ~SharedAsset() { this->maybeChangeCounter(-1); } + ~SharedAsset() { this->changeCounter(-1); } /** * Copy constructor. */ SharedAsset(const SharedAsset& other) { contents = other.contents; - this->maybeChangeCounter(1); + this->changeCounter(1); } /** @@ -159,32 +143,27 @@ template class SharedAsset { } private: - SharedAsset(AssetContainer* container) : contents(container) { - this->maybeChangeCounter(1); + SharedAsset( + CesiumUtility::IntrusivePointer> container) + : contents(container) { + this->changeCounter(1); } - void maybeChangeCounter(int amt) { - struct Operation { - int amt; - - void operator()(AssetContainer* asset) { - if (asset != nullptr) { - asset->counter += this->amt; - if (asset->counter <= 0) { - asset->parent->remove(asset->assetId); - } + void changeCounter(int amt) { + if (contents != nullptr) { + contents->counter += amt; + if (contents->counter <= 0) { + if (contents->parent != nullptr) { + contents->parent->remove(contents->assetId); } + contents = nullptr; } - - void operator()([[maybe_unused]] AssetType& asset) {} - }; - - std::visit(Operation{amt}, this->contents); + } } // A SharedAssetRef might point to an asset the SharedAssetDepot, or it // might not. If it doesn't, we own this asset now. - AssetContents contents; + CesiumUtility::IntrusivePointer> contents; friend class SharedAssetDepot; friend class AssetContainer; @@ -211,7 +190,7 @@ template class SingleAssetDepot { } auto [newIt, ok] = - this->assets.emplace(assetId, AssetContainer(assetId, asset, this)); + this->assets.emplace(assetId, new AssetContainer(assetId, asset, this)); CESIUM_ASSERT(ok); return newIt->second.toRef(); @@ -239,7 +218,7 @@ template class SingleAssetDepot { // We've already loaded an asset with this ID - we can just use that. return asyncSystem .createResolvedFuture(std::optional>( - SharedAsset(&existingIt->second))) + SharedAsset(existingIt->second))) .share(); } @@ -276,13 +255,16 @@ template class SingleAssetDepot { if (result.has_value()) { auto [it, ok] = pAssets->emplace( idHash, - AssetContainer(idHash, result.value(), pThiz)); + new AssetContainer( + idHash, + result.value(), + pThiz)); if (!ok) { return std::optional>(); } return std::optional>( - std::move(SharedAsset(&it->second))); + std::move(SharedAsset(it->second))); } return std::optional>(); @@ -309,7 +291,10 @@ template class SingleAssetDepot { void remove(IdHash hash) { this->assets.erase(hash); } // Assets that have a unique ID that can be used to de-duplicate them. - std::unordered_map> assets; + std::unordered_map< + IdHash, + CesiumUtility::IntrusivePointer>> + assets; // Futures for assets that still aren't loaded yet. std::unordered_map< IdHash, diff --git a/CesiumGltf/include/CesiumGltf/TextureView.h b/CesiumGltf/include/CesiumGltf/TextureView.h index 401149d93..3715401ea 100644 --- a/CesiumGltf/include/CesiumGltf/TextureView.h +++ b/CesiumGltf/include/CesiumGltf/TextureView.h @@ -125,7 +125,7 @@ class TextureView { */ TextureView( const Sampler& sampler, - const ImageCesium& image, + const SharedAsset& image, int64_t textureCoordinateSetIndex, const ExtensionKhrTextureTransform* pKhrTextureTransformExtension = nullptr, diff --git a/CesiumGltf/src/TextureView.cpp b/CesiumGltf/src/TextureView.cpp index 1ddeba939..ce7901b9c 100644 --- a/CesiumGltf/src/TextureView.cpp +++ b/CesiumGltf/src/TextureView.cpp @@ -79,7 +79,7 @@ TextureView::TextureView( TextureView::TextureView( const Sampler& sampler, - const ImageCesium& image, + const SharedAsset& image, int64_t textureCoordinateSetIndex, const ExtensionKhrTextureTransform* pKhrTextureTransformExtension, const TextureViewOptions& options) noexcept diff --git a/CesiumGltfReader/src/decodeDataUrls.cpp b/CesiumGltfReader/src/decodeDataUrls.cpp index d81a68cd3..764a9e076 100644 --- a/CesiumGltfReader/src/decodeDataUrls.cpp +++ b/CesiumGltfReader/src/decodeDataUrls.cpp @@ -141,7 +141,8 @@ void decodeDataUrls( continue; } - image.cesium = std::move(imageResult.image.value()); + CesiumGltf::ImageCesium& imageCesium = imageResult.image.value(); + image.cesium = imageCesium; if (options.clearDecodedDataUrls) { image.uri.reset(); From 2f852d0b9fab5553960a09f2f18e307ef172622c Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 23 Aug 2024 14:37:45 -0400 Subject: [PATCH 11/81] Re-add thread safety that I accidentally reverted --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 0ce548c40..0b14aeb74 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -45,7 +45,7 @@ template class AssetContainer : public CesiumUtility::ReferenceCounted, true> { public: - uint32_t counter; + std::atomic counter; IdHash assetId; AssetType asset; @@ -213,6 +213,14 @@ template class SingleAssetDepot { std::vector headers) { IdHash idHash = std::hash{}(uri); + // We need to avoid: + // - Two assets starting loading before the first asset has updated the + // pendingAssets map + // - An asset starting to load after the previous load has been removed from + // the pendingAssets map, but before the completed asset has been added to + // the assets map. + std::lock_guard lock(pendingAssetsMutex); + auto existingIt = this->assets.find(idHash); if (existingIt != this->assets.end()) { // We've already loaded an asset with this ID - we can just use that. @@ -247,8 +255,11 @@ template class SingleAssetDepot { .thenInMainThread([idHash, pThiz = this, pAssets = &this->assets, + pPendingAssetsMutex = &this->pendingAssetsMutex, pPendingAssets = &this->pendingAssets]( std::optional& result) { + std::lock_guard lock(*pPendingAssetsMutex); + // Get rid of our future. pPendingAssets->erase(idHash); @@ -300,6 +311,8 @@ template class SingleAssetDepot { IdHash, CesiumAsync::SharedFuture>>> pendingAssets; + // Mutex for checking or editing the pendingAssets map + std::mutex pendingAssetsMutex; friend class SharedAsset; }; From a5829effbcf1f7172de1325af5b0aef5f8047643 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 3 Sep 2024 11:28:02 -0400 Subject: [PATCH 12/81] Fix missing std::move --- CesiumAsync/include/CesiumAsync/AsyncSystem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumAsync/include/CesiumAsync/AsyncSystem.h b/CesiumAsync/include/CesiumAsync/AsyncSystem.h index 76f9d5696..0edd7e38f 100644 --- a/CesiumAsync/include/CesiumAsync/AsyncSystem.h +++ b/CesiumAsync/include/CesiumAsync/AsyncSystem.h @@ -315,7 +315,7 @@ class CESIUMASYNC_API AsyncSystem final { results.reserve(tasks.size()); for (auto it = tasks.begin(); it != tasks.end(); ++it) { - results.emplace_back(it->get()); + results.emplace_back(std::move(it->get())); } return results; }); From 96cd924630f8073b27c1daa067a5e436f220492a Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 11 Sep 2024 14:53:52 -0400 Subject: [PATCH 13/81] Update based on review --- .../src/TilesetContentManager.cpp | 5 - .../include/CesiumGeospatial/S2CellID.h | 1 + .../include/CesiumGltf/SharedAssetDepot.h | 128 ++++++++---------- .../include/CesiumGltfContent/GltfUtilities.h | 1 + CesiumGltfReader/src/GltfReader.cpp | 2 +- .../include/CesiumJsonReader/IJsonHandler.h | 1 + .../CesiumJsonReader/IgnoreValueJsonHandler.h | 1 + .../include/CesiumJsonWriter/JsonWriter.h | 1 + 8 files changed, 63 insertions(+), 77 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 3d58b6738..a4f0950af 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -53,11 +53,6 @@ struct ContentKindSetter { // size now. We'll be adding this number to our total memory usage soon, // and remove it when the tile is later unloaded, and we must use // the same size in each case. - - // TODO: this number will be wrong once we deduplicate images! it'll log - // the image's size for each time it's used, instead of only once as it - // should. if you are reading this comment in a code review, i forgot to - // fix this and you should remind me. if (image.cesium->sizeBytes < 0) { image.cesium->sizeBytes = int64_t(image.cesium->pixelData.size()); } diff --git a/CesiumGeospatial/include/CesiumGeospatial/S2CellID.h b/CesiumGeospatial/include/CesiumGeospatial/S2CellID.h index c98a092c9..fe63b58b5 100644 --- a/CesiumGeospatial/include/CesiumGeospatial/S2CellID.h +++ b/CesiumGeospatial/include/CesiumGeospatial/S2CellID.h @@ -6,6 +6,7 @@ #include #include +#include #include namespace CesiumGeometry { diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 0b14aeb74..963de417d 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -22,7 +23,6 @@ namespace CesiumGltf { namespace SharedAssetDepotInternals { -typedef size_t IdHash; template class SharedAsset; template class SingleAssetDepot; @@ -46,14 +46,14 @@ class AssetContainer : public CesiumUtility::ReferenceCounted, true> { public: std::atomic counter; - IdHash assetId; + std::string assetId; AssetType asset; // Pointer to this container's parent so we know how to clean it up. SingleAssetDepot* parent; AssetContainer( - IdHash assetId_, + std::string assetId_, AssetType& asset_, SingleAssetDepot* parent_) : counter(0), assetId(assetId_), asset(asset_), parent(parent_) {} @@ -70,10 +70,14 @@ class AssetContainer template class SharedAsset { public: SharedAsset(AssetType& asset) - : contents(new AssetContainer(0, asset, nullptr)) {} + : contents(new AssetContainer(std::string(), asset, nullptr)) { + } SharedAsset(std::nullptr_t) : contents(nullptr) {} SharedAsset() - : contents(new AssetContainer(0, AssetType(), nullptr)) {} + : contents(new AssetContainer( + std::string(), + AssetType(), + nullptr)) {} AssetType* get() { return &this->contents->asset; } @@ -176,23 +180,14 @@ template class SharedAsset { */ template class SingleAssetDepot { public: - SingleAssetDepot() {} - /** * Stores the AssetType in this depot and returns a reference to it, * or returns the existing asset if present. */ - SharedAsset store(IdHash assetId, AssetType& asset) { - auto it = this->assets.find(assetId); - if (it != this->assets.end()) { - // already stored - return it->second.toRef(); - } - - auto [newIt, ok] = - this->assets.emplace(assetId, new AssetContainer(assetId, asset, this)); - CESIUM_ASSERT(ok); - + SharedAsset store(std::string& assetId, AssetType&& asset) { + auto [newIt, added] = this->assets.try_emplace( + assetId, + new AssetContainer(assetId, std::move(asset), this)); return newIt->second.toRef(); } @@ -202,17 +197,12 @@ template class SingleAssetDepot { * If the asset has already started loading in this depot but hasn't finished, * its future will be returned. */ - template CesiumAsync::SharedFuture>> getOrFetch( CesiumAsync::AsyncSystem& asyncSystem, std::shared_ptr& pAssetAccessor, - typename std::enable_if< - std::is_base_of_v, Factory>, - const Factory&>::type factory, + const AssetFactory& factory, std::string& uri, std::vector headers) { - IdHash idHash = std::hash{}(uri); - // We need to avoid: // - Two assets starting loading before the first asset has updated the // pendingAssets map @@ -221,7 +211,7 @@ template class SingleAssetDepot { // the assets map. std::lock_guard lock(pendingAssetsMutex); - auto existingIt = this->assets.find(idHash); + auto existingIt = this->assets.find(uri); if (existingIt != this->assets.end()) { // We've already loaded an asset with this ID - we can just use that. return asyncSystem @@ -230,7 +220,7 @@ template class SingleAssetDepot { .share(); } - auto pendingIt = this->pendingAssets.find(idHash); + auto pendingIt = this->pendingAssets.find(uri); if (pendingIt != this->pendingAssets.end()) { // Return the existing future - the caller can chain off of it. return pendingIt->second; @@ -241,48 +231,51 @@ template class SingleAssetDepot { CesiumAsync::Future>> future = pAssetAccessor->get(asyncSystem, uri, headers) .thenInWorkerThread( - [factory]( - std::shared_ptr&& pRequest) { + [&factory]( + std::shared_ptr&& pRequest) + -> std::optional { const CesiumAsync::IAssetResponse* pResponse = pRequest->response(); if (!pResponse) { - return std::optional(std::nullopt); + return std::nullopt; } return factory.createFrom(pResponse->data()); }) // Do this in main thread since we're messing with the collections. - .thenInMainThread([idHash, - pThiz = this, - pAssets = &this->assets, - pPendingAssetsMutex = &this->pendingAssetsMutex, - pPendingAssets = &this->pendingAssets]( - std::optional& result) { - std::lock_guard lock(*pPendingAssetsMutex); - - // Get rid of our future. - pPendingAssets->erase(idHash); - - if (result.has_value()) { - auto [it, ok] = pAssets->emplace( - idHash, - new AssetContainer( - idHash, - result.value(), - pThiz)); - if (!ok) { - return std::optional>(); - } - - return std::optional>( - std::move(SharedAsset(it->second))); - } - - return std::optional>(); - }); + .thenInMainThread( + [uri, + this, + pAssets = &this->assets, + pPendingAssetsMutex = &this->pendingAssetsMutex, + pPendingAssets = + &this->pendingAssets](std::optional& result) + -> std::optional> { + std::lock_guard lock(*pPendingAssetsMutex); + + // Get rid of our future. + pPendingAssets->erase(uri); + + if (result.has_value()) { + auto [it, ok] = pAssets->emplace( + uri, + new AssetContainer( + uri, + std::move(result.value()), + this)); + if (!ok) { + return std::nullopt; + } + + return std::optional( + std::move(SharedAsset(it->second))); + } + + return std::nullopt; + }); auto& [it, ok] = - this->pendingAssets.emplace(idHash, std::move(future).share()); + this->pendingAssets.emplace(uri, std::move(future).share()); if (!ok) { return asyncSystem .createResolvedFuture(std::optional>()) @@ -299,16 +292,16 @@ template class SingleAssetDepot { * Removes the given asset from the depot. * Should only be called by {@link SharedAsset}. */ - void remove(IdHash hash) { this->assets.erase(hash); } + void remove(std::string& hash) { this->assets.erase(hash); } // Assets that have a unique ID that can be used to de-duplicate them. std::unordered_map< - IdHash, + std::string, CesiumUtility::IntrusivePointer>> assets; // Futures for assets that still aren't loaded yet. std::unordered_map< - IdHash, + std::string, CesiumAsync::SharedFuture>>> pendingAssets; // Mutex for checking or editing the pendingAssets map @@ -327,21 +320,14 @@ class SharedAssetDepot { /** * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. */ - template CesiumAsync::SharedFuture>> getOrFetch( CesiumAsync::AsyncSystem& asyncSystem, std::shared_ptr& pAssetAccessor, - typename std::enable_if< - std::is_base_of_v, Factory>, - const Factory&>::type factory, + const AssetFactory& factory, std::string& uri, std::vector headers) { - return images.getOrFetch( - asyncSystem, - pAssetAccessor, - factory, - uri, - headers); + return images + .getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); } size_t getImagesCount() const { return this->images.getDistinctCount(); } diff --git a/CesiumGltfContent/include/CesiumGltfContent/GltfUtilities.h b/CesiumGltfContent/include/CesiumGltfContent/GltfUtilities.h index 5e5a5413c..833a56289 100644 --- a/CesiumGltfContent/include/CesiumGltfContent/GltfUtilities.h +++ b/CesiumGltfContent/include/CesiumGltfContent/GltfUtilities.h @@ -9,6 +9,7 @@ #include #include +#include #include #include diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 04ab00cbe..aebc1d746 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -586,7 +586,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( SharedFuture>> operator()(std::shared_ptr depot) { // We have a depot, this is easy! - return depot->getOrFetch( + return depot->getOrFetch( asyncSystem, pAssetAccessor, factory, diff --git a/CesiumJsonReader/include/CesiumJsonReader/IJsonHandler.h b/CesiumJsonReader/include/CesiumJsonReader/IJsonHandler.h index e36f6b7fe..b04a77a35 100644 --- a/CesiumJsonReader/include/CesiumJsonReader/IJsonHandler.h +++ b/CesiumJsonReader/include/CesiumJsonReader/IJsonHandler.h @@ -3,6 +3,7 @@ #include "Library.h" #include +#include #include #include diff --git a/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h b/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h index 2adc9f917..2dd50d570 100644 --- a/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h +++ b/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h @@ -4,6 +4,7 @@ #include "Library.h" #include +#include namespace CesiumJsonReader { class CESIUMJSONREADER_API IgnoreValueJsonHandler : public IJsonHandler { diff --git a/CesiumJsonWriter/include/CesiumJsonWriter/JsonWriter.h b/CesiumJsonWriter/include/CesiumJsonWriter/JsonWriter.h index 0c60f7fd8..2d70c724a 100644 --- a/CesiumJsonWriter/include/CesiumJsonWriter/JsonWriter.h +++ b/CesiumJsonWriter/include/CesiumJsonWriter/JsonWriter.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include From 0e44d92482da3e3b85ccafee0d872be192813004 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 11 Sep 2024 15:12:22 -0400 Subject: [PATCH 14/81] clang-format --- CesiumGltf/include/CesiumGltf/ImageCesium.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index 1cfca8f70..991684188 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -30,7 +30,8 @@ struct CESIUMGLTF_API ImageCesiumMipPosition { * @brief Holds {@link Image} properties that are specific to the glTF loader * rather than part of the glTF spec. */ -struct CESIUMGLTF_API ImageCesium final : public CesiumUtility::ExtensibleObject { +struct CESIUMGLTF_API ImageCesium final + : public CesiumUtility::ExtensibleObject { /** * @brief The width of the image in pixels. */ From 8e664a18b34128cad0e55aafcc7289aa03fc95f9 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 13 Sep 2024 11:00:38 -0400 Subject: [PATCH 15/81] Fix warnings breaking Unreal build --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 963de417d..b3f64d7e7 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -54,7 +54,7 @@ class AssetContainer AssetContainer( std::string assetId_, - AssetType& asset_, + AssetType asset_, SingleAssetDepot* parent_) : counter(0), assetId(assetId_), asset(asset_), parent(parent_) {} SharedAsset toRef() { return SharedAsset(this); } @@ -274,7 +274,7 @@ template class SingleAssetDepot { return std::nullopt; }); - auto& [it, ok] = + auto [it, ok] = this->pendingAssets.emplace(uri, std::move(future).share()); if (!ok) { return asyncSystem From d612b329e016e39afd94489fbd896306c9addc45 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 17 Sep 2024 14:00:18 -0400 Subject: [PATCH 16/81] Use template for factory so it can be moved into the future --- .../include/CesiumGltf/SharedAssetDepot.h | 44 +++++++++---------- CesiumGltfReader/src/GltfReader.cpp | 13 +++--- CesiumGltfReader/src/decodeDataUrls.cpp | 2 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index b3f64d7e7..a812df805 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -26,18 +26,6 @@ namespace SharedAssetDepotInternals { template class SharedAsset; template class SingleAssetDepot; -/** - * Implemented for assets that can be constructed from data fetched from a URL. - */ -template class AssetFactory { -public: - /** - * Creates a new instance of this asset out of the given data, if possible. - */ - virtual std::optional - createFrom(const gsl::span& data) const = 0; -}; - /** * Contains the current state of an asset within the SharedAssetDepot. */ @@ -54,9 +42,12 @@ class AssetContainer AssetContainer( std::string assetId_, - AssetType asset_, + AssetType&& asset_, SingleAssetDepot* parent_) - : counter(0), assetId(assetId_), asset(asset_), parent(parent_) {} + : counter(0), + assetId(assetId_), + asset(std::move(asset_)), + parent(parent_) {} SharedAsset toRef() { return SharedAsset(this); } }; @@ -69,9 +60,16 @@ class AssetContainer */ template class SharedAsset { public: + SharedAsset(AssetType&& asset) + : contents(new AssetContainer( + std::string(), + std::forward(asset), + nullptr)) {} SharedAsset(AssetType& asset) - : contents(new AssetContainer(std::string(), asset, nullptr)) { - } + : contents(new AssetContainer( + std::string(), + std::forward(asset), + nullptr)) {} SharedAsset(std::nullptr_t) : contents(nullptr) {} SharedAsset() : contents(new AssetContainer( @@ -197,10 +195,11 @@ template class SingleAssetDepot { * If the asset has already started loading in this depot but hasn't finished, * its future will be returned. */ + template CesiumAsync::SharedFuture>> getOrFetch( CesiumAsync::AsyncSystem& asyncSystem, std::shared_ptr& pAssetAccessor, - const AssetFactory& factory, + const Factory& factory, std::string& uri, std::vector headers) { // We need to avoid: @@ -231,7 +230,7 @@ template class SingleAssetDepot { CesiumAsync::Future>> future = pAssetAccessor->get(asyncSystem, uri, headers) .thenInWorkerThread( - [&factory]( + [factory = std::move(factory)]( std::shared_ptr&& pRequest) -> std::optional { const CesiumAsync::IAssetResponse* pResponse = @@ -249,7 +248,7 @@ template class SingleAssetDepot { pAssets = &this->assets, pPendingAssetsMutex = &this->pendingAssetsMutex, pPendingAssets = - &this->pendingAssets](std::optional& result) + &this->pendingAssets](std::optional&& result) -> std::optional> { std::lock_guard lock(*pPendingAssetsMutex); @@ -274,8 +273,7 @@ template class SingleAssetDepot { return std::nullopt; }); - auto [it, ok] = - this->pendingAssets.emplace(uri, std::move(future).share()); + auto [it, ok] = this->pendingAssets.emplace(uri, std::move(future).share()); if (!ok) { return asyncSystem .createResolvedFuture(std::optional>()) @@ -320,10 +318,11 @@ class SharedAssetDepot { /** * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. */ + template CesiumAsync::SharedFuture>> getOrFetch( CesiumAsync::AsyncSystem& asyncSystem, std::shared_ptr& pAssetAccessor, - const AssetFactory& factory, + const Factory& factory, std::string& uri, std::vector headers) { return images @@ -340,7 +339,6 @@ class SharedAssetDepot { // actually export the public types to the right namespace // fairly sure this is anti-pattern actually but i'll fix it later -using SharedAssetDepotInternals::AssetFactory; using SharedAssetDepotInternals::SharedAsset; using SharedAssetDepotInternals::SharedAssetDepot; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index aebc1d746..c31b630d5 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -43,14 +43,14 @@ using namespace CesiumUtility; namespace { /** - * Used to construct a CesiumImage. + * Used to construct an ImageCesium. */ -struct ImageAssetFactory : public AssetFactory { +struct ImageAssetFactory { ImageAssetFactory(const Ktx2TranscodeTargets& ktx2TranscodeTargets_) : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} - virtual std::optional - createFrom(const gsl::span& data) const override { + std::optional + createFrom(const gsl::span& data) const { ImageReaderResult imageResult = ImageDecoder::readImage(data, this->ktx2TranscodeTargets); @@ -325,7 +325,8 @@ void postprocess( imageResult.errors.begin(), imageResult.errors.end()); if (imageResult.image) { - image.cesium = SharedAsset(imageResult.image.value()); + image.cesium = + SharedAsset(std::move(imageResult.image.value())); } else { if (image.mimeType) { readGltf.errors.emplace_back( @@ -574,7 +575,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( auto asset = pFactory->createFrom(bytes); if (asset.has_value()) { return std::optional>( - asset.value()); + std::move(asset.value())); } } diff --git a/CesiumGltfReader/src/decodeDataUrls.cpp b/CesiumGltfReader/src/decodeDataUrls.cpp index 764a9e076..4def2f4ed 100644 --- a/CesiumGltfReader/src/decodeDataUrls.cpp +++ b/CesiumGltfReader/src/decodeDataUrls.cpp @@ -142,7 +142,7 @@ void decodeDataUrls( } CesiumGltf::ImageCesium& imageCesium = imageResult.image.value(); - image.cesium = imageCesium; + image.cesium = std::move(imageCesium); if (options.clearDecodedDataUrls) { image.uri.reset(); From 5ae5539478e20d4a6f48b03db0b971c8b4dba1e9 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 18 Sep 2024 14:06:36 -0400 Subject: [PATCH 17/81] Clean up SharedAssetDepot --- .../include/CesiumGltf/SharedAssetDepot.h | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index a812df805..0d49e26df 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -200,8 +200,8 @@ template class SingleAssetDepot { CesiumAsync::AsyncSystem& asyncSystem, std::shared_ptr& pAssetAccessor, const Factory& factory, - std::string& uri, - std::vector headers) { + const std::string& uri, + const std::vector& headers) { // We need to avoid: // - Two assets starting loading before the first asset has updated the // pendingAssets map @@ -283,9 +283,26 @@ template class SingleAssetDepot { return it->second; } + /** + * @brief Returns the total number of distinct assets contained in this depot. + */ size_t getDistinctCount() const { return this->assets.size(); } + /** + * @brief Returns the number of active references to assets in this depot. + */ + size_t getUsageCount() const { + size_t count = 0; + for (auto& [key, item] : assets) { + count += item->counter; + } + return count; + } + private: + // Disable copy + void operator=(const SharedAsset& other) = delete; + /** * Removes the given asset from the depot. * Should only be called by {@link SharedAsset}. @@ -323,8 +340,8 @@ class SharedAssetDepot { CesiumAsync::AsyncSystem& asyncSystem, std::shared_ptr& pAssetAccessor, const Factory& factory, - std::string& uri, - std::vector headers) { + const std::string& uri, + const std::vector& headers) { return images .getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); } From 0ce17c897bfa4d5b301153184d5687264b4ecf90 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 18 Sep 2024 14:11:58 -0400 Subject: [PATCH 18/81] Fix wonky namespaces --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 0d49e26df..44da3fee7 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -21,8 +21,6 @@ namespace CesiumGltf { -namespace SharedAssetDepotInternals { - template class SharedAsset; template class SingleAssetDepot; @@ -330,7 +328,8 @@ template class SingleAssetDepot { */ class SharedAssetDepot { public: - SharedAssetDepot() {} + SharedAssetDepot() = default; + void operator=(const SharedAssetDepot& other) = delete; /** * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. @@ -346,17 +345,12 @@ class SharedAssetDepot { .getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); } - size_t getImagesCount() const { return this->images.getDistinctCount(); } + const SingleAssetDepot* getImageDepot() { + return &this->images; + } private: SingleAssetDepot images; }; -} // namespace SharedAssetDepotInternals - -// actually export the public types to the right namespace -// fairly sure this is anti-pattern actually but i'll fix it later -using SharedAssetDepotInternals::SharedAsset; -using SharedAssetDepotInternals::SharedAssetDepot; - } // namespace CesiumGltf From 4851ed2dc2b28365ebe4022a18f3275cf852c552 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 18 Sep 2024 16:20:19 -0400 Subject: [PATCH 19/81] Fix test --- Cesium3DTilesSelection/test/TestTilesetContentManager.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 97458e6cd..0cadd9005 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1682,7 +1682,9 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CHECK(images.size() == 1); } - CHECK(pManager->getSharedAssetDepot()->getImagesCount() == 2); + CHECK( + pManager->getSharedAssetDepot()->getImageDepot()->getDistinctCount() == + 2); // unload the tile content for (auto& child : containerTile.getChildren()) { From 31d0048b58044546182d2bcd9d72106bb5ca8991 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Wed, 25 Sep 2024 17:18:43 -0400 Subject: [PATCH 20/81] Add timer for deleting assets --- .../include/CesiumGltf/SharedAssetDepot.h | 92 +++++++++++++++++-- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 44da3fee7..4d324e775 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -61,12 +62,12 @@ template class SharedAsset { SharedAsset(AssetType&& asset) : contents(new AssetContainer( std::string(), - std::forward(asset), + std::move(asset), nullptr)) {} - SharedAsset(AssetType& asset) + SharedAsset(const AssetType& asset) : contents(new AssetContainer( std::string(), - std::forward(asset), + AssetType(asset), nullptr)) {} SharedAsset(std::nullptr_t) : contents(nullptr) {} SharedAsset() @@ -154,7 +155,7 @@ template class SharedAsset { contents->counter += amt; if (contents->counter <= 0) { if (contents->parent != nullptr) { - contents->parent->remove(contents->assetId); + contents->parent->markDeletionCandidate(contents->assetId); } contents = nullptr; } @@ -176,6 +177,15 @@ template class SharedAsset { */ template class SingleAssetDepot { public: + /** + * @brief Number of seconds an asset can remain unused before it's deleted. + */ + float assetDeletionThreshold = 60.0f; + + SingleAssetDepot() { + this->lastDeletionTick = std::chrono::steady_clock::now(); + } + /** * Stores the AssetType in this depot and returns a reference to it, * or returns the existing asset if present. @@ -206,7 +216,7 @@ template class SingleAssetDepot { // - An asset starting to load after the previous load has been removed from // the pendingAssets map, but before the completed asset has been added to // the assets map. - std::lock_guard lock(pendingAssetsMutex); + std::lock_guard lock(assetsMutex); auto existingIt = this->assets.find(uri); if (existingIt != this->assets.end()) { @@ -244,7 +254,7 @@ template class SingleAssetDepot { [uri, this, pAssets = &this->assets, - pPendingAssetsMutex = &this->pendingAssetsMutex, + pPendingAssetsMutex = &this->assetsMutex, pPendingAssets = &this->pendingAssets](std::optional&& result) -> std::optional> { @@ -281,6 +291,36 @@ template class SingleAssetDepot { return it->second; } + /** + * @brief Should be called every frame to handle deletion of assets that + * haven't been used in a while. + */ + void deletionTick() { + std::chrono::steady_clock::time_point now = + std::chrono::steady_clock::now(); + std::chrono::duration delta = + std::chrono::duration(now - this->lastDeletionTick); + + std::vector toDelete; + + std::lock_guard lock(this->deletionCandidatesMutex); + for (auto& [hash, age] : this->deletionCandidates) { + age += delta.count(); + + if (age >= this->assetDeletionThreshold) { + toDelete.push_back(hash); + } + } + + std::lock_guard assetsLock(this->assetsMutex); + for (std::string& hash : toDelete) { + this->deletionCandidates.erase(hash); + this->assets.erase(hash); + } + + this->lastDeletionTick = now; + } + /** * @brief Returns the total number of distinct assets contained in this depot. */ @@ -297,15 +337,40 @@ template class SingleAssetDepot { return count; } + /** + * @brief Obtains statistics on the number of assets currently listed as + * candidates for deletion, along with the average number of seconds that + * they've been pending deletion. + * @param outAverageAge The average time in seconds that the current deletion + * candidates have spent pending deletion. + * @param outCount The number of current deletion candidates. + */ + void getDeletionStats(float& outAverageAge, size_t& outCount) const { + size_t count = 0; + float total = 0; + + std::lock_guard lock(this->deletionCandidatesMutex); + for (auto& [asset, age] : this->deletionCandidates) { + total += age; + count++; + } + + outAverageAge = count > 0 ? total / count : 0; + outCount = count; + } + private: // Disable copy void operator=(const SharedAsset& other) = delete; /** - * Removes the given asset from the depot. + * Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void remove(std::string& hash) { this->assets.erase(hash); } + void markDeletionCandidate(const std::string& hash) { + std::lock_guard lock(this->deletionCandidatesMutex); + this->deletionCandidates.emplace(hash, 0.0f); + } // Assets that have a unique ID that can be used to de-duplicate them. std::unordered_map< @@ -318,7 +383,14 @@ template class SingleAssetDepot { CesiumAsync::SharedFuture>>> pendingAssets; // Mutex for checking or editing the pendingAssets map - std::mutex pendingAssetsMutex; + std::mutex assetsMutex; + + // Maps assets that are being considered for deletion to the time that they + // began to be considered for deletion. + std::unordered_map deletionCandidates; + // Mutex for modifying the deletionCandidates map. + mutable std::mutex deletionCandidatesMutex; + std::chrono::steady_clock::time_point lastDeletionTick; friend class SharedAsset; }; @@ -349,6 +421,8 @@ class SharedAssetDepot { return &this->images; } + void deletionTick() { this->images.deletionTick(); } + private: SingleAssetDepot images; }; From 97909b8e3f32133925625db1de50c668b0cce421 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 26 Sep 2024 17:18:42 +1000 Subject: [PATCH 21/81] Add AttributeSemantics. --- .../include/CesiumGltf/AttributeSemantics.h | 51 +++++++++++++ CesiumGltf/src/AttributeSemantics.cpp | 75 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 CesiumGltf/include/CesiumGltf/AttributeSemantics.h create mode 100644 CesiumGltf/src/AttributeSemantics.cpp diff --git a/CesiumGltf/include/CesiumGltf/AttributeSemantics.h b/CesiumGltf/include/CesiumGltf/AttributeSemantics.h new file mode 100644 index 000000000..57195e707 --- /dev/null +++ b/CesiumGltf/include/CesiumGltf/AttributeSemantics.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +namespace CesiumGltf { + +/** + * @brief The standard glTF attribute semantics from the specification. + */ +struct AttributeSemantics { + /** + * @brief Unitless XYZ vertex positions. + */ + static const std::string POSITION; + + /** + * @brief Normalized XYZ vertex normals. + */ + static const std::string NORMAL; + + /** + * @brief XYZW vertex tangents where the XYZ portion is normalized, and the W + * component is a sign value (-1 or +1) indicating handedness of the tangent + * basis. + */ + static const std::string TANGENT; + + /** + * @brief ST texture coordinates + */ + static const std::array TEXCOORD_n; + + /** + * @brief RGB or RGBA vertex color linear multiplier. + */ + static const std::array COLOR_n; + + /** + * @brief The indices of the joints from the corresponding skin.joints array + * that affect the vertex. + */ + static const std::array JOINTS_n; + + /** + * @brief The weights indicating how strongly the joint influences the vertex. + */ + static const std::array WEIGHTS_n; +}; + +} // namespace CesiumGltf diff --git a/CesiumGltf/src/AttributeSemantics.cpp b/CesiumGltf/src/AttributeSemantics.cpp new file mode 100644 index 000000000..5659c333e --- /dev/null +++ b/CesiumGltf/src/AttributeSemantics.cpp @@ -0,0 +1,75 @@ +#include + +namespace CesiumGltf { + +/** + * @brief Unitless XYZ vertex positions. + */ +const std::string AttributeSemantics::POSITION = "POSITION"; + +/** + * @brief Normalized XYZ vertex normals. + */ +const std::string AttributeSemantics::NORMAL = "NORMAL"; + +/** + * @brief XYZW vertex tangents where the XYZ portion is normalized, and the W + * component is a sign value (-1 or +1) indicating handedness of the tangent + * basis. + */ +const std::string AttributeSemantics::TANGENT = "TANGENT"; + +/** + * @brief ST texture coordinates + */ +const std::array AttributeSemantics::TEXCOORD_n = { + "TEXCOORD_0", + "TEXCOORD_1", + "TEXCOORD_2", + "TEXCOORD_3", + "TEXCOORD_4", + "TEXCOORD_5", + "TEXCOORD_6", + "TEXCOORD_7"}; + +/** + * @brief RGB or RGBA vertex color linear multiplier. + */ +const std::array AttributeSemantics::COLOR_n = { + "COLOR_0", + "COLOR_1", + "COLOR_2", + "COLOR_3", + "COLOR_4", + "COLOR_5", + "COLOR_6", + "COLOR_7"}; + +/** + * @brief The indices of the joints from the corresponding skin.joints array + * that affect the vertex. + */ +const std::array AttributeSemantics::JOINTS_n = { + "JOINTS_0", + "JOINTS_1", + "JOINTS_2", + "JOINTS_3", + "JOINTS_4", + "JOINTS_5", + "JOINTS_6", + "JOINTS_7"}; + +/** + * @brief The weights indicating how strongly the joint influences the vertex. + */ +const std::array AttributeSemantics::WEIGHTS_n = { + "WEIGHTS_0", + "WEIGHTS_1", + "WEIGHTS_2", + "WEIGHTS_3", + "WEIGHTS_4", + "WEIGHTS_5", + "WEIGHTS_6", + "WEIGHTS_7"}; + +} // namespace CesiumGltf From 86995ffd49c33774ea3a7b6f3613cfeebd402e2e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 14:44:49 +1000 Subject: [PATCH 22/81] Allow constructor parameters to pass to ExtensibleObject::addExtension. --- CesiumUtility/include/CesiumUtility/ExtensibleObject.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CesiumUtility/include/CesiumUtility/ExtensibleObject.h b/CesiumUtility/include/CesiumUtility/ExtensibleObject.h index 012323f5f..c8e56755b 100644 --- a/CesiumUtility/include/CesiumUtility/ExtensibleObject.h +++ b/CesiumUtility/include/CesiumUtility/ExtensibleObject.h @@ -70,9 +70,14 @@ struct CESIUMUTILITY_API ExtensibleObject { * @tparam T The type of the extension to add. * @return The added extension. */ - template T& addExtension() { + template + T& addExtension(ConstructorArgumentTypes&&... constructorArguments) { std::any& extension = - extensions.try_emplace(T::ExtensionName, std::make_any()) + extensions + .try_emplace( + T::ExtensionName, + std::make_any(std::forward( + constructorArguments)...)) .first->second; return std::any_cast(extension); } From 9fe3b4e742e44dfc5bfde586b9f7dfbb99eb40dc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 2 Sep 2024 17:22:23 +1000 Subject: [PATCH 23/81] WIP error reporting for raster overlays. --- .../RasterOverlayTileProvider.h | 16 +++---- .../src/QuadtreeRasterOverlayTileProvider.cpp | 9 ++-- .../src/RasterOverlayTileProvider.cpp | 42 +++++++++---------- .../src/TileMapServiceRasterOverlay.cpp | 5 ++- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h index 4d8a24217..3c06484bb 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -48,19 +49,12 @@ struct CESIUMRASTEROVERLAYS_API LoadedRasterOverlayImage { std::vector credits{}; /** - * @brief Error messages from loading the image. + * @brief Errors and warnings from loading the image. * - * If the image was loaded successfully, this should be empty. + * If the image was loaded successfully, there should not be any errors (but + * there may be warnings). */ - std::vector errors{}; - - /** - * @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{}; + CesiumUtility::ErrorList errors{}; /** * @brief Whether more detailed data, beyond this image, is available within diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index 0750628e8..ecd8d8cf8 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -319,7 +319,7 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( .catchImmediately([](std::exception&& e) { // Turn an exception into an error. LoadedRasterOverlayImage result; - result.errors.emplace_back(e.what()); + result.errors.emplaceError(e.what()); return result; }) .thenImmediately([&cachedBytes = this->_cachedBytes, @@ -328,7 +328,7 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( asyncSystem = this->getAsyncSystem(), loadParentTile = std::move(loadParentTile)]( LoadedRasterOverlayImage&& loaded) { - if (loaded.image && loaded.errors.empty() && + if (loaded.image && !loaded.errors.hasErrors() && loaded.image->width > 0 && loaded.image->height > 0) { // Successfully loaded, continue. cachedBytes += int64_t(loaded.image->pixelData.size()); @@ -470,13 +470,13 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( // For non-useful sets of images, just return an empty image, // 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. + // edge case that is not yet handled. Be sure to pass through any + // errors and warnings. return LoadedRasterOverlayImage{ ImageCesium(), Rectangle(), {}, {}, - {}, false}; } @@ -661,7 +661,6 @@ QuadtreeRasterOverlayTileProvider::combineImages( targetRectangle, {}, {}, - {}, true // TODO }; } diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 1d0191fbc..94f18a34c 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -136,27 +136,30 @@ RasterOverlayTileProvider::loadTileImageFromUrl( CESIUM_TRACE("load image"); const IAssetResponse* pResponse = pRequest->response(); if (pResponse == nullptr) { + ErrorList errors; + errors.emplaceError( + fmt::format("Image request for {} failed.", pRequest->url())); return LoadedRasterOverlayImage{ std::nullopt, options.rectangle, std::move(options.credits), - {"Image request for " + pRequest->url() + " failed."}, - {}, + std::move(errors), 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(); + ErrorList errors; + errors.emplaceError(fmt::format( + "Received response code {} for image {}.", + pResponse->statusCode(), + pRequest->url())); return LoadedRasterOverlayImage{ std::nullopt, options.rectangle, std::move(options.credits), - {message}, - {}, + std::move(errors), options.moreDetailAvailable}; } @@ -167,15 +170,18 @@ RasterOverlayTileProvider::loadTileImageFromUrl( options.rectangle, std::move(options.credits), {}, - {}, options.moreDetailAvailable}; } + + ErrorList errors; + errors.emplaceError(fmt::format( + "Image response for {} is empty.", + pRequest->url())); return LoadedRasterOverlayImage{ std::nullopt, options.rectangle, std::move(options.credits), - {"Image response for " + pRequest->url() + " is empty."}, - {}, + std::move(errors), options.moreDetailAvailable}; } @@ -242,24 +248,16 @@ static LoadResult createLoadResultFromLoadedImage( LoadedRasterOverlayImage&& loadedImage, const std::any& rendererOptions) { if (!loadedImage.image.has_value()) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Failed to load image for tile {}:\n- {}", - "TODO", - // Cesium3DTilesSelection::TileIdUtilities::createTileIdString(tileId), - CesiumUtility::joinToString(loadedImage.errors, "\n- ")); + loadedImage.errors.logError(pLogger, "Failed to load image for tile"); LoadResult result; result.state = RasterOverlayTile::LoadState::Failed; return result; } - if (!loadedImage.warnings.empty()) { - SPDLOG_LOGGER_WARN( + if (!loadedImage.errors.warnings.empty()) { + loadedImage.errors.logWarning( pLogger, - "Warnings while loading image for tile {}:\n- {}", - "TODO", - // Cesium3DTilesSelection::TileIdUtilities::createTileIdString(tileId), - CesiumUtility::joinToString(loadedImage.warnings, "\n- ")); + "Warnings while loading image for tile"); } CesiumGltf::ImageCesium& image = loadedImage.image.value(); diff --git a/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp index 686e3358a..b81b103ce 100644 --- a/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp @@ -91,13 +91,14 @@ class TileMapServiceTileProvider final this->_headers, std::move(options)); } else { + ErrorList errors; + errors.emplaceError("Failed to load image from TMS."); return this->getAsyncSystem() .createResolvedFuture( {std::nullopt, options.rectangle, {}, - {"Failed to load image from TMS."}, - {}, + std::move(errors), options.moreDetailAvailable}); } } From 95840fe38dd6584c5629c6766d4efc826ff74d32 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 5 Sep 2024 15:52:25 +1000 Subject: [PATCH 24/81] Fix compile errors in tests. --- Cesium3DTilesSelection/test/TestTilesetContentManager.cpp | 1 - .../test/TestQuadtreeRasterOverlayTileProvider.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 0cadd9005..07241d350 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1311,7 +1311,6 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { overlayTile.getRectangle(), {}, {}, - {}, true}); } }; diff --git a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp index 05528416b..4e83a3d27 100644 --- a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp @@ -59,7 +59,7 @@ class TestTileProvider : public QuadtreeRasterOverlayTileProvider { if (std::find(errorTiles.begin(), errorTiles.end(), tileID) != errorTiles.end()) { - result.errors.emplace_back("Tile errored."); + result.errors.emplaceError("Tile errored."); } else { // Return an image where every component of every pixel is equal to the // tile level. From 8ec646c6dd04a5bc7b41b0b7c29b9b6bf43cfefe Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 5 Sep 2024 22:53:58 +1000 Subject: [PATCH 25/81] Propagate raster overlay load errors to the user. --- .../src/QuadtreeRasterOverlayTileProvider.cpp | 17 +++++++++++++++-- .../src/RasterOverlayTileProvider.cpp | 4 ++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index ecd8d8cf8..bae8ae44a 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -472,11 +472,17 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( // See https://github.com/CesiumGS/cesium-native/issues/316 for an // edge case that is not yet handled. Be sure to pass through any // errors and warnings. + ErrorList errors; + for (LoadedQuadtreeImage& image : images) { + if (image.pLoaded) { + errors.merge(image.pLoaded->errors); + } + } return LoadedRasterOverlayImage{ ImageCesium(), Rectangle(), {}, - {}, + std::move(errors), false}; } @@ -645,6 +651,12 @@ QuadtreeRasterOverlayTileProvider::combineImages( const Rectangle& targetRectangle, const Projection& /* projection */, std::vector&& images) { + ErrorList errors; + for (LoadedQuadtreeImage& image : images) { + if (image.pLoaded) { + errors.merge(std::move(image.pLoaded->errors)); + } + } const CombinedImageMeasurements measurements = QuadtreeRasterOverlayTileProvider::measureCombinedImage( @@ -660,7 +672,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( std::nullopt, targetRectangle, {}, - {}, + std::move(errors), true // TODO }; } @@ -668,6 +680,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( LoadedRasterOverlayImage result; result.rectangle = measurements.rectangle; result.moreDetailAvailable = false; + result.errors = std::move(errors); ImageCesium& target = result.image.emplace(); target.bytesPerChannel = measurements.bytesPerChannel; diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 94f18a34c..1dfa06995 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -254,6 +254,10 @@ static LoadResult createLoadResultFromLoadedImage( return result; } + if (loadedImage.errors.hasErrors()) { + loadedImage.errors.logError(pLogger, "Errors while loading image for tile"); + } + if (!loadedImage.errors.warnings.empty()) { loadedImage.errors.logWarning( pLogger, From 03b60efa8c528929993654002b3beae9aa83d924 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Sep 2024 14:46:44 +1000 Subject: [PATCH 26/81] Don't let failing overlays stop rendering completely. --- .../src/RasterMappedTo3DTile.cpp | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp b/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp index a7434e787..7f57d3a5b 100644 --- a/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp +++ b/Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp @@ -83,8 +83,10 @@ RasterOverlayTile::MoreDetailAvailable RasterMappedTo3DTile::update( RasterOverlayTile::LoadState::Failed && pTile) { // Note when our original tile fails to load so that we don't report more - // data available. This means - by design - we won't refine past a failed - // tile. + // data available. This means - by design - we won't upsample for a failed + // raster overlay tile. However, each real (non-upsampled) geometry tile + // will have raster overlay images mapped to it, even if the parent geometry + // tile's images already failed to load. this->_originalFailed = true; pTile = pTile->getParent(); @@ -151,6 +153,16 @@ RasterOverlayTile::MoreDetailAvailable RasterMappedTo3DTile::update( // Compute the translation and scale for the new tile. this->computeTranslationAndScale(tile); + } else if ( + pCandidate == nullptr && this->_pReadyTile == nullptr && + this->_pLoadingTile->getState() == + RasterOverlayTile::LoadState::Failed) { + // This overlay tile failed to load, and there are no better candidates + // available. So mark this failed tile ready so that it doesn't block the + // entire tileset from rendering. + this->_pReadyTile = this->_pLoadingTile; + this->_pLoadingTile = nullptr; + this->_state = AttachmentState::Attached; } } @@ -203,11 +215,15 @@ void RasterMappedTo3DTile::detachFromTile( return; } - prepareRendererResources.detachRasterInMainThread( - tile, - this->getTextureCoordinateID(), - *this->_pReadyTile, - this->_pReadyTile->getRendererResources()); + // Failed tiles aren't attached with the renderer, so don't detach them, + // either. + if (this->_pReadyTile->getState() != RasterOverlayTile::LoadState::Failed) { + prepareRendererResources.detachRasterInMainThread( + tile, + this->getTextureCoordinateID(), + *this->_pReadyTile, + this->_pReadyTile->getRendererResources()); + } this->_state = AttachmentState::Unattached; } From f60168034a66d55eeff35ded9c22c77111a8dbf9 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Sep 2024 15:21:32 +1000 Subject: [PATCH 27/81] errors -> errorList. --- .../CesiumRasterOverlays/RasterOverlayTileProvider.h | 2 +- .../src/QuadtreeRasterOverlayTileProvider.cpp | 10 +++++----- .../src/RasterOverlayTileProvider.cpp | 12 +++++++----- .../test/TestQuadtreeRasterOverlayTileProvider.cpp | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h index 3c06484bb..f232c5b8d 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h @@ -54,7 +54,7 @@ struct CESIUMRASTEROVERLAYS_API LoadedRasterOverlayImage { * If the image was loaded successfully, there should not be any errors (but * there may be warnings). */ - CesiumUtility::ErrorList errors{}; + CesiumUtility::ErrorList errorList{}; /** * @brief Whether more detailed data, beyond this image, is available within diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index bae8ae44a..28d407801 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -319,7 +319,7 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( .catchImmediately([](std::exception&& e) { // Turn an exception into an error. LoadedRasterOverlayImage result; - result.errors.emplaceError(e.what()); + result.errorList.emplaceError(e.what()); return result; }) .thenImmediately([&cachedBytes = this->_cachedBytes, @@ -328,7 +328,7 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( asyncSystem = this->getAsyncSystem(), loadParentTile = std::move(loadParentTile)]( LoadedRasterOverlayImage&& loaded) { - if (loaded.image && !loaded.errors.hasErrors() && + if (loaded.image && !loaded.errorList.hasErrors() && loaded.image->width > 0 && loaded.image->height > 0) { // Successfully loaded, continue. cachedBytes += int64_t(loaded.image->pixelData.size()); @@ -475,7 +475,7 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( ErrorList errors; for (LoadedQuadtreeImage& image : images) { if (image.pLoaded) { - errors.merge(image.pLoaded->errors); + errors.merge(image.pLoaded->errorList); } } return LoadedRasterOverlayImage{ @@ -654,7 +654,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( ErrorList errors; for (LoadedQuadtreeImage& image : images) { if (image.pLoaded) { - errors.merge(std::move(image.pLoaded->errors)); + errors.merge(std::move(image.pLoaded->errorList)); } } @@ -680,7 +680,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( LoadedRasterOverlayImage result; result.rectangle = measurements.rectangle; result.moreDetailAvailable = false; - result.errors = std::move(errors); + result.errorList = std::move(errors); ImageCesium& target = result.image.emplace(); target.bytesPerChannel = measurements.bytesPerChannel; diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 1dfa06995..1df7f6038 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -248,18 +248,20 @@ static LoadResult createLoadResultFromLoadedImage( LoadedRasterOverlayImage&& loadedImage, const std::any& rendererOptions) { if (!loadedImage.image.has_value()) { - loadedImage.errors.logError(pLogger, "Failed to load image for tile"); + loadedImage.errorList.logError(pLogger, "Failed to load image for tile"); LoadResult result; result.state = RasterOverlayTile::LoadState::Failed; return result; } - if (loadedImage.errors.hasErrors()) { - loadedImage.errors.logError(pLogger, "Errors while loading image for tile"); + if (loadedImage.errorList.hasErrors()) { + loadedImage.errorList.logError( + pLogger, + "Errors while loading image for tile"); } - if (!loadedImage.errors.warnings.empty()) { - loadedImage.errors.logWarning( + if (!loadedImage.errorList.warnings.empty()) { + loadedImage.errorList.logWarning( pLogger, "Warnings while loading image for tile"); } diff --git a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp index 4e83a3d27..594014ddf 100644 --- a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp @@ -59,7 +59,7 @@ class TestTileProvider : public QuadtreeRasterOverlayTileProvider { if (std::find(errorTiles.begin(), errorTiles.end(), tileID) != errorTiles.end()) { - result.errors.emplaceError("Tile errored."); + result.errorList.emplaceError("Tile errored."); } else { // Return an image where every component of every pixel is equal to the // tile level. From e83222c5da9f50f41eb8ae397730f3930f3ab074 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Sep 2024 15:27:33 +1000 Subject: [PATCH 28/81] Update CHANGES.md. --- CHANGES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 023b7e438..351fb7985 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,16 @@ # Change Log +### Not Released Yet + +##### Breaking Changes :mega: + +- `LoadedRasterOverlayImage` now has a single `errorList` property instead of separate `errors` and `warnings` properties. + +##### Fixes :wrench: + +- Errors while loading raster overlays are now logged. Previously, they were silently ignored in many cases. +- A raster overlay image failing to load will no longer completely prevent the geometry tile to which it is attached from rendering. Instead, once the raster overlay fails, the geometry tile will be shown without the raster overlay. + ### v0.39.0 - 2024-09-02 ##### Breaking Changes :mega: From 407be444f5e5651a80dccc967cd95777b3eaf7c1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 6 Sep 2024 15:42:52 +1000 Subject: [PATCH 29/81] Fix clang error. --- CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 1df7f6038..834cb789c 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -203,8 +203,9 @@ RasterOverlayTileProvider::loadTileImageFromUrl( loadedImage.image, options.rectangle, std::move(options.credits), - std::move(loadedImage.errors), - std::move(loadedImage.warnings), + ErrorList{ + std::move(loadedImage.errors), + std::move(loadedImage.warnings)}, options.moreDetailAvailable}; }); } From 05e1af1e80c4a8a01872e2d4a70acd3d08af4657 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 18 Sep 2024 09:15:10 +1000 Subject: [PATCH 30/81] Show tileset if overlay tile provider can't be created. --- .../src/EmptyRasterOverlayTileProvider.cpp | 27 +++++++++++++++ .../src/EmptyRasterOverlayTileProvider.h | 20 +++++++++++ .../src/RasterOverlayCollection.cpp | 33 ++++++++++++------- 3 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.cpp create mode 100644 Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.h diff --git a/Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.cpp b/Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.cpp new file mode 100644 index 000000000..f386d32ad --- /dev/null +++ b/Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.cpp @@ -0,0 +1,27 @@ +#include "EmptyRasterOverlayTileProvider.h" + +using namespace CesiumRasterOverlays; + +namespace Cesium3DTilesSelection { + +EmptyRasterOverlayTileProvider::EmptyRasterOverlayTileProvider( + const CesiumUtility::IntrusivePointer& pOwner, + const CesiumAsync::AsyncSystem& asyncSystem) noexcept + : RasterOverlayTileProvider( + pOwner, + asyncSystem, + nullptr, + std::nullopt, + nullptr, + nullptr, + CesiumGeospatial::GeographicProjection(), + CesiumGeometry::Rectangle()) {} + +CesiumAsync::Future +EmptyRasterOverlayTileProvider::loadTileImage( + CesiumRasterOverlays::RasterOverlayTile& /*overlayTile*/) { + return this->getAsyncSystem() + .createResolvedFuture({}); +} + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.h b/Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.h new file mode 100644 index 000000000..ca1b3d0e8 --- /dev/null +++ b/Cesium3DTilesSelection/src/EmptyRasterOverlayTileProvider.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace Cesium3DTilesSelection { + +class EmptyRasterOverlayTileProvider + : public CesiumRasterOverlays::RasterOverlayTileProvider { +public: + EmptyRasterOverlayTileProvider( + const CesiumUtility::IntrusivePointer< + const CesiumRasterOverlays::RasterOverlay>& pOwner, + const CesiumAsync::AsyncSystem& asyncSystem) noexcept; + +protected: + virtual CesiumAsync::Future + loadTileImage(CesiumRasterOverlays::RasterOverlayTile& overlayTile) override; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/RasterOverlayCollection.cpp b/Cesium3DTilesSelection/src/RasterOverlayCollection.cpp index 065411948..f9fa5875b 100644 --- a/Cesium3DTilesSelection/src/RasterOverlayCollection.cpp +++ b/Cesium3DTilesSelection/src/RasterOverlayCollection.cpp @@ -1,3 +1,5 @@ +#include "EmptyRasterOverlayTileProvider.h" + #include #include #include @@ -113,19 +115,14 @@ void RasterOverlayCollection::add( "Error while creating tile provider: {0}", e.what())}); }) - .thenInMainThread([pOverlay, pList, pLogger = this->_externals.pLogger]( + .thenInMainThread([pOverlay, + pList, + pLogger = this->_externals.pLogger, + asyncSystem = this->_externals.asyncSystem]( RasterOverlay::CreateTileProviderResult&& result) { + IntrusivePointer pProvider = nullptr; if (result) { - // Find the overlay's current location in the list. - // It's possible it has been removed completely. - auto it = std::find( - pList->overlays.begin(), - pList->overlays.end(), - pOverlay); - if (it != pList->overlays.end()) { - std::int64_t index = it - pList->overlays.begin(); - pList->tileProviders[size_t(index)] = *result; - } + pProvider = *result; } else { // Report error creating the tile provider. const RasterOverlayLoadFailureDetails& failureDetails = @@ -134,7 +131,21 @@ void RasterOverlayCollection::add( if (pOverlay->getOptions().loadErrorCallback) { pOverlay->getOptions().loadErrorCallback(failureDetails); } + + // Create a tile provider that does not provide any tiles at all. + pProvider = new EmptyRasterOverlayTileProvider(pOverlay, asyncSystem); } + + auto it = + std::find(pList->overlays.begin(), pList->overlays.end(), pOverlay); + + // Find the overlay's current location in the list. + // It's possible it has been removed completely. + if (it != pList->overlays.end()) { + std::int64_t index = it - pList->overlays.begin(); + pList->tileProviders[size_t(index)] = pProvider; + } + // CESIUM_TRACE_END_IN_TRACK("createTileProvider"); }); } From e781a9c4bc4cf67a1108755df847e6bd1636a792 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 08:04:54 +1000 Subject: [PATCH 31/81] AttributeSemantics -> VertexAttributeSemantics --- ...uteSemantics.h => VertexAttributeSemantics.h} | 4 ++-- ...emantics.cpp => VertexAttributeSemantics.cpp} | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) rename CesiumGltf/include/CesiumGltf/{AttributeSemantics.h => VertexAttributeSemantics.h} (90%) rename CesiumGltf/src/{AttributeSemantics.cpp => VertexAttributeSemantics.cpp} (68%) diff --git a/CesiumGltf/include/CesiumGltf/AttributeSemantics.h b/CesiumGltf/include/CesiumGltf/VertexAttributeSemantics.h similarity index 90% rename from CesiumGltf/include/CesiumGltf/AttributeSemantics.h rename to CesiumGltf/include/CesiumGltf/VertexAttributeSemantics.h index 57195e707..03ea6a84f 100644 --- a/CesiumGltf/include/CesiumGltf/AttributeSemantics.h +++ b/CesiumGltf/include/CesiumGltf/VertexAttributeSemantics.h @@ -6,9 +6,9 @@ namespace CesiumGltf { /** - * @brief The standard glTF attribute semantics from the specification. + * @brief The standard glTF vertex attribute semantics from the specification. */ -struct AttributeSemantics { +struct VertexAttributeSemantics { /** * @brief Unitless XYZ vertex positions. */ diff --git a/CesiumGltf/src/AttributeSemantics.cpp b/CesiumGltf/src/VertexAttributeSemantics.cpp similarity index 68% rename from CesiumGltf/src/AttributeSemantics.cpp rename to CesiumGltf/src/VertexAttributeSemantics.cpp index 5659c333e..932a3e002 100644 --- a/CesiumGltf/src/AttributeSemantics.cpp +++ b/CesiumGltf/src/VertexAttributeSemantics.cpp @@ -1,28 +1,28 @@ -#include +#include namespace CesiumGltf { /** * @brief Unitless XYZ vertex positions. */ -const std::string AttributeSemantics::POSITION = "POSITION"; +const std::string VertexAttributeSemantics::POSITION = "POSITION"; /** * @brief Normalized XYZ vertex normals. */ -const std::string AttributeSemantics::NORMAL = "NORMAL"; +const std::string VertexAttributeSemantics::NORMAL = "NORMAL"; /** * @brief XYZW vertex tangents where the XYZ portion is normalized, and the W * component is a sign value (-1 or +1) indicating handedness of the tangent * basis. */ -const std::string AttributeSemantics::TANGENT = "TANGENT"; +const std::string VertexAttributeSemantics::TANGENT = "TANGENT"; /** * @brief ST texture coordinates */ -const std::array AttributeSemantics::TEXCOORD_n = { +const std::array VertexAttributeSemantics::TEXCOORD_n = { "TEXCOORD_0", "TEXCOORD_1", "TEXCOORD_2", @@ -35,7 +35,7 @@ const std::array AttributeSemantics::TEXCOORD_n = { /** * @brief RGB or RGBA vertex color linear multiplier. */ -const std::array AttributeSemantics::COLOR_n = { +const std::array VertexAttributeSemantics::COLOR_n = { "COLOR_0", "COLOR_1", "COLOR_2", @@ -49,7 +49,7 @@ const std::array AttributeSemantics::COLOR_n = { * @brief The indices of the joints from the corresponding skin.joints array * that affect the vertex. */ -const std::array AttributeSemantics::JOINTS_n = { +const std::array VertexAttributeSemantics::JOINTS_n = { "JOINTS_0", "JOINTS_1", "JOINTS_2", @@ -62,7 +62,7 @@ const std::array AttributeSemantics::JOINTS_n = { /** * @brief The weights indicating how strongly the joint influences the vertex. */ -const std::array AttributeSemantics::WEIGHTS_n = { +const std::array VertexAttributeSemantics::WEIGHTS_n = { "WEIGHTS_0", "WEIGHTS_1", "WEIGHTS_2", From cee6f21ea64b8f5f4ffc6797ba9c75f7bab909cb Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 16:10:28 +1000 Subject: [PATCH 32/81] Make SharedAsset a base class an eliminate AssetContainer. --- .../include/Cesium3DTilesSelection/Tileset.h | 4 +- Cesium3DTilesSelection/src/Tile.cpp | 4 +- .../src/TileContentLoadInfo.cpp | 6 +- .../src/TileContentLoadInfo.h | 5 +- Cesium3DTilesSelection/src/Tileset.cpp | 4 +- .../src/TilesetContentManager.cpp | 23 +- .../src/TilesetContentManager.h | 4 +- .../src/TilesetJsonLoader.h | 4 +- .../test/TestTilesetContentManager.cpp | 5 +- CesiumGltf/include/CesiumGltf/Image.h | 5 +- CesiumGltf/include/CesiumGltf/ImageCesium.h | 5 +- .../CesiumGltf/PropertyTexturePropertyView.h | 4 +- .../include/CesiumGltf/PropertyTextureView.h | 9 +- CesiumGltf/include/CesiumGltf/SharedAsset.h | 92 +++++++ .../include/CesiumGltf/SharedAssetDepot.h | 258 +++--------------- .../include/CesiumGltf/SharedAssetDepots.h | 44 +++ CesiumGltf/include/CesiumGltf/TextureView.h | 23 +- CesiumGltf/src/PropertyTextureView.cpp | 8 +- CesiumGltf/src/TextureView.cpp | 21 +- CesiumGltf/test/TestFeatureIdTextureView.cpp | 170 ++++++------ CesiumGltf/test/TestPropertyTextureView.cpp | 55 ++-- .../include/CesiumGltfReader/GltfReader.h | 6 +- .../include/CesiumGltfReader/ImageDecoder.h | 4 +- CesiumGltfReader/src/GltfReader.cpp | 112 ++++---- CesiumGltfReader/src/ImageDecoder.cpp | 11 +- CesiumGltfReader/src/decodeDataUrls.cpp | 5 +- CesiumGltfReader/test/TestGltfReader.cpp | 22 +- .../src/QuantizedMeshLoader.cpp | 12 +- .../RasterOverlayTileProvider.h | 2 +- .../src/DebugColorizeTilesRasterOverlay.cpp | 2 +- .../src/QuadtreeRasterOverlayTileProvider.cpp | 65 +++-- .../src/RasterOverlayTileProvider.cpp | 14 +- .../src/RasterizedPolygonsOverlay.cpp | 2 +- .../src/TileMapServiceRasterOverlay.cpp | 2 +- .../test/TestAddRasterOverlayToGltf.cpp | 3 +- .../TestQuadtreeRasterOverlayTileProvider.cpp | 12 +- .../include/CesiumUtility/IntrusivePointer.h | 21 ++ 37 files changed, 520 insertions(+), 528 deletions(-) create mode 100644 CesiumGltf/include/CesiumGltf/SharedAsset.h create mode 100644 CesiumGltf/include/CesiumGltf/SharedAssetDepots.h diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 0d0ebff20..647a88c3a 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -184,10 +184,10 @@ class CESIUM3DTILESSELECTION_API Tileset final { /** * @brief Returns the {@link SharedAssetDepot} of this tileset. */ - CesiumGltf::SharedAssetDepot& getSharedAssetDepot() noexcept; + CesiumGltf::SharedAssetDepots& getSharedAssetDepot() noexcept; /** @copydoc Tileset::getSharedAssetDepot() */ - const CesiumGltf::SharedAssetDepot& getSharedAssetDepot() const noexcept; + const CesiumGltf::SharedAssetDepots& getSharedAssetDepot() const noexcept; /** * @brief Updates this view but waits for all tiles that meet sse to finish diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 4e2131f0d..a9eefaadd 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -175,7 +175,9 @@ int64_t Tile::computeByteSize() const noexcept { // sizeBytes is set in TilesetContentManager::ContentKindSetter, if not // sooner (e.g., by the renderer implementation). - bytes += image.cesium->sizeBytes; + if (image.pCesium) { + bytes += image.pCesium->sizeBytes; + } } } diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp index 31796498f..99b0b71b8 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp @@ -9,8 +9,8 @@ TileContentLoadInfo::TileContentLoadInfo( const std::shared_ptr& pPrepareRendererResources_, const std::shared_ptr& pLogger_, - const std::optional> - maybeAssetDepot_, + const CesiumUtility::IntrusivePointer + pAssetDepot_, const TilesetContentOptions& contentOptions_, const Tile& tile) : asyncSystem(asyncSystem_), @@ -20,7 +20,7 @@ TileContentLoadInfo::TileContentLoadInfo( tileID(tile.getTileID()), tileBoundingVolume(tile.getBoundingVolume()), tileContentBoundingVolume(tile.getContentBoundingVolume()), - maybeAssetDepot{maybeAssetDepot_}, + pAssetDepot{pAssetDepot_}, tileRefine(tile.getRefine()), tileGeometricError(tile.getGeometricError()), tileTransform(tile.getTransform()), diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.h b/Cesium3DTilesSelection/src/TileContentLoadInfo.h index 0245cc033..9240c66d2 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.h +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -24,7 +25,7 @@ struct TileContentLoadInfo { const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, - const std::optional> + const CesiumUtility::IntrusivePointer maybeAssetDepot, const TilesetContentOptions& contentOptions, const Tile& tile); @@ -42,7 +43,7 @@ struct TileContentLoadInfo { BoundingVolume tileBoundingVolume; std::optional tileContentBoundingVolume; - std::optional> maybeAssetDepot; + CesiumUtility::IntrusivePointer pAssetDepot; TileRefine tileRefine; diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index f63237011..4cd26a1f4 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -149,11 +149,11 @@ const RasterOverlayCollection& Tileset::getOverlays() const noexcept { return this->_pTilesetContentManager->getRasterOverlayCollection(); } -CesiumGltf::SharedAssetDepot& Tileset::getSharedAssetDepot() noexcept { +CesiumGltf::SharedAssetDepots& Tileset::getSharedAssetDepot() noexcept { return *this->_pTilesetContentManager->getSharedAssetDepot(); } -const CesiumGltf::SharedAssetDepot& +const CesiumGltf::SharedAssetDepots& Tileset::getSharedAssetDepot() const noexcept { return *this->_pTilesetContentManager->getSharedAssetDepot(); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 909a3fcc0..fdbf3c981 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -49,12 +49,15 @@ struct ContentKindSetter { void operator()(CesiumGltf::Model&& model) { for (CesiumGltf::Image& image : model.images) { + if (!image.pCesium) + continue; + // If the image size hasn't been overridden, store the pixelData // size now. We'll be adding this number to our total memory usage soon, // and remove it when the tile is later unloaded, and we must use // the same size in each case. - if (image.cesium->sizeBytes < 0) { - image.cesium->sizeBytes = int64_t(image.cesium->pixelData.size()); + if (image.pCesium->sizeBytes < 0) { + image.pCesium->sizeBytes = int64_t(image.pCesium->pixelData.size()); } } @@ -562,8 +565,8 @@ postProcessContentInWorkerThread( tileLoadInfo.contentOptions.ktx2TranscodeTargets; gltfOptions.applyTextureTransform = tileLoadInfo.contentOptions.applyTextureTransform; - if (tileLoadInfo.maybeAssetDepot.has_value()) { - gltfOptions.sharedAssets = *tileLoadInfo.maybeAssetDepot; + if (tileLoadInfo.pAssetDepot) { + gltfOptions.pSharedAssets = tileLoadInfo.pAssetDepot; } auto asyncSystem = tileLoadInfo.asyncSystem; @@ -663,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _assetDepot(std::make_shared()), + _pAssetDepot(new CesiumGltf::SharedAssetDepots()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -693,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _assetDepot(std::make_shared()), + _pAssetDepot(new CesiumGltf::SharedAssetDepots()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -845,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _assetDepot(std::make_shared()), + _pAssetDepot(new CesiumGltf::SharedAssetDepots()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -991,7 +994,7 @@ void TilesetContentManager::loadTileContent( this->_externals.pAssetAccessor, this->_externals.pPrepareRendererResources, this->_externals.pLogger, - std::optional(this->_assetDepot), + this->_pAssetDepot, tilesetOptions.contentOptions, tile}; @@ -1234,9 +1237,9 @@ TilesetContentManager::getTilesetCredits() const noexcept { return this->_tilesetCredits; } -const std::shared_ptr& +const CesiumUtility::IntrusivePointer& TilesetContentManager::getSharedAssetDepot() const noexcept { - return this->_assetDepot; + return this->_pAssetDepot; } int32_t TilesetContentManager::getNumberOfTilesLoading() const noexcept { diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 8d09e7986..ce36425e7 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -116,7 +116,7 @@ class TilesetContentManager const std::vector& getTilesetCredits() const noexcept; - const std::shared_ptr& + const CesiumUtility::IntrusivePointer& getSharedAssetDepot() const noexcept; int32_t getNumberOfTilesLoading() const noexcept; @@ -172,7 +172,7 @@ class TilesetContentManager int64_t _tilesDataUsed; // Stores assets that might be shared between tiles. - std::shared_ptr _assetDepot; + CesiumUtility::IntrusivePointer _pAssetDepot; CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index c9a673dc1..615cb5ce6 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include @@ -56,7 +56,7 @@ class TilesetJsonLoader : public TilesetContentLoader { private: std::string _baseUrl; CesiumGeospatial::Ellipsoid _ellipsoid; - std::optional> _maybeAssetDepot; + CesiumUtility::IntrusivePointer _pAssetDepot; /** * @brief The axis that was declared as the "up-axis" for glTF content. diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 07241d350..128ffc69d 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1298,7 +1298,8 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CesiumAsync::Future loadTileImage(RasterOverlayTile& overlayTile) override { - CesiumGltf::ImageCesium image{}; + CesiumUtility::IntrusivePointer pImage; + CesiumGltf::ImageCesium& image = pImage.emplace(); image.width = 1; image.height = 1; image.channels = 1; @@ -1307,7 +1308,7 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { return this->getAsyncSystem().createResolvedFuture( LoadedRasterOverlayImage{ - std::move(image), + std::move(pImage), overlayTile.getRectangle(), {}, {}, diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index 0e755d365..701836eff 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -11,8 +11,9 @@ namespace CesiumGltf { struct CESIUMGLTF_API Image final : public ImageSpec { /** * @brief Holds properties that are specific to the glTF loader rather than - * part of the glTF spec. + * part of the glTF spec. When an image is loaded from a URL, multiple `Image` + * instances may all point to the same `ImageCesium` instance. */ - SharedAsset cesium; + CesiumUtility::IntrusivePointer pCesium; }; } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index 991684188..469061429 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -2,7 +2,7 @@ #include "CesiumGltf/Ktx2TranscodeTargets.h" #include "CesiumGltf/Library.h" -#include "CesiumUtility/ExtensibleObject.h" +#include "CesiumGltf/SharedAsset.h" #include #include @@ -30,8 +30,7 @@ struct CESIUMGLTF_API ImageCesiumMipPosition { * @brief Holds {@link Image} properties that are specific to the glTF loader * rather than part of the glTF spec. */ -struct CESIUMGLTF_API ImageCesium final - : public CesiumUtility::ExtensibleObject { +struct CESIUMGLTF_API ImageCesium final : public SharedAsset { /** * @brief The width of the image in pixels. */ diff --git a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h index fd96b3602..f3dcd168e 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h @@ -298,7 +298,7 @@ class PropertyTexturePropertyView const PropertyTextureProperty& property, const ClassProperty& classProperty, const Sampler& sampler, - const SharedAsset& image, + const ImageCesium& image, const TextureViewOptions& options = TextureViewOptions()) noexcept : PropertyView(classProperty, property), TextureView( @@ -531,7 +531,7 @@ class PropertyTexturePropertyView const PropertyTextureProperty& property, const ClassProperty& classProperty, const Sampler& sampler, - const SharedAsset& image, + const ImageCesium& image, const TextureViewOptions& options = TextureViewOptions()) noexcept : PropertyView(classProperty, property), TextureView( diff --git a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h index d0b01c656..6fc8ec158 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h @@ -748,15 +748,16 @@ class PropertyTextureView { return PropertyTexturePropertyView(status); } - const SharedAsset& image = _pModel->images[imageIndex].cesium; + const CesiumUtility::IntrusivePointer& pImage = + _pModel->images[imageIndex].pCesium; const std::vector& channels = propertyTextureProperty.channels; - status = checkChannels(channels, *image); + status = checkChannels(channels, *pImage); if (status != PropertyTexturePropertyViewStatus::Valid) { return PropertyTexturePropertyView(status); } - if (channels.size() * image->bytesPerChannel != elementSize) { + if (channels.size() * pImage->bytesPerChannel != elementSize) { return PropertyTexturePropertyViewStatus::ErrorChannelsAndTypeMismatch; } @@ -764,7 +765,7 @@ class PropertyTextureView { propertyTextureProperty, classProperty, _pModel->samplers[samplerIndex], - image, + *pImage, propertyOptions); } diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h new file mode 100644 index 000000000..78bdbf197 --- /dev/null +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +#include + +namespace CesiumGltf { + +/** + * @brief An asset that is potentially shared between multiple objects, such as + * an image shared between multiple glTF models. This is intended to be the base + * class for such assets. + * + * The lifetime of instances of this class should be managed by reference + * counting with {@link IntrusivePointer}. + * + * @tparam T The type that is _deriving_ from this class. For example, you + * should declare your class as + * `class MyClass : public SharedAsset { ... };` + */ +template +class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { +public: + SharedAsset() = default; + + // Assets can be copied, but the fresh instance has no references and is not + // in the asset depot. + SharedAsset(const SharedAsset& rhs) + : ExtensibleObject(rhs), + _referenceCount(0), + _pDepot(nullptr), + _uniqueAssetId() {} + + // Move construction is not allowed, because existing references to this + // shared asset would be affected as well. + SharedAsset(SharedAsset&& rhs) = delete; + + // Assignment does not affect the asset's relationship with the depot, but is + // useful to assign the data in derived classes. + SharedAsset& operator=(const SharedAsset& rhs) { + CesiumUtility::ExtensibleObject::operator=(rhs); + return *this; + } + + SharedAsset& operator=(SharedAsset&& rhs) { + CesiumUtility::ExtensibleObject::operator=(std::move(rhs)); + return *this; + } + + /** + * @brief Adds a counted reference to this object. Use + * {@link CesiumUtility::IntrusivePointer} instead of calling this method + * directly. + */ + void addReference() const /*noexcept*/ { ++this->_referenceCount; } + + /** + * @brief Removes a counted reference from this object. When the last + * reference is removed, this method will delete this instance. Use + * {@link CesiumUtility::IntrusivePointer} instead of calling this method + * directly. + */ + void releaseReference() const /*noexcept*/ { + CESIUM_ASSERT(this->_referenceCount > 0); + const int32_t references = --this->_referenceCount; + if (references == 0) { + CesiumUtility::IntrusivePointer> pDepot = + this->_pDepot; + if (pDepot) { + // Let the depot manage this object's lifetime. + pDepot->markDeletionCandidate( + this->_uniqueAssetId, + const_cast(static_cast(this))); + } else { + // No depot, so destroy this object directly. + delete static_cast(this); + } + } + } + +private: + mutable std::atomic _referenceCount{0}; + CesiumUtility::IntrusivePointer> _pDepot; + std::string _uniqueAssetId; + + // To allow the depot to modify _pDepot and _uniqueAssetId. + friend class SharedAssetDepot; +}; + +} // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 4d324e775..5af8e03f4 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -1,7 +1,5 @@ #pragma once -#include "CesiumGltf/ImageCesium.h" - #include #include #include @@ -22,167 +20,23 @@ namespace CesiumGltf { -template class SharedAsset; -template class SingleAssetDepot; +template class SharedAsset; /** - * Contains the current state of an asset within the SharedAssetDepot. + * A depot for {@link SharedAsset} instances, which are potentially shared between multiple objects. + * @tparam AssetType The type of asset stored in this depot. This should usually + * be derived from {@link SharedAsset}. */ template -class AssetContainer - : public CesiumUtility::ReferenceCounted, true> { -public: - std::atomic counter; - std::string assetId; - AssetType asset; - - // Pointer to this container's parent so we know how to clean it up. - SingleAssetDepot* parent; - - AssetContainer( - std::string assetId_, - AssetType&& asset_, - SingleAssetDepot* parent_) - : counter(0), - assetId(assetId_), - asset(std::move(asset_)), - parent(parent_) {} - SharedAsset toRef() { return SharedAsset(this); } -}; - -/** - * An asset that may or may not be stored in the SharedAssetDepot and shared - * across multiple tiles. This can either be an ImageCesium itself or a - * reference-counted pointer to an entry in the SharedAssetDepot. You should - * always treat it as if it was an ImageCesium, not a smart pointer. - * @tparam AssetType The type of the asset that we're getting a reference to. - */ -template class SharedAsset { -public: - SharedAsset(AssetType&& asset) - : contents(new AssetContainer( - std::string(), - std::move(asset), - nullptr)) {} - SharedAsset(const AssetType& asset) - : contents(new AssetContainer( - std::string(), - AssetType(asset), - nullptr)) {} - SharedAsset(std::nullptr_t) : contents(nullptr) {} - SharedAsset() - : contents(new AssetContainer( - std::string(), - AssetType(), - nullptr)) {} - - AssetType* get() { return &this->contents->asset; } - - const AssetType* get() const { return &this->contents->asset; } - - AssetType* operator->() { return this->get(); } - const AssetType* operator->() const { return this->get(); } - AssetType& operator*() { return *this->get(); } - const AssetType& operator*() const { return *this->get(); } - - bool operator==(const SharedAsset& other) const { - return other.get() == this->get(); - } - - bool operator==(SharedAsset& other) { - return other.get() == this->get(); - } - - bool operator!=(const SharedAsset& other) const { - return other.get() != this->get(); - } - - bool operator!=(SharedAsset& other) { - return other.get() != this->get(); - } - - /** - * Copy assignment operator for SharedAsset. - */ - void operator=(const SharedAsset& other) { - if (*this != other) { - // Decrement this reference - this->changeCounter(-1); - this->contents = other.contents; - // Increment the new reference - this->changeCounter(1); - } - } - - /** - * Move assignment operator for SharedAsset. - */ - void operator=(SharedAsset&& other) noexcept { - if (*this != other) { - this->changeCounter(-1); - this->contents = std::move(other.contents); - other.contents = nullptr; - } - } - - ~SharedAsset() { this->changeCounter(-1); } - - /** - * Copy constructor. - */ - SharedAsset(const SharedAsset& other) { - contents = other.contents; - this->changeCounter(1); - } - - /** - * Move constructor. - */ - SharedAsset(SharedAsset&& other) noexcept { - contents = std::move(other.contents); - other.contents = nullptr; - } - -private: - SharedAsset( - CesiumUtility::IntrusivePointer> container) - : contents(container) { - this->changeCounter(1); - } - - void changeCounter(int amt) { - if (contents != nullptr) { - contents->counter += amt; - if (contents->counter <= 0) { - if (contents->parent != nullptr) { - contents->parent->markDeletionCandidate(contents->assetId); - } - contents = nullptr; - } - } - } - - // A SharedAssetRef might point to an asset the SharedAssetDepot, or it - // might not. If it doesn't, we own this asset now. - CesiumUtility::IntrusivePointer> contents; - - friend class SharedAssetDepot; - friend class AssetContainer; - friend class SingleAssetDepot; -}; - -/** - * Contains one or more assets of the given type. - * @tparam AssetType The type of asset stored in this depot. - */ -template class SingleAssetDepot { +class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< + SharedAssetDepot> { public: /** * @brief Number of seconds an asset can remain unused before it's deleted. */ float assetDeletionThreshold = 60.0f; - SingleAssetDepot() { + SharedAssetDepot() { this->lastDeletionTick = std::chrono::steady_clock::now(); } @@ -190,11 +44,11 @@ template class SingleAssetDepot { * Stores the AssetType in this depot and returns a reference to it, * or returns the existing asset if present. */ - SharedAsset store(std::string& assetId, AssetType&& asset) { - auto [newIt, added] = this->assets.try_emplace( - assetId, - new AssetContainer(assetId, std::move(asset), this)); - return newIt->second.toRef(); + CesiumUtility::IntrusivePointer store( + std::string& assetId, + const CesiumUtility::IntrusivePointer& pAsset) { + auto [newIt, added] = this->assets.try_emplace(assetId, pAsset); + return newIt->second; } /** @@ -204,9 +58,10 @@ template class SingleAssetDepot { * its future will be returned. */ template - CesiumAsync::SharedFuture>> getOrFetch( - CesiumAsync::AsyncSystem& asyncSystem, - std::shared_ptr& pAssetAccessor, + CesiumAsync::SharedFuture> + getOrFetch( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, const Factory& factory, const std::string& uri, const std::vector& headers) { @@ -222,8 +77,8 @@ template class SingleAssetDepot { if (existingIt != this->assets.end()) { // We've already loaded an asset with this ID - we can just use that. return asyncSystem - .createResolvedFuture(std::optional>( - SharedAsset(existingIt->second))) + .createResolvedFuture( + CesiumUtility::IntrusivePointer(existingIt->second)) .share(); } @@ -235,16 +90,16 @@ template class SingleAssetDepot { // We haven't loaded or started to load this asset yet. // Let's do that now. - CesiumAsync::Future>> future = + CesiumAsync::Future> future = pAssetAccessor->get(asyncSystem, uri, headers) .thenInWorkerThread( [factory = std::move(factory)]( std::shared_ptr&& pRequest) - -> std::optional { + -> CesiumUtility::IntrusivePointer { const CesiumAsync::IAssetResponse* pResponse = pRequest->response(); if (!pResponse) { - return std::nullopt; + return nullptr; } return factory.createFrom(pResponse->data()); @@ -255,36 +110,31 @@ template class SingleAssetDepot { this, pAssets = &this->assets, pPendingAssetsMutex = &this->assetsMutex, - pPendingAssets = - &this->pendingAssets](std::optional&& result) - -> std::optional> { + pPendingAssets = &this->pendingAssets]( + CesiumUtility::IntrusivePointer&& pResult) + -> CesiumUtility::IntrusivePointer { std::lock_guard lock(*pPendingAssetsMutex); // Get rid of our future. pPendingAssets->erase(uri); - if (result.has_value()) { - auto [it, ok] = pAssets->emplace( - uri, - new AssetContainer( - uri, - std::move(result.value()), - this)); + if (pResult) { + auto [it, ok] = pAssets->emplace(uri, pResult); if (!ok) { - return std::nullopt; + return nullptr; } - return std::optional( - std::move(SharedAsset(it->second))); + return it->second; } - return std::nullopt; + return nullptr; }); auto [it, ok] = this->pendingAssets.emplace(uri, std::move(future).share()); if (!ok) { return asyncSystem - .createResolvedFuture(std::optional>()) + .createResolvedFuture>( + nullptr) .share(); } @@ -360,27 +210,27 @@ template class SingleAssetDepot { } private: - // Disable copy - void operator=(const SharedAsset& other) = delete; + // Disable copy and move + void operator=(const SharedAssetDepot& other) = delete; /** * Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void markDeletionCandidate(const std::string& hash) { + void markDeletionCandidate( + const std::string& hash, + const CesiumUtility::IntrusivePointer& pAsset) { std::lock_guard lock(this->deletionCandidatesMutex); this->deletionCandidates.emplace(hash, 0.0f); } // Assets that have a unique ID that can be used to de-duplicate them. - std::unordered_map< - std::string, - CesiumUtility::IntrusivePointer>> + std::unordered_map> assets; // Futures for assets that still aren't loaded yet. std::unordered_map< std::string, - CesiumAsync::SharedFuture>>> + CesiumAsync::SharedFuture>> pendingAssets; // Mutex for checking or editing the pendingAssets map std::mutex assetsMutex; @@ -395,36 +245,4 @@ template class SingleAssetDepot { friend class SharedAsset; }; -/** - * @brief Contains assets that are potentially shared across multiple tiles. - */ -class SharedAssetDepot { -public: - SharedAssetDepot() = default; - void operator=(const SharedAssetDepot& other) = delete; - - /** - * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. - */ - template - CesiumAsync::SharedFuture>> getOrFetch( - CesiumAsync::AsyncSystem& asyncSystem, - std::shared_ptr& pAssetAccessor, - const Factory& factory, - const std::string& uri, - const std::vector& headers) { - return images - .getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); - } - - const SingleAssetDepot* getImageDepot() { - return &this->images; - } - - void deletionTick() { this->images.deletionTick(); } - -private: - SingleAssetDepot images; -}; - } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepots.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepots.h new file mode 100644 index 000000000..31737cf95 --- /dev/null +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepots.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace CesiumGltf { + +/** + * @brief Contains assets that are potentially shared across multiple glTF + * models. + */ +class SharedAssetDepots + : public CesiumUtility::ReferenceCountedThreadSafe { +public: + SharedAssetDepots() = default; + void operator=(const SharedAssetDepots& other) = delete; + + /** + * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. + */ + template + CesiumAsync::SharedFuture< + CesiumUtility::IntrusivePointer> + getOrFetch( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const Factory& factory, + const std::string& uri, + const std::vector& headers) { + return images + .getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); + } + + const SharedAssetDepot* getImageDepot() { + return &this->images; + } + + void deletionTick() { this->images.deletionTick(); } + +private: + SharedAssetDepot images; +}; + +} // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/TextureView.h b/CesiumGltf/include/CesiumGltf/TextureView.h index 3715401ea..35faa8938 100644 --- a/CesiumGltf/include/CesiumGltf/TextureView.h +++ b/CesiumGltf/include/CesiumGltf/TextureView.h @@ -1,10 +1,11 @@ #pragma once -#include "CesiumGltf/ImageCesium.h" -#include "CesiumGltf/KhrTextureTransform.h" -#include "CesiumGltf/Sampler.h" -#include "CesiumGltf/SharedAssetDepot.h" -#include "CesiumGltf/TextureInfo.h" +#include +#include +#include +#include +#include +#include #include @@ -125,7 +126,7 @@ class TextureView { */ TextureView( const Sampler& sampler, - const SharedAsset& image, + const ImageCesium& image, int64_t textureCoordinateSetIndex, const ExtensionKhrTextureTransform* pKhrTextureTransformExtension = nullptr, @@ -174,10 +175,10 @@ class TextureView { * problems during construction. */ const ImageCesium* getImage() const noexcept { - if (this->_imageCopy) { - return &(this->_imageCopy.value()); + if (this->_pImageCopy) { + return this->_pImageCopy.get(); } - return &*this->_pImage; + return this->_pImage.get(); } /** @@ -211,12 +212,12 @@ class TextureView { TextureViewStatus _textureViewStatus; const Sampler* _pSampler; - SharedAsset _pImage; + CesiumUtility::IntrusivePointer _pImage; int64_t _texCoordSetIndex; bool _applyTextureTransform; std::optional _textureTransform; - std::optional _imageCopy; + CesiumUtility::IntrusivePointer _pImageCopy; }; } // namespace CesiumGltf diff --git a/CesiumGltf/src/PropertyTextureView.cpp b/CesiumGltf/src/PropertyTextureView.cpp index f8c3db846..1b0da8aa2 100644 --- a/CesiumGltf/src/PropertyTextureView.cpp +++ b/CesiumGltf/src/PropertyTextureView.cpp @@ -80,14 +80,14 @@ PropertyTextureView::checkImage(const int32_t imageIndex) const noexcept { return PropertyTexturePropertyViewStatus::ErrorInvalidImage; } - const SharedAsset& image = - _pModel->images[static_cast(imageIndex)].cesium; + const CesiumUtility::IntrusivePointer& pImage = + _pModel->images[static_cast(imageIndex)].pCesium; - if (image->width < 1 || image->height < 1) { + if (pImage->width < 1 || pImage->height < 1) { return PropertyTexturePropertyViewStatus::ErrorEmptyImage; } - if (image->bytesPerChannel > 1) { + if (pImage->bytesPerChannel > 1) { return PropertyTexturePropertyViewStatus::ErrorInvalidBytesPerChannel; } diff --git a/CesiumGltf/src/TextureView.cpp b/CesiumGltf/src/TextureView.cpp index ce7901b9c..c41dfae0f 100644 --- a/CesiumGltf/src/TextureView.cpp +++ b/CesiumGltf/src/TextureView.cpp @@ -14,7 +14,7 @@ TextureView::TextureView() noexcept _texCoordSetIndex(-1), _applyTextureTransform(false), _textureTransform(std::nullopt), - _imageCopy(std::nullopt) {} + _pImageCopy(nullptr) {} TextureView::TextureView( const Model& model, @@ -26,7 +26,7 @@ TextureView::TextureView( _texCoordSetIndex(textureInfo.texCoord), _applyTextureTransform(options.applyKhrTextureTransformExtension), _textureTransform(std::nullopt), - _imageCopy(std::nullopt) { + _pImageCopy(nullptr) { int32_t textureIndex = textureInfo.index; if (textureIndex < 0 || static_cast(textureIndex) >= model.textures.size()) { @@ -41,7 +41,7 @@ TextureView::TextureView( return; } - this->_pImage = model.images[static_cast(texture.source)].cesium; + this->_pImage = model.images[static_cast(texture.source)].pCesium; if (this->_pImage->width < 1 || this->_pImage->height < 1) { this->_textureViewStatus = TextureViewStatus::ErrorEmptyImage; return; @@ -71,7 +71,8 @@ TextureView::TextureView( } if (options.makeImageCopy) { - this->_imageCopy = *this->_pImage; + this->_pImageCopy = + this->_pImage ? new ImageCesium(*this->_pImage) : nullptr; } this->_textureViewStatus = TextureViewStatus::Valid; @@ -79,17 +80,17 @@ TextureView::TextureView( TextureView::TextureView( const Sampler& sampler, - const SharedAsset& image, + const ImageCesium& image, int64_t textureCoordinateSetIndex, const ExtensionKhrTextureTransform* pKhrTextureTransformExtension, const TextureViewOptions& options) noexcept : _textureViewStatus(TextureViewStatus::ErrorUninitialized), _pSampler(&sampler), - _pImage(image), + _pImage(new ImageCesium(image)), _texCoordSetIndex(textureCoordinateSetIndex), _applyTextureTransform(options.applyKhrTextureTransformExtension), _textureTransform(std::nullopt), - _imageCopy(std::nullopt) { + _pImageCopy(nullptr) { // TODO: once compressed texture support is merged, check that the image is // decompressed here. @@ -104,7 +105,8 @@ TextureView::TextureView( } if (options.makeImageCopy) { - this->_imageCopy = *this->_pImage; + this->_pImageCopy = + this->_pImage ? new ImageCesium(*this->_pImage) : nullptr; } this->_textureViewStatus = TextureViewStatus::Valid; @@ -130,7 +132,8 @@ std::vector TextureView::sampleNearestPixel( u = applySamplerWrapS(u, this->_pSampler->wrapS); v = applySamplerWrapT(v, this->_pSampler->wrapT); - const ImageCesium& image = this->_imageCopy.value_or(*this->_pImage); + const ImageCesium& image = + this->_pImageCopy != nullptr ? *this->_pImageCopy : *this->_pImage; // For nearest filtering, std::floor is used instead of std::round. // This is because filtering is supposed to consider the pixel centers. But diff --git a/CesiumGltf/test/TestFeatureIdTextureView.cpp b/CesiumGltf/test/TestFeatureIdTextureView.cpp index 2df7bc40e..12bee9135 100644 --- a/CesiumGltf/test/TestFeatureIdTextureView.cpp +++ b/CesiumGltf/test/TestFeatureIdTextureView.cpp @@ -75,8 +75,9 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with empty image") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 0; - image.cesium->height = 0; + image.pCesium.emplace(); + image.pCesium->width = 0; + image.pCesium->height = 0; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -107,9 +108,10 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with too many bytes " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; - image.cesium->bytesPerChannel = 2; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; + image.pCesium->bytesPerChannel = 2; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -142,8 +144,9 @@ TEST_CASE( sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -174,8 +177,9 @@ TEST_CASE( sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -206,8 +210,9 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with out of range " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -237,8 +242,9 @@ TEST_CASE("Test FeatureIdTextureView on valid feature ID texture") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -269,8 +275,9 @@ TEST_CASE("Test FeatureIdTextureView with applyKhrTextureTransformExtension = " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -318,8 +325,9 @@ TEST_CASE("Test FeatureIdTextureView with applyKhrTextureTransformExtension = " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -371,13 +379,14 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(featureIDs.size()); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -403,14 +412,14 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { // Clear the original image data. std::vector emptyData; - image.cesium->pixelData.swap(emptyData); + image.pCesium->pixelData.swap(emptyData); const ImageCesium* pImage = view.getImage(); REQUIRE(pImage); - REQUIRE(pImage->width == image.cesium->width); - REQUIRE(pImage->height == image.cesium->height); - REQUIRE(pImage->channels == image.cesium->channels); - REQUIRE(pImage->bytesPerChannel == image.cesium->bytesPerChannel); + REQUIRE(pImage->width == image.pCesium->width); + REQUIRE(pImage->height == image.pCesium->height); + REQUIRE(pImage->channels == image.pCesium->channels); + REQUIRE(pImage->bytesPerChannel == image.pCesium->bytesPerChannel); REQUIRE(pImage->pixelData.size() == featureIDs.size()); } @@ -423,8 +432,9 @@ TEST_CASE("Test getFeatureID on invalid feature ID texture view") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 1; - image.cesium->height = 1; + image.pCesium.emplace(); + image.pCesium->width = 1; + image.pCesium->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -457,13 +467,14 @@ TEST_CASE("Test getFeatureID on valid feature ID texture view") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(featureIDs.size()); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -502,13 +513,14 @@ TEST_CASE("Test getFeatureID on view with applyKhrTextureTransformExtension = " std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(featureIDs.size()); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -553,13 +565,14 @@ TEST_CASE( std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(featureIDs.size()); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -609,13 +622,14 @@ TEST_CASE("Test getFeatureId on view with makeImageCopy = true") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(featureIDs.size()); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -641,7 +655,7 @@ TEST_CASE("Test getFeatureId on view with makeImageCopy = true") { // Clear the original image data. std::vector emptyData; - image.cesium->pixelData.swap(emptyData); + image.pCesium->pixelData.swap(emptyData); REQUIRE(view.getFeatureID(0, 0) == 1); REQUIRE(view.getFeatureID(1, 0) == 2); @@ -660,13 +674,14 @@ TEST_CASE("Test getFeatureID rounds to nearest pixel") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(featureIDs.size()); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(featureIDs.size()); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -704,12 +719,13 @@ TEST_CASE("Test getFeatureID clamps values") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; - auto& data = image.cesium->pixelData; + auto& data = image.pCesium->pixelData; data.resize(featureIDs.size() * sizeof(uint8_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); @@ -747,12 +763,13 @@ TEST_CASE("Test getFeatureID handles multiple channels") { std::vector featureIDs{260, 512, 8, 17}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 2; - image.cesium->bytesPerChannel = 1; + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 2; + image.pCesium->bytesPerChannel = 1; - auto& data = image.cesium->pixelData; + auto& data = image.pCesium->pixelData; data.resize(featureIDs.size() * sizeof(uint16_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); @@ -788,12 +805,13 @@ TEST_CASE("Check FeatureIdTextureView sampling with different wrap values") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = 1; - image.cesium->bytesPerChannel = 1; + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = 1; + image.pCesium->bytesPerChannel = 1; - auto& data = image.cesium->pixelData; + auto& data = image.pCesium->pixelData; data.resize(featureIDs.size() * sizeof(uint8_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); diff --git a/CesiumGltf/test/TestPropertyTextureView.cpp b/CesiumGltf/test/TestPropertyTextureView.cpp index 23a0f498d..81325270b 100644 --- a/CesiumGltf/test/TestPropertyTextureView.cpp +++ b/CesiumGltf/test/TestPropertyTextureView.cpp @@ -19,12 +19,13 @@ void addTextureToModel( int32_t channels, const std::vector& data) { Image& image = model.images.emplace_back(); - image.cesium->width = width; - image.cesium->height = height; - image.cesium->channels = channels; - image.cesium->bytesPerChannel = 1; + image.pCesium.emplace(); + image.pCesium->width = width; + image.pCesium->height = height; + image.pCesium->channels = channels; + image.pCesium->bytesPerChannel = 1; - std::vector& imageData = image.cesium->pixelData; + std::vector& imageData = image.pCesium->pixelData; imageData.resize(data.size()); std::memcpy(imageData.data(), data.data(), data.size()); @@ -248,7 +249,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -308,7 +309,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium->channels = 2; + model.images[imageIndex].pCesium->channels = 2; propertyTextureProperty.channels = {0, 1}; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); @@ -336,7 +337,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].cesium->bytesPerChannel = 2; + model.images[imageIndex].pCesium->bytesPerChannel = 2; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -345,7 +346,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Empty image") { - model.images[imageIndex].cesium->width = 0; + model.images[imageIndex].pCesium->width = 0; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -493,7 +494,7 @@ TEST_CASE("Test scalar PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -556,7 +557,7 @@ TEST_CASE("Test scalar PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium->channels = 2; + model.images[imageIndex].pCesium->channels = 2; propertyTextureProperty.channels = {0, 1}; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); @@ -689,7 +690,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -750,7 +751,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium->channels = 4; + model.images[imageIndex].pCesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); @@ -769,7 +770,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].cesium->bytesPerChannel = 2; + model.images[imageIndex].pCesium->bytesPerChannel = 2; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -903,7 +904,7 @@ TEST_CASE("Test vecN PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -973,7 +974,7 @@ TEST_CASE("Test vecN PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium->channels = 4; + model.images[imageIndex].pCesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); @@ -1151,7 +1152,7 @@ TEST_CASE("Test array PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -1220,7 +1221,7 @@ TEST_CASE("Test array PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium->channels = 4; + model.images[imageIndex].pCesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView> uint8ArrayProperty = view.getPropertyView>("TestClassProperty"); @@ -1239,7 +1240,7 @@ TEST_CASE("Test array PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].cesium->bytesPerChannel = 2; + model.images[imageIndex].pCesium->bytesPerChannel = 2; PropertyTexturePropertyView> uint8ArrayProperty = view.getPropertyView>("TestClassProperty"); REQUIRE( @@ -1422,7 +1423,7 @@ TEST_CASE("Test array PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -1492,7 +1493,7 @@ TEST_CASE("Test array PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].cesium->channels = 4; + model.images[imageIndex].pCesium->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView, true> uint8ArrayProperty = @@ -2244,7 +2245,7 @@ TEST_CASE("Test callback for scalar PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap( + model.images[model.images.size() - 1].pCesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2366,7 +2367,7 @@ TEST_CASE("Test callback for scalar PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap( + model.images[model.images.size() - 1].pCesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2497,7 +2498,7 @@ TEST_CASE("Test callback for vecN PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap( + model.images[model.images.size() - 1].pCesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2629,7 +2630,7 @@ TEST_CASE("Test callback for vecN PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap( + model.images[model.images.size() - 1].pCesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2782,7 +2783,7 @@ TEST_CASE("Test callback for array PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap( + model.images[model.images.size() - 1].pCesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2952,7 +2953,7 @@ TEST_CASE("Test callback for array PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].cesium->pixelData.swap( + model.images[model.images.size() - 1].pCesium->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index 16a6c4607..f3c3b4cc5 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -115,8 +115,8 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * appear in this glTF. If not present, assets will not be shared between * glTFs, even if they're loaded from the same URL. */ - std::variant> - sharedAssets = std::monostate(); + CesiumUtility::IntrusivePointer pSharedAssets = + nullptr; }; /** diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h index 57b7a6ace..76b783657 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -1,6 +1,8 @@ #include "CesiumGltf/ImageCesium.h" #include "CesiumGltfReader/Library.h" +#include + #include #include @@ -20,7 +22,7 @@ struct CESIUMGLTFREADER_API ImageReaderResult { * * This will be `std::nullopt` if the image could not be read. */ - std::optional image; + CesiumUtility::IntrusivePointer pImage; /** * @brief Error messages that occurred while trying to read the image. diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index c31b630d5..0d52739b8 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -49,16 +49,12 @@ struct ImageAssetFactory { ImageAssetFactory(const Ktx2TranscodeTargets& ktx2TranscodeTargets_) : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} - std::optional + CesiumUtility::IntrusivePointer createFrom(const gsl::span& data) const { ImageReaderResult imageResult = ImageDecoder::readImage(data, this->ktx2TranscodeTargets); - - if (imageResult.image) { - return std::optional(imageResult.image); - } - - return std::nullopt; + // TODO: report warnings and errors! + return imageResult.pImage; } private: @@ -290,7 +286,7 @@ void postprocess( } // Image has already been decoded - if (!image.cesium->pixelData.empty()) { + if (image.pCesium && !image.pCesium->pixelData.empty()) { continue; } @@ -324,9 +320,8 @@ void postprocess( readGltf.errors.end(), imageResult.errors.begin(), imageResult.errors.end()); - if (imageResult.image) { - image.cesium = - SharedAsset(std::move(imageResult.image.value())); + if (imageResult.pImage) { + image.pCesium = imageResult.pImage; } else { if (image.mimeType) { readGltf.errors.emplace_back( @@ -554,66 +549,57 @@ void CesiumGltfReader::GltfReader::postprocessGltf( if (image.uri && image.uri->substr(0, dataPrefixLength) != dataPrefix) { const std::string uri = Uri::resolve(baseUrl, *image.uri); - struct Operation { - ImageAssetFactory factory; - AsyncSystem& asyncSystem; - std::shared_ptr pAssetAccessor; - std::string uri; - std::vector headers; - - SharedFuture>> - operator()(std::monostate) { - // We don't have a depot, we have to fetch this the old way. - return pAssetAccessor->get(asyncSystem, uri, headers) - .thenInWorkerThread( - [uri = this->uri, pFactory = &this->factory]( - std::shared_ptr&& pRequest) { - const IAssetResponse* pResponse = pRequest->response(); - - if (pResponse) { - gsl::span bytes = pResponse->data(); - auto asset = pFactory->createFrom(bytes); - if (asset.has_value()) { - return std::optional>( - std::move(asset.value())); - } - } - - return std::optional>(); - }) - .share(); - } - - SharedFuture>> - operator()(std::shared_ptr depot) { - // We have a depot, this is easy! - return depot->getOrFetch( - asyncSystem, - pAssetAccessor, - factory, - uri, - headers); - } - }; - - SharedFuture>> future = - std::visit( - Operation{ - ImageAssetFactory(options.ktx2TranscodeTargets), + auto getAsset = + [&options]( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const std::string& uri, + const std::vector& headers) { + if (options.pSharedAssets == nullptr) { + // We don't have a depot, we have to fetch this the old way. + return pAssetAccessor->get(asyncSystem, uri, headers) + .thenInWorkerThread( + [uri, + options](std::shared_ptr&& pRequest) { + const IAssetResponse* pResponse = + pRequest->response(); + + CesiumUtility::IntrusivePointer pAsset = + nullptr; + + if (pResponse) { + gsl::span bytes = + pResponse->data(); + pAsset = + ImageAssetFactory(options.ktx2TranscodeTargets) + .createFrom(bytes); + } + + return pAsset; + }) + .share(); + } else { + // We have a depot, this is easy! + return options.pSharedAssets->getOrFetch( asyncSystem, pAssetAccessor, + ImageAssetFactory(options.ktx2TranscodeTargets), uri, - tHeaders}, - options.sharedAssets); + headers); + } + }; + + SharedFuture> future = + getAsset(asyncSystem, pAssetAccessor, uri, tHeaders); resolvedBuffers.push_back(future.thenInWorkerThread( - [pImage = &image]( - std::optional> maybeLoadedImage) { + [pImage = + &image](const IntrusivePointer& pLoadedImage) { std::string imageUri = *pImage->uri; pImage->uri = std::nullopt; - if (maybeLoadedImage.has_value()) { - pImage->cesium = std::move(maybeLoadedImage.value()); + if (pLoadedImage) { + pImage->pCesium = pLoadedImage; return ExternalBufferLoadResult{true, imageUri}; } diff --git a/CesiumGltfReader/src/ImageDecoder.cpp b/CesiumGltfReader/src/ImageDecoder.cpp index 73cbf9fa3..a55549473 100644 --- a/CesiumGltfReader/src/ImageDecoder.cpp +++ b/CesiumGltfReader/src/ImageDecoder.cpp @@ -77,8 +77,7 @@ ImageReaderResult ImageDecoder::readImage( ImageReaderResult result; - result.image.emplace(); - CesiumGltf::ImageCesium& image = result.image.value(); + CesiumGltf::ImageCesium& image = result.pImage.emplace(); if (isKtx(data)) { ktxTexture2* pTexture = nullptr; @@ -265,7 +264,7 @@ ImageReaderResult ImageDecoder::readImage( } } - result.image.reset(); + result.pImage = nullptr; result.errors.emplace_back( "KTX2 loading failed with error: " + std::string(ktxErrorString(errorCode))); @@ -289,7 +288,7 @@ ImageReaderResult ImageDecoder::readImage( image.pixelData.size(), image.width * image.channels); if (!pImage) { - result.image.reset(); + result.pImage = nullptr; result.errors.emplace_back("Unable to decode WebP"); } return result; @@ -324,7 +323,7 @@ ImageReaderResult ImageDecoder::readImage( TJPF_RGBA, 0)) { result.errors.emplace_back("Unable to decode JPEG"); - result.image.reset(); + result.pImage = nullptr; } } else { CESIUM_TRACE("Decode PNG"); @@ -355,7 +354,7 @@ ImageReaderResult ImageDecoder::readImage( std::copy(pImage, pImage + lastByte, u8Pointer); stbi_image_free(pImage); } else { - result.image.reset(); + result.pImage = nullptr; result.errors.emplace_back(stbi_failure_reason()); } } diff --git a/CesiumGltfReader/src/decodeDataUrls.cpp b/CesiumGltfReader/src/decodeDataUrls.cpp index 4def2f4ed..10af277b5 100644 --- a/CesiumGltfReader/src/decodeDataUrls.cpp +++ b/CesiumGltfReader/src/decodeDataUrls.cpp @@ -137,12 +137,11 @@ void decodeDataUrls( ImageReaderResult imageResult = reader.readImage(decoded.value().data, options.ktx2TranscodeTargets); - if (!imageResult.image) { + if (!imageResult.pImage) { continue; } - CesiumGltf::ImageCesium& imageCesium = imageResult.image.value(); - image.cesium = std::move(imageCesium); + image.pCesium = imageResult.pImage; if (options.clearDecodedDataUrls) { image.uri.reset(); diff --git a/CesiumGltfReader/test/TestGltfReader.cpp b/CesiumGltfReader/test/TestGltfReader.cpp index 1126015f6..01fa6b0dd 100644 --- a/CesiumGltfReader/test/TestGltfReader.cpp +++ b/CesiumGltfReader/test/TestGltfReader.cpp @@ -572,9 +572,9 @@ TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { std::vector data = readFile(ktx2File.string()); ImageReaderResult imageResult = GltfReader::readImage(data, Ktx2TranscodeTargets{}); - REQUIRE(imageResult.image.has_value()); + REQUIRE(imageResult.pImage); - const ImageCesium& image = *imageResult.image; + const ImageCesium& image = *imageResult.pImage; REQUIRE(image.mipPositions.size() == 1); CHECK(image.mipPositions[0].byteOffset == 0); CHECK(image.mipPositions[0].byteSize > 0); @@ -592,9 +592,9 @@ TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { std::vector data = readFile(ktx2File.string()); ImageReaderResult imageResult = GltfReader::readImage(data, Ktx2TranscodeTargets{}); - REQUIRE(imageResult.image.has_value()); + REQUIRE(imageResult.pImage); - const ImageCesium& image = *imageResult.image; + const ImageCesium& image = *imageResult.pImage; REQUIRE(image.mipPositions.size() == 0); CHECK(image.pixelData.size() > 0); } @@ -606,9 +606,9 @@ TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { std::vector data = readFile(ktx2File.string()); ImageReaderResult imageResult = GltfReader::readImage(data, Ktx2TranscodeTargets{}); - REQUIRE(imageResult.image.has_value()); + REQUIRE(imageResult.pImage); - const ImageCesium& image = *imageResult.image; + const ImageCesium& image = *imageResult.pImage; REQUIRE(image.mipPositions.size() == 9); CHECK(image.mipPositions[0].byteSize > 0); CHECK( @@ -689,7 +689,7 @@ TEST_CASE("Decodes images with data uris") { REQUIRE(model.images.size() == 1); - const ImageCesium& image = *model.images.front().cesium; + const ImageCesium& image = *model.images.front().pCesium; CHECK(image.width == 256); CHECK(image.height == 256); @@ -782,9 +782,9 @@ TEST_CASE("GltfReader::loadGltf") { REQUIRE(result.model->images.size() == 1); const CesiumGltf::Image& image = result.model->images[0]; - CHECK(image.cesium->width == 2048); - CHECK(image.cesium->height == 2048); - CHECK(image.cesium->pixelData.size() == 2048 * 2048 * 4); + CHECK(image.pCesium->width == 2048); + CHECK(image.pCesium->height == 2048); + CHECK(image.pCesium->pixelData.size() == 2048 * 2048 * 4); CHECK(!result.model->buffers.empty()); for (const CesiumGltf::Buffer& buffer : result.model->buffers) { @@ -810,7 +810,7 @@ TEST_CASE("GltfReader::loadGltf") { REQUIRE(result.model->images.size() == 1); const CesiumGltf::Image& image = result.model->images[0]; CHECK(image.uri.has_value()); - CHECK(image.cesium->pixelData.empty()); + CHECK(!image.pCesium); } } diff --git a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp index 5a1a36962..b9c08d3f7 100644 --- a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp +++ b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp @@ -1073,13 +1073,13 @@ static std::vector generateNormals( const size_t waterMaskImageId = model.images.size(); model.images.emplace_back(); CesiumGltf::Image& waterMaskImage = model.images[waterMaskImageId]; - waterMaskImage.cesium->width = 256; - waterMaskImage.cesium->height = 256; - waterMaskImage.cesium->channels = 1; - waterMaskImage.cesium->bytesPerChannel = 1; - waterMaskImage.cesium->pixelData.resize(65536); + waterMaskImage.pCesium->width = 256; + waterMaskImage.pCesium->height = 256; + waterMaskImage.pCesium->channels = 1; + waterMaskImage.pCesium->bytesPerChannel = 1; + waterMaskImage.pCesium->pixelData.resize(65536); std::memcpy( - waterMaskImage.cesium->pixelData.data(), + waterMaskImage.pCesium->pixelData.data(), meshView->waterMaskBuffer.data(), 65536); diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h index f232c5b8d..daf143391 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h @@ -32,7 +32,7 @@ struct CESIUMRASTEROVERLAYS_API LoadedRasterOverlayImage { * This will be an empty optional if the loading failed. In this case, * the `errors` vector will contain the corresponding error messages. */ - std::optional image{}; + CesiumUtility::IntrusivePointer pImage{nullptr}; /** * @brief The projected rectangle defining the bounds of this image. diff --git a/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp b/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp index 6ca6b7da1..fb728490c 100644 --- a/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp @@ -42,7 +42,7 @@ class DebugTileProvider : public RasterOverlayTileProvider { result.rectangle = overlayTile.getRectangle(); - ImageCesium& image = result.image.emplace(); + ImageCesium& image = result.pImage.emplace(); image.width = 1; image.height = 1; image.channels = 4; diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index 28d407801..9e4c95333 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -328,21 +328,21 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( asyncSystem = this->getAsyncSystem(), loadParentTile = std::move(loadParentTile)]( LoadedRasterOverlayImage&& loaded) { - if (loaded.image && !loaded.errorList.hasErrors() && - loaded.image->width > 0 && loaded.image->height > 0) { + if (loaded.pImage && !loaded.errorList.hasErrors() && + loaded.pImage->width > 0 && loaded.pImage->height > 0) { // Successfully loaded, continue. - cachedBytes += int64_t(loaded.image->pixelData.size()); + cachedBytes += int64_t(loaded.pImage->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; + for (int32_t j = 0; j < loaded.pImage->height; ++j) { + for (int32_t i = 0; i < loaded.pImage->width; ++i) { + if (i == 0 || j == 0 || i == loaded.pImage->width - 1 || + j == loaded.pImage->height - 1) { + pixels[j * loaded.pImage->width + i] = 0xFF0000FF; } } } @@ -462,8 +462,7 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( images.begin(), images.end(), [](const LoadedQuadtreeImage& image) { - return image.pLoaded->image.has_value() && - !image.subset.has_value(); + return image.pLoaded->pImage && !image.subset.has_value(); }); if (!haveAnyUsefulImageData) { @@ -479,7 +478,7 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( } } return LoadedRasterOverlayImage{ - ImageCesium(), + new ImageCesium(), Rectangle(), {}, std::move(errors), @@ -523,8 +522,8 @@ void QuadtreeRasterOverlayTileProvider::unloadCachedTiles() { // If this is the last use of this data, it will be freed when the shared // pointer goes out of scope, so reduce the cachedBytes accordingly. if (pImage.use_count() == 1) { - if (pImage->image) { - this->_cachedBytes -= int64_t(pImage->image->pixelData.size()); + if (pImage->pImage) { + this->_cachedBytes -= int64_t(pImage->pImage->pixelData.size()); CESIUM_ASSERT(this->_cachedBytes >= 0); } } @@ -550,28 +549,28 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage( int32_t bytesPerChannel = -1; for (const LoadedQuadtreeImage& image : images) { const LoadedRasterOverlayImage& loaded = *image.pLoaded; - if (!loaded.image || loaded.image->width <= 0 || - loaded.image->height <= 0) { + if (!loaded.pImage || loaded.pImage->width <= 0 || + loaded.pImage->height <= 0) { continue; } projectedWidthPerPixel = glm::min( projectedWidthPerPixel, - loaded.rectangle.computeWidth() / loaded.image->width); + loaded.rectangle.computeWidth() / loaded.pImage->width); projectedHeightPerPixel = glm::min( projectedHeightPerPixel, - loaded.rectangle.computeHeight() / loaded.image->height); + loaded.rectangle.computeHeight() / loaded.pImage->height); - channels = glm::max(channels, loaded.image->channels); - bytesPerChannel = glm::max(bytesPerChannel, loaded.image->bytesPerChannel); + channels = glm::max(channels, loaded.pImage->channels); + bytesPerChannel = glm::max(bytesPerChannel, loaded.pImage->bytesPerChannel); } std::optional combinedRectangle; for (const LoadedQuadtreeImage& image : images) { const LoadedRasterOverlayImage& loaded = *image.pLoaded; - if (!loaded.image || loaded.image->width <= 0 || - loaded.image->height <= 0) { + if (!loaded.pImage || loaded.pImage->width <= 0 || + loaded.pImage->height <= 0) { continue; } @@ -669,7 +668,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( if (targetImageBytes <= 0) { // Target image has no pixels, so our work here is done. return LoadedRasterOverlayImage{ - std::nullopt, + nullptr, targetRectangle, {}, std::move(errors), @@ -682,7 +681,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( result.moreDetailAvailable = false; result.errorList = std::move(errors); - ImageCesium& target = result.image.emplace(); + ImageCesium& target = result.pImage.emplace(); target.bytesPerChannel = measurements.bytesPerChannel; target.channels = measurements.channels; target.width = measurements.widthPixels; @@ -692,7 +691,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( for (auto it = images.begin(); it != images.end(); ++it) { const LoadedRasterOverlayImage& loaded = *it->pLoaded; - if (!loaded.image) { + if (!loaded.pImage) { continue; } @@ -701,7 +700,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( blitImage( target, result.rectangle, - *loaded.image, + *loaded.pImage, loaded.rectangle, it->subset); } @@ -709,7 +708,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( size_t combinedCreditsCount = 0; for (auto it = images.begin(); it != images.end(); ++it) { const LoadedRasterOverlayImage& loaded = *it->pLoaded; - if (!loaded.image) { + if (!loaded.pImage) { continue; } @@ -719,7 +718,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( result.credits.reserve(combinedCreditsCount); for (auto it = images.begin(); it != images.end(); ++it) { const LoadedRasterOverlayImage& loaded = *it->pLoaded; - if (!loaded.image) { + if (!loaded.pImage) { continue; } @@ -731,12 +730,12 @@ QuadtreeRasterOverlayTileProvider::combineImages( // Highlight the edges in yellow to show tile boundaries. #if SHOW_TILE_BOUNDARIES gsl::span pixels = - reintepretCastSpan(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] = 0xFF00FFFF; + reintepretCastSpan(result.pImage->pixelData); + for (int32_t j = 0; j < result.pImage->height; ++j) { + for (int32_t i = 0; i < result.pImage->width; ++i) { + if (i == 0 || j == 0 || i == result.pImage->width - 1 || + j == result.pImage->height - 1) { + pixels[j * result.pImage->width + i] = 0xFF00FFFF; } } } diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 834cb789c..3ba53e689 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -140,7 +140,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( errors.emplaceError( fmt::format("Image request for {} failed.", pRequest->url())); return LoadedRasterOverlayImage{ - std::nullopt, + nullptr, options.rectangle, std::move(options.credits), std::move(errors), @@ -156,7 +156,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( pResponse->statusCode(), pRequest->url())); return LoadedRasterOverlayImage{ - std::nullopt, + nullptr, options.rectangle, std::move(options.credits), std::move(errors), @@ -166,7 +166,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( if (pResponse->data().empty()) { if (options.allowEmptyImages) { return LoadedRasterOverlayImage{ - CesiumGltf::ImageCesium(), + new CesiumGltf::ImageCesium(), options.rectangle, std::move(options.credits), {}, @@ -178,7 +178,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( "Image response for {} is empty.", pRequest->url())); return LoadedRasterOverlayImage{ - std::nullopt, + nullptr, options.rectangle, std::move(options.credits), std::move(errors), @@ -200,7 +200,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( } return LoadedRasterOverlayImage{ - loadedImage.image, + loadedImage.pImage, options.rectangle, std::move(options.credits), ErrorList{ @@ -248,7 +248,7 @@ static LoadResult createLoadResultFromLoadedImage( const std::shared_ptr& pLogger, LoadedRasterOverlayImage&& loadedImage, const std::any& rendererOptions) { - if (!loadedImage.image.has_value()) { + if (!loadedImage.pImage) { loadedImage.errorList.logError(pLogger, "Failed to load image for tile"); LoadResult result; result.state = RasterOverlayTile::LoadState::Failed; @@ -267,7 +267,7 @@ static LoadResult createLoadResultFromLoadedImage( "Warnings while loading image for tile"); } - CesiumGltf::ImageCesium& image = loadedImage.image.value(); + CesiumGltf::ImageCesium& image = *loadedImage.pImage; const int32_t bytesPerPixel = image.channels * image.bytesPerChannel; const int64_t requiredBytes = diff --git a/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp b/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp index 6a46e4eba..d8da4114c 100644 --- a/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp +++ b/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp @@ -24,7 +24,7 @@ void rasterizePolygons( const std::vector& cartographicPolygons, bool invertSelection) { - CesiumGltf::ImageCesium& image = loaded.image.emplace(); + CesiumGltf::ImageCesium& image = loaded.pImage.emplace(); std::byte insideColor; std::byte outsideColor; diff --git a/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp index b81b103ce..42634aaab 100644 --- a/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/TileMapServiceRasterOverlay.cpp @@ -95,7 +95,7 @@ class TileMapServiceTileProvider final errors.emplaceError("Failed to load image from TMS."); return this->getAsyncSystem() .createResolvedFuture( - {std::nullopt, + {nullptr, options.rectangle, {}, std::move(errors), diff --git a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp index d0664d233..a6427a1d9 100644 --- a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp +++ b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp @@ -266,7 +266,8 @@ TEST_CASE("Add raster overlay to glTF") { const Model& gltfBack = *resultBack.model; REQUIRE(gltfBack.images.size() == 1); - CHECK(!gltfBack.images[0].cesium->pixelData.empty()); + REQUIRE(gltfBack.images[0].pCesium != nullptr); + CHECK(!gltfBack.images[0].pCesium->pixelData.empty()); REQUIRE(!gltfBack.meshes.empty()); REQUIRE(!gltfBack.meshes[0].primitives.empty()); diff --git a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp index 594014ddf..ddfe60440 100644 --- a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp @@ -63,12 +63,12 @@ class TestTileProvider : public QuadtreeRasterOverlayTileProvider { } else { // Return an image where every component of every pixel is equal to the // tile level. - result.image.emplace(); - result.image->width = int32_t(this->getWidth()); - result.image->height = int32_t(this->getHeight()); - result.image->bytesPerChannel = 1; - result.image->channels = 4; - result.image->pixelData.resize( + result.pImage.emplace(); + result.pImage->width = int32_t(this->getWidth()); + result.pImage->height = int32_t(this->getHeight()); + result.pImage->bytesPerChannel = 1; + result.pImage->channels = 4; + result.pImage->pixelData.resize( this->getWidth() * this->getHeight() * 4, std::byte(tileID.level)); } diff --git a/CesiumUtility/include/CesiumUtility/IntrusivePointer.h b/CesiumUtility/include/CesiumUtility/IntrusivePointer.h index 5a0169d5f..24bb22d0f 100644 --- a/CesiumUtility/include/CesiumUtility/IntrusivePointer.h +++ b/CesiumUtility/include/CesiumUtility/IntrusivePointer.h @@ -68,6 +68,27 @@ template class IntrusivePointer final { */ ~IntrusivePointer() noexcept { this->releaseReference(); } + /** + * @brief Constructs a new instance and assigns it to this IntrusivePointer. + * If this IntrusivePointer already points to another instance, + * {@link releaseReference} is called on it. + * + * @param constructorArguments The arguments to the constructor to create the + * new instance. + * @return A reference to the newly-created instance. + */ + template + T& emplace(ConstructorArgumentTypes&&... constructorArguments) { + *this = + new T(std::forward(constructorArguments)...); + return *this->get(); + } + + /** + * @brief Reset this pointer to nullptr. + */ + void reset() { *this = nullptr; } + /** * @brief Assignment operator. */ From ab21441ce42bc508c99f0badd6a50612bb5995c8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 18:50:27 +1000 Subject: [PATCH 33/81] Fix water mask bug, add assets to depot. --- CesiumGltf/include/CesiumGltf/SharedAsset.h | 12 ++++++++++++ CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 3 +++ .../src/QuantizedMeshLoader.cpp | 1 + 3 files changed, 16 insertions(+) diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index 78bdbf197..fe0d9579f 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -80,6 +80,18 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { } } + /** + * @brief Determines if this image is shareable because it is managed by an + * asset depot. An image that is not shareable can be understood to be + * exclusively owned by, for example, the glTF that references it. If it is + * shareable, then potentially multiple glTFs reference it. + * + * An example of a non-shareable asset is an image embedded in a Binary glTF + * (GLB) buffer. An example of a shareable asset is an image referenced in a + * glTF by URI. + */ + bool isShareable() const { return this->_pDepot != nullptr; } + private: mutable std::atomic _referenceCount{0}; CesiumUtility::IntrusivePointer> _pDepot; diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 5af8e03f4..66d983782 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -119,6 +119,9 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< pPendingAssets->erase(uri); if (pResult) { + pResult->_pDepot = this; + pResult->_uniqueAssetId = uri; + auto [it, ok] = pAssets->emplace(uri, pResult); if (!ok) { return nullptr; diff --git a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp index b9c08d3f7..ef05516a9 100644 --- a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp +++ b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp @@ -1073,6 +1073,7 @@ static std::vector generateNormals( const size_t waterMaskImageId = model.images.size(); model.images.emplace_back(); CesiumGltf::Image& waterMaskImage = model.images[waterMaskImageId]; + waterMaskImage.pCesium.emplace(); waterMaskImage.pCesium->width = 256; waterMaskImage.pCesium->height = 256; waterMaskImage.pCesium->channels = 1; From 275bac03251ccb1e01d419e402036d97893a77e0 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 21:16:11 +1000 Subject: [PATCH 34/81] Better naming. --- .../include/Cesium3DTilesSelection/Tileset.h | 6 ++-- .../src/TileContentLoadInfo.cpp | 2 +- .../src/TileContentLoadInfo.h | 6 ++-- Cesium3DTilesSelection/src/Tileset.cpp | 10 +++---- .../src/TilesetContentManager.cpp | 10 +++---- .../src/TilesetContentManager.h | 6 ++-- .../src/TilesetJsonLoader.h | 4 +-- .../test/TestTilesetContentManager.cpp | 4 +-- .../include/CesiumGltf/SharedAssetDepot.h | 10 +++---- ...haredAssetDepots.h => SharedAssetSystem.h} | 28 +++++++++++-------- CesiumGltf/src/SharedAssetSystem.cpp | 17 +++++++++++ .../include/CesiumGltfReader/GltfReader.h | 4 +-- 12 files changed, 62 insertions(+), 45 deletions(-) rename CesiumGltf/include/CesiumGltf/{SharedAssetDepots.h => SharedAssetSystem.h} (58%) create mode 100644 CesiumGltf/src/SharedAssetSystem.cpp diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 647a88c3a..09dbe01ac 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -184,10 +184,10 @@ class CESIUM3DTILESSELECTION_API Tileset final { /** * @brief Returns the {@link SharedAssetDepot} of this tileset. */ - CesiumGltf::SharedAssetDepots& getSharedAssetDepot() noexcept; + CesiumGltf::SharedAssetSystem& getSharedAssetSystem() noexcept; - /** @copydoc Tileset::getSharedAssetDepot() */ - const CesiumGltf::SharedAssetDepots& getSharedAssetDepot() const noexcept; + /** @copydoc Tileset::getSharedAssetSystem() */ + const CesiumGltf::SharedAssetSystem& getSharedAssetSystem() const noexcept; /** * @brief Updates this view but waits for all tiles that meet sse to finish diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp index 99b0b71b8..b09c988cb 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp @@ -9,7 +9,7 @@ TileContentLoadInfo::TileContentLoadInfo( const std::shared_ptr& pPrepareRendererResources_, const std::shared_ptr& pLogger_, - const CesiumUtility::IntrusivePointer + const CesiumUtility::IntrusivePointer pAssetDepot_, const TilesetContentOptions& contentOptions_, const Tile& tile) diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.h b/Cesium3DTilesSelection/src/TileContentLoadInfo.h index 9240c66d2..a887d1c60 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.h +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.h @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -25,7 +25,7 @@ struct TileContentLoadInfo { const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, - const CesiumUtility::IntrusivePointer + const CesiumUtility::IntrusivePointer maybeAssetDepot, const TilesetContentOptions& contentOptions, const Tile& tile); @@ -43,7 +43,7 @@ struct TileContentLoadInfo { BoundingVolume tileBoundingVolume; std::optional tileContentBoundingVolume; - CesiumUtility::IntrusivePointer pAssetDepot; + CesiumUtility::IntrusivePointer pAssetDepot; TileRefine tileRefine; diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 4cd26a1f4..f5b1b7b5c 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -149,13 +149,13 @@ const RasterOverlayCollection& Tileset::getOverlays() const noexcept { return this->_pTilesetContentManager->getRasterOverlayCollection(); } -CesiumGltf::SharedAssetDepots& Tileset::getSharedAssetDepot() noexcept { - return *this->_pTilesetContentManager->getSharedAssetDepot(); +CesiumGltf::SharedAssetSystem& Tileset::getSharedAssetSystem() noexcept { + return *this->_pTilesetContentManager->getSharedAssetSystem(); } -const CesiumGltf::SharedAssetDepots& -Tileset::getSharedAssetDepot() const noexcept { - return *this->_pTilesetContentManager->getSharedAssetDepot(); +const CesiumGltf::SharedAssetSystem& +Tileset::getSharedAssetSystem() const noexcept { + return *this->_pTilesetContentManager->getSharedAssetSystem(); } static bool diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index fdbf3c981..cf0a8f223 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -666,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pAssetDepot(new CesiumGltf::SharedAssetDepots()), + _pAssetDepot(new CesiumGltf::SharedAssetSystem()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pAssetDepot(new CesiumGltf::SharedAssetDepots()), + _pAssetDepot(new CesiumGltf::SharedAssetSystem()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pAssetDepot(new CesiumGltf::SharedAssetDepots()), + _pAssetDepot(new CesiumGltf::SharedAssetSystem()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -1237,8 +1237,8 @@ TilesetContentManager::getTilesetCredits() const noexcept { return this->_tilesetCredits; } -const CesiumUtility::IntrusivePointer& -TilesetContentManager::getSharedAssetDepot() const noexcept { +const CesiumUtility::IntrusivePointer& +TilesetContentManager::getSharedAssetSystem() const noexcept { return this->_pAssetDepot; } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index ce36425e7..5a58e77d6 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -116,8 +116,8 @@ class TilesetContentManager const std::vector& getTilesetCredits() const noexcept; - const CesiumUtility::IntrusivePointer& - getSharedAssetDepot() const noexcept; + const CesiumUtility::IntrusivePointer& + getSharedAssetSystem() const noexcept; int32_t getNumberOfTilesLoading() const noexcept; @@ -172,7 +172,7 @@ class TilesetContentManager int64_t _tilesDataUsed; // Stores assets that might be shared between tiles. - CesiumUtility::IntrusivePointer _pAssetDepot; + CesiumUtility::IntrusivePointer _pAssetDepot; CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index 615cb5ce6..0039edc6e 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include @@ -56,7 +56,7 @@ class TilesetJsonLoader : public TilesetContentLoader { private: std::string _baseUrl; CesiumGeospatial::Ellipsoid _ellipsoid; - CesiumUtility::IntrusivePointer _pAssetDepot; + CesiumUtility::IntrusivePointer _pAssetDepot; /** * @brief The axis that was declared as the "up-axis" for glTF content. diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 128ffc69d..f297c54d4 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1682,9 +1682,7 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CHECK(images.size() == 1); } - CHECK( - pManager->getSharedAssetDepot()->getImageDepot()->getDistinctCount() == - 2); + CHECK(pManager->getSharedAssetSystem()->image().getDistinctCount() == 2); // unload the tile content for (auto& child : containerTile.getChildren()) { diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 66d983782..f039a0a3f 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -9,14 +9,12 @@ #include #include -#include -#include #include +#include #include #include #include -#include -#include +#include namespace CesiumGltf { @@ -184,8 +182,8 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< */ size_t getUsageCount() const { size_t count = 0; - for (auto& [key, item] : assets) { - count += item->counter; + for (const auto& [key, item] : assets) { + count += item->_referenceCount; } return count; } diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepots.h b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h similarity index 58% rename from CesiumGltf/include/CesiumGltf/SharedAssetDepots.h rename to CesiumGltf/include/CesiumGltf/SharedAssetSystem.h index 31737cf95..a116b9136 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepots.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h @@ -1,19 +1,24 @@ #pragma once #include -#include +#include namespace CesiumGltf { +struct ImageCesium; + /** * @brief Contains assets that are potentially shared across multiple glTF * models. */ -class SharedAssetDepots - : public CesiumUtility::ReferenceCountedThreadSafe { +class SharedAssetSystem + : public CesiumUtility::ReferenceCountedThreadSafe { public: - SharedAssetDepots() = default; - void operator=(const SharedAssetDepots& other) = delete; + SharedAssetSystem() noexcept; + ~SharedAssetSystem() noexcept; + + SharedAssetSystem(const SharedAssetSystem&) = delete; + void operator=(const SharedAssetSystem& other) = delete; /** * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. @@ -27,18 +32,17 @@ class SharedAssetDepots const Factory& factory, const std::string& uri, const std::vector& headers) { - return images - .getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); + return this->_pImages + ->getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); } - const SharedAssetDepot* getImageDepot() { - return &this->images; - } + const SharedAssetDepot& image(); - void deletionTick() { this->images.deletionTick(); } + void deletionTick(); private: - SharedAssetDepot images; + CesiumUtility::IntrusivePointer> + _pImages; }; } // namespace CesiumGltf diff --git a/CesiumGltf/src/SharedAssetSystem.cpp b/CesiumGltf/src/SharedAssetSystem.cpp new file mode 100644 index 000000000..7a3dd69b4 --- /dev/null +++ b/CesiumGltf/src/SharedAssetSystem.cpp @@ -0,0 +1,17 @@ +#include +#include + +namespace CesiumGltf { + +SharedAssetSystem::SharedAssetSystem() noexcept + : _pImages(new SharedAssetDepot()) {} + +SharedAssetSystem::~SharedAssetSystem() noexcept = default; + +const SharedAssetDepot& SharedAssetSystem::image() { + return *this->_pImages; +} + +void SharedAssetSystem::deletionTick() { this->_pImages->deletionTick(); } + +} // namespace CesiumGltf diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index f3c3b4cc5..a1e941bd7 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -115,7 +115,7 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * appear in this glTF. If not present, assets will not be shared between * glTFs, even if they're loaded from the same URL. */ - CesiumUtility::IntrusivePointer pSharedAssets = + CesiumUtility::IntrusivePointer pSharedAssets = nullptr; }; From a88d83f8d1168389388c9ddb809dd3ce625c1a9e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 23:17:48 +1000 Subject: [PATCH 35/81] Move constructor for SharedAsset. --- CesiumGltf/include/CesiumGltf/SharedAsset.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index fe0d9579f..a2a927365 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -24,6 +24,7 @@ template class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { public: SharedAsset() = default; + ~SharedAsset() { CESIUM_ASSERT(this->_referenceCount == 0); } // Assets can be copied, but the fresh instance has no references and is not // in the asset depot. @@ -33,9 +34,13 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { _pDepot(nullptr), _uniqueAssetId() {} - // Move construction is not allowed, because existing references to this - // shared asset would be affected as well. - SharedAsset(SharedAsset&& rhs) = delete; + // After a move construction, the content of the asset is moved to the new + // instance, but the asset depot still references the old instance. + SharedAsset(SharedAsset&& rhs) + : ExtensibleObject(std::move(rhs)), + _referenceCount(0), + _pDepot(nullptr), + _uniqueAssetId() {} // Assignment does not affect the asset's relationship with the depot, but is // useful to assign the data in derived classes. From e3e566502b00940302daa0b4e4789ac907d0b7a5 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 4 Oct 2024 11:42:47 -0400 Subject: [PATCH 36/81] Rework deletion --- CesiumGltf/include/CesiumGltf/ImageCesium.h | 2 + CesiumGltf/include/CesiumGltf/SharedAsset.h | 17 ++- .../include/CesiumGltf/SharedAssetDepot.h | 102 ++++++++++-------- CesiumGltf/src/SharedAssetDepot.cpp | 11 -- CesiumGltf/src/SharedAssetSystem.cpp | 2 - 5 files changed, 73 insertions(+), 61 deletions(-) delete mode 100644 CesiumGltf/src/SharedAssetDepot.cpp diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index 469061429..305169110 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -111,5 +111,7 @@ struct CESIUMGLTF_API ImageCesium final : public SharedAsset { * this image. */ int64_t sizeBytes = -1; + + int64_t getSizeBytes() const override { return this->sizeBytes; } }; } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index fe0d9579f..47ef04843 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -54,7 +54,13 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { * {@link CesiumUtility::IntrusivePointer} instead of calling this method * directly. */ - void addReference() const /*noexcept*/ { ++this->_referenceCount; } + void addReference() const /*noexcept*/ { + const int32_t prevReferences = this->_referenceCount++; + if (this->_pDepot && prevReferences <= 0) { + this->_pDepot->unmarkDeletionCandidate( + const_cast(static_cast(this))); + } + } /** * @brief Removes a counted reference from this object. When the last @@ -71,7 +77,6 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { if (pDepot) { // Let the depot manage this object's lifetime. pDepot->markDeletionCandidate( - this->_uniqueAssetId, const_cast(static_cast(this))); } else { // No depot, so destroy this object directly. @@ -92,7 +97,15 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { */ bool isShareable() const { return this->_pDepot != nullptr; } + /** + * The number of bytes of memory usage that this asset takes up. + * This is used for deletion logic by the {@link SharedAssetDepot}. + */ + virtual int64_t getSizeBytes() const = 0; + private: + const std::string& getUniqueAssetId() const { return this->_uniqueAssetId; } + mutable std::atomic _referenceCount{0}; CesiumUtility::IntrusivePointer> _pDepot; std::string _uniqueAssetId; diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index f039a0a3f..46c512577 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -7,8 +7,8 @@ #include #include -#include #include +#include #include #include #include @@ -30,13 +30,20 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< SharedAssetDepot> { public: /** - * @brief Number of seconds an asset can remain unused before it's deleted. + * @brief The maximum total byte usage of assets that have been loaded but are no + * longer needed. + * + * When cached assets are no longer needed, they're marked as + * candidates for deletion. However, this deletion doesn't actually occur + * until the total byte usage of deletion candidates exceeds this threshold. + * At that point, assets are cleaned up in the order that they were marked for + * deletion until the total dips below this threshold again. + * + * Default is 100MB. */ - float assetDeletionThreshold = 60.0f; + int64_t staleAssetSizeLimit = 1000 * 1000 * 100; - SharedAssetDepot() { - this->lastDeletionTick = std::chrono::steady_clock::now(); - } + SharedAssetDepot() = default; /** * Stores the AssetType in this depot and returns a reference to it, @@ -64,12 +71,12 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< const std::string& uri, const std::vector& headers) { // We need to avoid: - // - Two assets starting loading before the first asset has updated the + // - Two assets starting to load before the first asset has updated the // pendingAssets map // - An asset starting to load after the previous load has been removed from // the pendingAssets map, but before the completed asset has been added to // the assets map. - std::lock_guard lock(assetsMutex); + std::lock_guard lock(this->assetsMutex); auto existingIt = this->assets.find(uri); if (existingIt != this->assets.end()) { @@ -142,36 +149,6 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< return it->second; } - /** - * @brief Should be called every frame to handle deletion of assets that - * haven't been used in a while. - */ - void deletionTick() { - std::chrono::steady_clock::time_point now = - std::chrono::steady_clock::now(); - std::chrono::duration delta = - std::chrono::duration(now - this->lastDeletionTick); - - std::vector toDelete; - - std::lock_guard lock(this->deletionCandidatesMutex); - for (auto& [hash, age] : this->deletionCandidates) { - age += delta.count(); - - if (age >= this->assetDeletionThreshold) { - toDelete.push_back(hash); - } - } - - std::lock_guard assetsLock(this->assetsMutex); - for (std::string& hash : toDelete) { - this->deletionCandidates.erase(hash); - this->assets.erase(hash); - } - - this->lastDeletionTick = now; - } - /** * @brief Returns the total number of distinct assets contained in this depot. */ @@ -211,7 +188,7 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< } private: - // Disable copy and move + // Disable copy void operator=(const SharedAssetDepot& other) = delete; /** @@ -219,10 +196,41 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< * Should only be called by {@link SharedAsset}. */ void markDeletionCandidate( - const std::string& hash, const CesiumUtility::IntrusivePointer& pAsset) { std::lock_guard lock(this->deletionCandidatesMutex); - this->deletionCandidates.emplace(hash, 0.0f); + this->deletionCandidates.push_back(pAsset); + this->totalDeletionCandidateMemoryUsage += pAsset->getSizeBytes(); + + if (this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { + std::lock_guard assetsLock(this->assetsMutex); + while (!this->deletionCandidates.empty() && this->totalDeletionCandidateMemoryUsage > + this->staleAssetSizeLimit) { + const CesiumUtility::IntrusivePointer& pOldAsset = + this->deletionCandidates.front(); + this->deletionCandidates.pop_front(); + this->assets.erase(pOldAsset->getUniqueAssetId()); + this->totalDeletionCandidateMemoryUsage -= pOldAsset->getSizeBytes(); + } + } + } + + /** + * Unmarks the given asset as a candidate for deletion. + * Should only be called by {@link SharedAsset}. + */ + void unmarkDeletionCandidate( + const CesiumUtility::IntrusivePointer& pAsset) { + std::lock_guard lock(this->deletionCandidatesMutex); + const std::string& assetId = pAsset->getUniqueAssetId(); + for (auto it = this->deletionCandidates.begin(); + it != this->deletionCandidates.end(); + ++it) { + if ((*it)->getUniqueAssetId() == assetId) { + this->deletionCandidates.erase(it); + this->totalDeletionCandidateMemoryUsage -= (*it)->getSizeBytes(); + break; + } + } } // Assets that have a unique ID that can be used to de-duplicate them. @@ -236,12 +244,14 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< // Mutex for checking or editing the pendingAssets map std::mutex assetsMutex; - // Maps assets that are being considered for deletion to the time that they - // began to be considered for deletion. - std::unordered_map deletionCandidates; - // Mutex for modifying the deletionCandidates map. + // List of assets that are being considered for deletion, in the order that + // they were added. + std::list> deletionCandidates; + // The total amount of memory used by all assets in the deletionCandidates + // list. + int64_t totalDeletionCandidateMemoryUsage; + // Mutex for modifying the deletionCandidates list. mutable std::mutex deletionCandidatesMutex; - std::chrono::steady_clock::time_point lastDeletionTick; friend class SharedAsset; }; diff --git a/CesiumGltf/src/SharedAssetDepot.cpp b/CesiumGltf/src/SharedAssetDepot.cpp deleted file mode 100644 index 1842c9aa7..000000000 --- a/CesiumGltf/src/SharedAssetDepot.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "CesiumGltf/SharedAssetDepot.h" - -#include "CesiumGltf/ImageCesium.h" - -#include - -namespace CesiumGltf { - -namespace SharedAssetDepotInternals {} // namespace SharedAssetDepotInternals - -} // namespace CesiumGltf diff --git a/CesiumGltf/src/SharedAssetSystem.cpp b/CesiumGltf/src/SharedAssetSystem.cpp index 7a3dd69b4..a07c324b9 100644 --- a/CesiumGltf/src/SharedAssetSystem.cpp +++ b/CesiumGltf/src/SharedAssetSystem.cpp @@ -12,6 +12,4 @@ const SharedAssetDepot& SharedAssetSystem::image() { return *this->_pImages; } -void SharedAssetSystem::deletionTick() { this->_pImages->deletionTick(); } - } // namespace CesiumGltf From 95749246de10f66bb2c0cb8af3500e487a6ba6ed Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 4 Oct 2024 11:45:25 -0400 Subject: [PATCH 37/81] Format --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 46c512577..86cc2a419 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -30,9 +30,9 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< SharedAssetDepot> { public: /** - * @brief The maximum total byte usage of assets that have been loaded but are no - * longer needed. - * + * @brief The maximum total byte usage of assets that have been loaded but are + * no longer needed. + * * When cached assets are no longer needed, they're marked as * candidates for deletion. However, this deletion doesn't actually occur * until the total byte usage of deletion candidates exceeds this threshold. @@ -203,8 +203,9 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< if (this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { std::lock_guard assetsLock(this->assetsMutex); - while (!this->deletionCandidates.empty() && this->totalDeletionCandidateMemoryUsage > - this->staleAssetSizeLimit) { + while (!this->deletionCandidates.empty() && + this->totalDeletionCandidateMemoryUsage > + this->staleAssetSizeLimit) { const CesiumUtility::IntrusivePointer& pOldAsset = this->deletionCandidates.front(); this->deletionCandidates.pop_front(); From 924b000e00fc9054d60591bae494c71532a8ec61 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 8 Oct 2024 15:27:52 -0400 Subject: [PATCH 38/81] Add accessors for deletion stats --- CesiumGltf/include/CesiumGltf/SharedAsset.h | 2 +- .../include/CesiumGltf/SharedAssetDepot.h | 25 +++++-------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index 9b71de5c2..d80fb2904 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -108,9 +108,9 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { */ virtual int64_t getSizeBytes() const = 0; -private: const std::string& getUniqueAssetId() const { return this->_uniqueAssetId; } +private: mutable std::atomic _referenceCount{0}; CesiumUtility::IntrusivePointer> _pDepot; std::string _uniqueAssetId; diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 86cc2a419..c12dce504 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -165,26 +165,13 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< return count; } - /** - * @brief Obtains statistics on the number of assets currently listed as - * candidates for deletion, along with the average number of seconds that - * they've been pending deletion. - * @param outAverageAge The average time in seconds that the current deletion - * candidates have spent pending deletion. - * @param outCount The number of current deletion candidates. - */ - void getDeletionStats(float& outAverageAge, size_t& outCount) const { - size_t count = 0; - float total = 0; - + size_t getDeletionCandidateCount() const { std::lock_guard lock(this->deletionCandidatesMutex); - for (auto& [asset, age] : this->deletionCandidates) { - total += age; - count++; - } + return this->deletionCandidates.size(); + } - outAverageAge = count > 0 ? total / count : 0; - outCount = count; + int64_t getDeletionCandidateTotalSizeBytes() const { + return this->totalDeletionCandidateMemoryUsage; } private: @@ -250,7 +237,7 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< std::list> deletionCandidates; // The total amount of memory used by all assets in the deletionCandidates // list. - int64_t totalDeletionCandidateMemoryUsage; + std::atomic totalDeletionCandidateMemoryUsage; // Mutex for modifying the deletionCandidates list. mutable std::mutex deletionCandidatesMutex; From 21ecab90b80d13d1c221cdcd5fb1a3fe3264655f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 9 Oct 2024 22:18:11 +1100 Subject: [PATCH 39/81] Fix asset lifetime, avoid circular reference counting. --- CesiumGltf/include/CesiumGltf/ImageCesium.h | 2 +- CesiumGltf/include/CesiumGltf/SharedAsset.h | 17 +--- .../include/CesiumGltf/SharedAssetDepot.h | 86 +++++++++++-------- 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index 305169110..fbc31d23f 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -112,6 +112,6 @@ struct CESIUMGLTF_API ImageCesium final : public SharedAsset { */ int64_t sizeBytes = -1; - int64_t getSizeBytes() const override { return this->sizeBytes; } + int64_t getSizeBytes() const { return this->sizeBytes; } }; } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index d80fb2904..095bded53 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -62,8 +62,7 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { void addReference() const /*noexcept*/ { const int32_t prevReferences = this->_referenceCount++; if (this->_pDepot && prevReferences <= 0) { - this->_pDepot->unmarkDeletionCandidate( - const_cast(static_cast(this))); + this->_pDepot->unmarkDeletionCandidate(static_cast(this)); } } @@ -77,12 +76,10 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { CESIUM_ASSERT(this->_referenceCount > 0); const int32_t references = --this->_referenceCount; if (references == 0) { - CesiumUtility::IntrusivePointer> pDepot = - this->_pDepot; + SharedAssetDepot* pDepot = this->_pDepot; if (pDepot) { // Let the depot manage this object's lifetime. - pDepot->markDeletionCandidate( - const_cast(static_cast(this))); + pDepot->markDeletionCandidate(static_cast(this)); } else { // No depot, so destroy this object directly. delete static_cast(this); @@ -102,17 +99,11 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { */ bool isShareable() const { return this->_pDepot != nullptr; } - /** - * The number of bytes of memory usage that this asset takes up. - * This is used for deletion logic by the {@link SharedAssetDepot}. - */ - virtual int64_t getSizeBytes() const = 0; - const std::string& getUniqueAssetId() const { return this->_uniqueAssetId; } private: mutable std::atomic _referenceCount{0}; - CesiumUtility::IntrusivePointer> _pDepot; + SharedAssetDepot* _pDepot; std::string _uniqueAssetId; // To allow the depot to modify _pDepot and _uniqueAssetId. diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index c12dce504..16259b26d 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -45,15 +45,49 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< SharedAssetDepot() = default; + ~SharedAssetDepot() { + // It's possible the assets will outlive the depot, if they're still in use. + std::lock_guard lock(this->assetsMutex); + + for (auto& pair : this->assets) { + // Transfer ownership to the reference counting system. + CesiumUtility::IntrusivePointer pCounted = + pair.second.release(); + pCounted->_pDepot = nullptr; + pCounted->_uniqueAssetId = "TODO: released"; + } + } + /** * Stores the AssetType in this depot and returns a reference to it, * or returns the existing asset if present. */ CesiumUtility::IntrusivePointer store( - std::string& assetId, + const std::string& assetId, const CesiumUtility::IntrusivePointer& pAsset) { - auto [newIt, added] = this->assets.try_emplace(assetId, pAsset); - return newIt->second; + std::lock_guard lock(this->assetsMutex); + + auto findIt = this->assets.find(assetId); + if (findIt != this->assets.end()) { + // This asset ID already exists in the depot, so we can't add this asset. + return findIt->second.get(); + } + + pAsset->_pDepot = this; + pAsset->_uniqueAssetId = assetId; + + // Now that this asset is owned by the depot, we exclusively + // control its lifetime with a std::unique_ptr. + std::unique_ptr pOwnedAsset(pAsset.get()); + + auto [addIt, added] = this->assets.emplace(assetId, std::move(pOwnedAsset)); + + // We should always add successfully, because we verified it didn't already + // exist. A failed emplace is disastrous because our unique_ptr will end up + // destroying the user's object, which may still be in use. + CESIUM_ASSERT(added); + + return addIt->second.get(); } /** @@ -82,8 +116,8 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< if (existingIt != this->assets.end()) { // We've already loaded an asset with this ID - we can just use that. return asyncSystem - .createResolvedFuture( - CesiumUtility::IntrusivePointer(existingIt->second)) + .createResolvedFuture(CesiumUtility::IntrusivePointer( + existingIt->second.get())) .share(); } @@ -112,30 +146,13 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< // Do this in main thread since we're messing with the collections. .thenInMainThread( [uri, - this, - pAssets = &this->assets, - pPendingAssetsMutex = &this->assetsMutex, - pPendingAssets = &this->pendingAssets]( - CesiumUtility::IntrusivePointer&& pResult) + this](CesiumUtility::IntrusivePointer&& pResult) -> CesiumUtility::IntrusivePointer { - std::lock_guard lock(*pPendingAssetsMutex); - // Get rid of our future. - pPendingAssets->erase(uri); - - if (pResult) { - pResult->_pDepot = this; - pResult->_uniqueAssetId = uri; + this->pendingAssets.erase(uri); - auto [it, ok] = pAssets->emplace(uri, pResult); - if (!ok) { - return nullptr; - } - - return it->second; - } - - return nullptr; + // Store the new asset in the depot. + return this->store(uri, pResult); }); auto [it, ok] = this->pendingAssets.emplace(uri, std::move(future).share()); @@ -182,10 +199,9 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< * Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void markDeletionCandidate( - const CesiumUtility::IntrusivePointer& pAsset) { + void markDeletionCandidate(const AssetType* pAsset) { std::lock_guard lock(this->deletionCandidatesMutex); - this->deletionCandidates.push_back(pAsset); + this->deletionCandidates.push_back(const_cast(pAsset)); this->totalDeletionCandidateMemoryUsage += pAsset->getSizeBytes(); if (this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { @@ -193,9 +209,9 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< while (!this->deletionCandidates.empty() && this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { - const CesiumUtility::IntrusivePointer& pOldAsset = - this->deletionCandidates.front(); + const AssetType* pOldAsset = this->deletionCandidates.front(); this->deletionCandidates.pop_front(); + CESIUM_ASSERT(pOldAsset->_referenceCount == 0); this->assets.erase(pOldAsset->getUniqueAssetId()); this->totalDeletionCandidateMemoryUsage -= pOldAsset->getSizeBytes(); } @@ -206,8 +222,7 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< * Unmarks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void unmarkDeletionCandidate( - const CesiumUtility::IntrusivePointer& pAsset) { + void unmarkDeletionCandidate(const AssetType* pAsset) { std::lock_guard lock(this->deletionCandidatesMutex); const std::string& assetId = pAsset->getUniqueAssetId(); for (auto it = this->deletionCandidates.begin(); @@ -222,8 +237,7 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< } // Assets that have a unique ID that can be used to de-duplicate them. - std::unordered_map> - assets; + std::unordered_map> assets; // Futures for assets that still aren't loaded yet. std::unordered_map< std::string, @@ -234,7 +248,7 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< // List of assets that are being considered for deletion, in the order that // they were added. - std::list> deletionCandidates; + std::list deletionCandidates; // The total amount of memory used by all assets in the deletionCandidates // list. std::atomic totalDeletionCandidateMemoryUsage; From ee4b391d1451bd49711128de37b84a0668fdd1f9 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 9 Oct 2024 22:39:58 +1100 Subject: [PATCH 40/81] Remove TODO. --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 16259b26d..a0c452aaf 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -54,7 +54,7 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< CesiumUtility::IntrusivePointer pCounted = pair.second.release(); pCounted->_pDepot = nullptr; - pCounted->_uniqueAssetId = "TODO: released"; + pCounted->_uniqueAssetId.clear(); } } From 1b0934ec6c91e1c11fe418df1cf3e600138c4e5c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 9 Oct 2024 22:48:16 +1100 Subject: [PATCH 41/81] Don't use an iterator after it's erased. --- CesiumGltf/include/CesiumGltf/SharedAssetDepot.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index a0c452aaf..0c4c6dceb 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -229,8 +229,8 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< it != this->deletionCandidates.end(); ++it) { if ((*it)->getUniqueAssetId() == assetId) { - this->deletionCandidates.erase(it); this->totalDeletionCandidateMemoryUsage -= (*it)->getSizeBytes(); + this->deletionCandidates.erase(it); break; } } From 8595163c9201964655736ca6a001fa8a343b8485 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 20:15:25 +1100 Subject: [PATCH 42/81] getSizeBytes can use sizeInBytes or pixelData.size(). --- CesiumGltf/include/CesiumGltf/ImageCesium.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index fbc31d23f..55d146dea 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -112,6 +112,14 @@ struct CESIUMGLTF_API ImageCesium final : public SharedAsset { */ int64_t sizeBytes = -1; - int64_t getSizeBytes() const { return this->sizeBytes; } + /** + * @brief Gets the size of this asset, in bytes. + * + * If {@link sizeBytes} is greater than or equal to zero, it is returned. + * Otherwise, the size of the {@link pixelData} array is returned. + */ + int64_t getSizeBytes() const { + return this->sizeBytes >= 0 ? this->sizeBytes : this->pixelData.size(); + } }; } // namespace CesiumGltf From c8d961214c2f0b2d9c2781dcaf8aeb8bbdae24e5 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 20:16:31 +1100 Subject: [PATCH 43/81] Remove unnecessary SharedAssetSystem::deletionTick. --- CesiumGltf/include/CesiumGltf/SharedAssetSystem.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h index a116b9136..5e38dde0f 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h @@ -38,8 +38,6 @@ class SharedAssetSystem const SharedAssetDepot& image(); - void deletionTick(); - private: CesiumUtility::IntrusivePointer> _pImages; From 64597295599032cb808036d7cf1ce0fc0f629a71 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 20:18:27 +1100 Subject: [PATCH 44/81] Raster overlays store heap-allocated ImageCesiums. --- .../CesiumRasterOverlays/RasterOverlayTile.h | 11 ++++--- .../src/RasterOverlayTile.cpp | 4 +-- .../src/RasterOverlayTileProvider.cpp | 33 ++++++++++--------- .../test/TestAddRasterOverlayToGltf.cpp | 2 +- .../TestQuadtreeRasterOverlayTileProvider.cpp | 8 +++-- .../test/TestTileMapServiceRasterOverlay.cpp | 4 ++- 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h index f74023e16..f27c3adcc 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h @@ -192,8 +192,9 @@ class RasterOverlayTile final * * @return The image data. */ - const CesiumGltf::ImageCesium& getImage() const noexcept { - return this->_image; + CesiumUtility::IntrusivePointer + getImage() const noexcept { + return this->_pImage; } /** @@ -204,7 +205,9 @@ class RasterOverlayTile final * * @return The image data. */ - CesiumGltf::ImageCesium& getImage() noexcept { return this->_image; } + CesiumUtility::IntrusivePointer getImage() noexcept { + return this->_pImage; + } /** * @brief Create the renderer resources for the loaded image. @@ -256,7 +259,7 @@ class RasterOverlayTile final CesiumGeometry::Rectangle _rectangle; std::vector _tileCredits; LoadState _state; - CesiumGltf::ImageCesium _image; + CesiumUtility::IntrusivePointer _pImage; void* _pRendererResources; MoreDetailAvailable _moreDetailAvailable; }; diff --git a/CesiumRasterOverlays/src/RasterOverlayTile.cpp b/CesiumRasterOverlays/src/RasterOverlayTile.cpp index 722ef553b..2b9c19207 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTile.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTile.cpp @@ -17,7 +17,7 @@ RasterOverlayTile::RasterOverlayTile( _rectangle(CesiumGeometry::Rectangle(0.0, 0.0, 0.0, 0.0)), _tileCredits(), _state(LoadState::Placeholder), - _image(), + _pImage(nullptr), _pRendererResources(nullptr), _moreDetailAvailable(MoreDetailAvailable::Unknown) {} @@ -30,7 +30,7 @@ RasterOverlayTile::RasterOverlayTile( _rectangle(rectangle), _tileCredits(), _state(LoadState::Unloaded), - _image(), + _pImage(nullptr), _pRendererResources(nullptr), _moreDetailAvailable(MoreDetailAvailable::Unknown) {} diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 3ba53e689..591c2c0c5 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -91,8 +91,9 @@ RasterOverlayTileProvider::getTile( void RasterOverlayTileProvider::removeTile(RasterOverlayTile* pTile) noexcept { CESIUM_ASSERT(pTile->getReferenceCount() == 0); - - this->_tileDataBytes -= pTile->getImage().sizeBytes; + if (pTile->getImage()) { + this->_tileDataBytes -= pTile->getImage()->sizeBytes; + } } CesiumAsync::Future @@ -213,7 +214,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( namespace { struct LoadResult { RasterOverlayTile::LoadState state = RasterOverlayTile::LoadState::Unloaded; - CesiumGltf::ImageCesium image = {}; + CesiumUtility::IntrusivePointer pImage = nullptr; CesiumGeometry::Rectangle rectangle = {}; std::vector credits = {}; void* pRendererResources = nullptr; @@ -288,7 +289,7 @@ static LoadResult createLoadResultFromLoadedImage( LoadResult result; result.state = RasterOverlayTile::LoadState::Loaded; - result.image = std::move(image); + result.pImage = loadedImage.pImage; result.rectangle = loadedImage.rectangle; result.credits = std::move(loadedImage.credits); result.pRendererResources = pRendererResources; @@ -341,7 +342,7 @@ CesiumAsync::Future RasterOverlayTileProvider::doLoad( [thiz, pTile, isThrottledLoad](LoadResult&& result) noexcept { pTile->_rectangle = result.rectangle; pTile->_pRendererResources = result.pRendererResources; - pTile->_image = std::move(result.image); + pTile->_pImage = std::move(result.pImage); pTile->_tileCredits = std::move(result.credits); pTile->_moreDetailAvailable = result.moreDetailAvailable @@ -349,17 +350,19 @@ CesiumAsync::Future RasterOverlayTileProvider::doLoad( : RasterOverlayTile::MoreDetailAvailable::No; pTile->setState(result.state); - ImageCesium& imageCesium = pTile->getImage(); + if (pTile->getImage() != nullptr) { + 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()); - } + // 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()); + } - thiz->_tileDataBytes += imageCesium.sizeBytes; + thiz->_tileDataBytes += imageCesium.sizeBytes; + } thiz->finalizeTileLoad(isThrottledLoad); @@ -368,7 +371,7 @@ CesiumAsync::Future RasterOverlayTileProvider::doLoad( .catchInMainThread( [thiz, pTile, isThrottledLoad](const std::exception& /*e*/) { pTile->_pRendererResources = nullptr; - pTile->_image = {}; + pTile->_pImage = nullptr; pTile->_tileCredits = {}; pTile->_moreDetailAvailable = RasterOverlayTile::MoreDetailAvailable::No; diff --git a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp index a6427a1d9..21e591018 100644 --- a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp +++ b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp @@ -186,7 +186,7 @@ TEST_CASE("Add raster overlay to glTF") { // PNG-encode the raster overlay image and store it in the main // buffer. ImageManipulation::savePng( - loadResult.pTile->getImage(), + *loadResult.pTile->getImage(), buffer.cesium.data); BufferView& bufferView = gltf.bufferViews.emplace_back(); diff --git a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp index ddfe60440..705f90fe8 100644 --- a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp @@ -170,7 +170,9 @@ TEST_CASE("QuadtreeRasterOverlayTileProvider getTile") { CHECK(pTile->getState() == RasterOverlayTile::LoadState::Loaded); - const ImageCesium& image = pTile->getImage(); + REQUIRE(pTile->getImage()); + + const ImageCesium& image = *pTile->getImage(); CHECK(image.width > 0); CHECK(image.height > 0); CHECK(image.pixelData.size() > 0); @@ -224,7 +226,9 @@ TEST_CASE("QuadtreeRasterOverlayTileProvider getTile") { CHECK(pTile->getState() == RasterOverlayTile::LoadState::Loaded); - const ImageCesium& image = pTile->getImage(); + REQUIRE(pTile->getImage()); + + const ImageCesium& image = *pTile->getImage(); CHECK(image.width > 0); CHECK(image.height > 0); CHECK(image.pixelData.size() > 0); diff --git a/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp index bf131b6b8..6f3228285 100644 --- a/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp @@ -72,7 +72,9 @@ TEST_CASE("TileMapServiceRasterOverlay") { REQUIRE(pTile); waitForFuture(asyncSystem, pTileProvider->loadTile(*pTile)); - ImageCesium& image = pTile->getImage(); + REQUIRE(pTile->getImage()); + + const ImageCesium& image = *pTile->getImage(); CHECK(image.width > 0); CHECK(image.height > 0); } From 36783ad0303c9078077a6caf87abe0a6029c8026 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 20:18:58 +1100 Subject: [PATCH 45/81] SharedAsset improvements... * Make SharedAsset constructor and destructor protected, so instances of this type can't be created directly. * Maintain `_sizeInDepot` field on `SharedAsset` to defend against the asset size changing between when we add its size and when we remove its size. * Add some doc comments. * Change the staleAssetSizeLimit to 16 MiB. * Add some defensive assertions. * Fix use-after-free when removing deletion candidates. --- CesiumGltf/include/CesiumGltf/SharedAsset.h | 20 +++++-- .../include/CesiumGltf/SharedAssetDepot.h | 59 ++++++++++++------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index 095bded53..7e8de7284 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -23,9 +23,6 @@ namespace CesiumGltf { template class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { public: - SharedAsset() = default; - ~SharedAsset() { CESIUM_ASSERT(this->_referenceCount == 0); } - // Assets can be copied, but the fresh instance has no references and is not // in the asset depot. SharedAsset(const SharedAsset& rhs) @@ -99,12 +96,25 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { */ bool isShareable() const { return this->_pDepot != nullptr; } + /** + * @brief Gets the unique ID of this asset, if it {@link isShareable}. + * + * If this asset is not shareable, this method will return an empty string. + */ const std::string& getUniqueAssetId() const { return this->_uniqueAssetId; } +protected: + SharedAsset() = default; + ~SharedAsset() { CESIUM_ASSERT(this->_referenceCount == 0); } + private: mutable std::atomic _referenceCount{0}; - SharedAssetDepot* _pDepot; - std::string _uniqueAssetId; + SharedAssetDepot* _pDepot{nullptr}; + std::string _uniqueAssetId{}; + + // The size of this asset when it was counted by the depot. This is stored so + // that the exact same size can be subtracted later. + int64_t _sizeInDepot{0}; // To allow the depot to modify _pDepot and _uniqueAssetId. friend class SharedAssetDepot; diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h index 0c4c6dceb..63d1c4956 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h @@ -39,9 +39,9 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< * At that point, assets are cleaned up in the order that they were marked for * deletion until the total dips below this threshold again. * - * Default is 100MB. + * Default is 16MiB. */ - int64_t staleAssetSizeLimit = 1000 * 1000 * 100; + int64_t staleAssetSizeLimit = 16 * 1024 * 1024; SharedAssetDepot() = default; @@ -73,6 +73,11 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< return findIt->second.get(); } + // If this asset ID is pending, but hasn't completed yet, where did this + // asset come from? It shouldn't happen. + CESIUM_ASSERT( + this->pendingAssets.find(assetId) == this->pendingAssets.end()); + pAsset->_pDepot = this; pAsset->_uniqueAssetId = assetId; @@ -148,20 +153,19 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< [uri, this](CesiumUtility::IntrusivePointer&& pResult) -> CesiumUtility::IntrusivePointer { - // Get rid of our future. + // Remove pending asset. this->pendingAssets.erase(uri); // Store the new asset in the depot. return this->store(uri, pResult); }); - auto [it, ok] = this->pendingAssets.emplace(uri, std::move(future).share()); - if (!ok) { - return asyncSystem - .createResolvedFuture>( - nullptr) - .share(); - } + auto [it, added] = + this->pendingAssets.emplace(uri, std::move(future).share()); + + // Should always be added successfully, because we checked above that the + // URI doesn't exist in the map yet. + CESIUM_ASSERT(added); return it->second; } @@ -201,19 +205,28 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< */ void markDeletionCandidate(const AssetType* pAsset) { std::lock_guard lock(this->deletionCandidatesMutex); - this->deletionCandidates.push_back(const_cast(pAsset)); - this->totalDeletionCandidateMemoryUsage += pAsset->getSizeBytes(); + + AssetType* pMutableAsset = const_cast(pAsset); + pMutableAsset->_sizeInDepot = pMutableAsset->getSizeBytes(); + this->totalDeletionCandidateMemoryUsage += pMutableAsset->_sizeInDepot; + + this->deletionCandidates.push_back(pMutableAsset); if (this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { std::lock_guard assetsLock(this->assetsMutex); + + // Delete the deletion candidates until we're below the limit. while (!this->deletionCandidates.empty() && this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { const AssetType* pOldAsset = this->deletionCandidates.front(); this->deletionCandidates.pop_front(); + + this->totalDeletionCandidateMemoryUsage -= pOldAsset->_sizeInDepot; + + // This will actually delete the asset. CESIUM_ASSERT(pOldAsset->_referenceCount == 0); this->assets.erase(pOldAsset->getUniqueAssetId()); - this->totalDeletionCandidateMemoryUsage -= pOldAsset->getSizeBytes(); } } } @@ -224,15 +237,17 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< */ void unmarkDeletionCandidate(const AssetType* pAsset) { std::lock_guard lock(this->deletionCandidatesMutex); - const std::string& assetId = pAsset->getUniqueAssetId(); - for (auto it = this->deletionCandidates.begin(); - it != this->deletionCandidates.end(); - ++it) { - if ((*it)->getUniqueAssetId() == assetId) { - this->totalDeletionCandidateMemoryUsage -= (*it)->getSizeBytes(); - this->deletionCandidates.erase(it); - break; - } + + auto it = std::find( + this->deletionCandidates.begin(), + this->deletionCandidates.end(), + pAsset); + + CESIUM_ASSERT(it != this->deletionCandidates.end()); + + if (it != this->deletionCandidates.end()) { + this->totalDeletionCandidateMemoryUsage -= (*it)->_sizeInDepot; + this->deletionCandidates.erase(it); } } From e3975911cdea297b23ec0a7cdb5c5b769cb80ad9 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 20:31:17 +1100 Subject: [PATCH 46/81] Even independent assets can be shared. --- CesiumGltf/include/CesiumGltf/SharedAsset.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumGltf/include/CesiumGltf/SharedAsset.h index 7e8de7284..b53bcb2b3 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumGltf/include/CesiumGltf/SharedAsset.h @@ -85,16 +85,16 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { } /** - * @brief Determines if this image is shareable because it is managed by an - * asset depot. An image that is not shareable can be understood to be - * exclusively owned by, for example, the glTF that references it. If it is - * shareable, then potentially multiple glTFs reference it. - * - * An example of a non-shareable asset is an image embedded in a Binary glTF - * (GLB) buffer. An example of a shareable asset is an image referenced in a - * glTF by URI. + * @brief Gets the shared asset depot that owns this asset, or nullptr if this + * asset is independent of an asset depot. + */ + const SharedAssetDepot* getDepot() const { return this->_pDepot; } + + /** + * @brief Gets the shared asset depot that owns this asset, or nullptr if this + * asset is independent of an asset depot. */ - bool isShareable() const { return this->_pDepot != nullptr; } + SharedAssetDepot* getDepot() { return this->_pDepot; } /** * @brief Gets the unique ID of this asset, if it {@link isShareable}. @@ -116,7 +116,7 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { // that the exact same size can be subtracted later. int64_t _sizeInDepot{0}; - // To allow the depot to modify _pDepot and _uniqueAssetId. + // To allow the depot to modify _pDepot, _uniqueAssetId, and _sizeInDepot. friend class SharedAssetDepot; }; From a9516a0c842c226cf4e7481cc87476f5fb52e575 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 21:00:16 +1100 Subject: [PATCH 47/81] Move SharedAsset[Depot] to CesiumAsync. --- Cesium3DTilesSelection/src/TilesetContentManager.h | 1 - .../include/CesiumAsync}/SharedAsset.h | 8 ++++---- .../include/CesiumAsync}/SharedAssetDepot.h | 9 +++++---- CesiumGltf/include/CesiumGltf/Image.h | 2 +- CesiumGltf/include/CesiumGltf/ImageCesium.h | 5 +++-- CesiumGltf/include/CesiumGltf/SharedAssetSystem.h | 7 ++++--- CesiumGltf/include/CesiumGltf/TextureView.h | 1 - CesiumGltf/src/SharedAssetSystem.cpp | 5 +++-- CesiumGltfReader/src/GltfReader.cpp | 2 +- 9 files changed, 21 insertions(+), 19 deletions(-) rename {CesiumGltf/include/CesiumGltf => CesiumAsync/include/CesiumAsync}/SharedAsset.h (95%) rename {CesiumGltf/include/CesiumGltf => CesiumAsync/include/CesiumAsync}/SharedAssetDepot.h (98%) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 5a58e77d6..4a4ec286f 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -11,7 +11,6 @@ #include #include #include -#include #include #include diff --git a/CesiumGltf/include/CesiumGltf/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h similarity index 95% rename from CesiumGltf/include/CesiumGltf/SharedAsset.h rename to CesiumAsync/include/CesiumAsync/SharedAsset.h index b53bcb2b3..3a34cfda8 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -1,12 +1,12 @@ #pragma once -#include +#include #include #include #include -namespace CesiumGltf { +namespace CesiumAsync { /** * @brief An asset that is potentially shared between multiple objects, such as @@ -21,7 +21,7 @@ namespace CesiumGltf { * `class MyClass : public SharedAsset { ... };` */ template -class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { +class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { public: // Assets can be copied, but the fresh instance has no references and is not // in the asset depot. @@ -120,4 +120,4 @@ class CESIUMGLTF_API SharedAsset : public CesiumUtility::ExtensibleObject { friend class SharedAssetDepot; }; -} // namespace CesiumGltf +} // namespace CesiumAsync diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h similarity index 98% rename from CesiumGltf/include/CesiumGltf/SharedAssetDepot.h rename to CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 63d1c4956..0c3932122 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -16,7 +16,7 @@ #include #include -namespace CesiumGltf { +namespace CesiumAsync { template class SharedAsset; @@ -26,8 +26,9 @@ template class SharedAsset; * be derived from {@link SharedAsset}. */ template -class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< - SharedAssetDepot> { +class CESIUMASYNC_API SharedAssetDepot + : public CesiumUtility::ReferenceCountedThreadSafe< + SharedAssetDepot> { public: /** * @brief The maximum total byte usage of assets that have been loaded but are @@ -273,4 +274,4 @@ class SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< friend class SharedAsset; }; -} // namespace CesiumGltf +} // namespace CesiumAsync diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index 701836eff..b2a45c046 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -4,7 +4,7 @@ #include "CesiumGltf/ImageSpec.h" #include "CesiumGltf/Library.h" -#include +#include namespace CesiumGltf { /** @copydoc ImageSpec */ diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageCesium.h index 55d146dea..6feedf656 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageCesium.h @@ -1,8 +1,8 @@ #pragma once +#include "CesiumAsync/SharedAsset.h" #include "CesiumGltf/Ktx2TranscodeTargets.h" #include "CesiumGltf/Library.h" -#include "CesiumGltf/SharedAsset.h" #include #include @@ -30,7 +30,8 @@ struct CESIUMGLTF_API ImageCesiumMipPosition { * @brief Holds {@link Image} properties that are specific to the glTF loader * rather than part of the glTF spec. */ -struct CESIUMGLTF_API ImageCesium final : public SharedAsset { +struct CESIUMGLTF_API ImageCesium final + : public CesiumAsync::SharedAsset { /** * @brief The width of the image in pixels. */ diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h index 5e38dde0f..b8abb23ec 100644 --- a/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h +++ b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include namespace CesiumGltf { @@ -36,10 +36,11 @@ class SharedAssetSystem ->getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); } - const SharedAssetDepot& image(); + const CesiumAsync::SharedAssetDepot& image(); private: - CesiumUtility::IntrusivePointer> + CesiumUtility::IntrusivePointer< + CesiumAsync::SharedAssetDepot> _pImages; }; diff --git a/CesiumGltf/include/CesiumGltf/TextureView.h b/CesiumGltf/include/CesiumGltf/TextureView.h index 35faa8938..22f677cef 100644 --- a/CesiumGltf/include/CesiumGltf/TextureView.h +++ b/CesiumGltf/include/CesiumGltf/TextureView.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/CesiumGltf/src/SharedAssetSystem.cpp b/CesiumGltf/src/SharedAssetSystem.cpp index a07c324b9..aac4114ac 100644 --- a/CesiumGltf/src/SharedAssetSystem.cpp +++ b/CesiumGltf/src/SharedAssetSystem.cpp @@ -4,11 +4,12 @@ namespace CesiumGltf { SharedAssetSystem::SharedAssetSystem() noexcept - : _pImages(new SharedAssetDepot()) {} + : _pImages(new CesiumAsync::SharedAssetDepot()) {} SharedAssetSystem::~SharedAssetSystem() noexcept = default; -const SharedAssetDepot& SharedAssetSystem::image() { +const CesiumAsync::SharedAssetDepot& +SharedAssetSystem::image() { return *this->_pImages; } diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 0d52739b8..5914cc980 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -11,11 +11,11 @@ #include #include #include +#include #include #include #include #include -#include #include #include #include From 7f798379f472bcc6c525019a67dec33f0813cff2 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 21:56:09 +1100 Subject: [PATCH 48/81] GltfSharedAssetSystem --- .../include/Cesium3DTilesSelection/Tileset.h | 5 +- .../src/TileContentLoadInfo.cpp | 4 +- .../src/TileContentLoadInfo.h | 9 ++-- Cesium3DTilesSelection/src/Tileset.cpp | 5 +- .../src/TilesetContentManager.cpp | 12 ++--- .../src/TilesetContentManager.h | 6 ++- .../src/TilesetJsonLoader.h | 5 +- .../test/TestTilesetContentManager.cpp | 2 +- .../include/CesiumGltf/SharedAssetSystem.h | 47 ------------------- CesiumGltf/src/SharedAssetSystem.cpp | 16 ------- .../include/CesiumGltfReader/GltfReader.h | 11 ++--- .../CesiumGltfReader/GltfSharedAssetSystem.h | 26 ++++++++++ CesiumGltfReader/src/GltfReader.cpp | 5 +- .../src/GltfSharedAssetSystem.cpp | 26 ++++++++++ 14 files changed, 87 insertions(+), 92 deletions(-) delete mode 100644 CesiumGltf/include/CesiumGltf/SharedAssetSystem.h delete mode 100644 CesiumGltf/src/SharedAssetSystem.cpp create mode 100644 CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h create mode 100644 CesiumGltfReader/src/GltfSharedAssetSystem.cpp diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 09dbe01ac..b8f9699bf 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -184,10 +184,11 @@ class CESIUM3DTILESSELECTION_API Tileset final { /** * @brief Returns the {@link SharedAssetDepot} of this tileset. */ - CesiumGltf::SharedAssetSystem& getSharedAssetSystem() noexcept; + CesiumGltfReader::GltfSharedAssetSystem& getSharedAssetSystem() noexcept; /** @copydoc Tileset::getSharedAssetSystem() */ - const CesiumGltf::SharedAssetSystem& getSharedAssetSystem() const noexcept; + const CesiumGltfReader::GltfSharedAssetSystem& + getSharedAssetSystem() const noexcept; /** * @brief Updates this view but waits for all tiles that meet sse to finish diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp index b09c988cb..f643f71e4 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp @@ -9,8 +9,8 @@ TileContentLoadInfo::TileContentLoadInfo( const std::shared_ptr& pPrepareRendererResources_, const std::shared_ptr& pLogger_, - const CesiumUtility::IntrusivePointer - pAssetDepot_, + const CesiumUtility::IntrusivePointer< + CesiumGltfReader::GltfSharedAssetSystem> pAssetDepot_, const TilesetContentOptions& contentOptions_, const Tile& tile) : asyncSystem(asyncSystem_), diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.h b/Cesium3DTilesSelection/src/TileContentLoadInfo.h index a887d1c60..4c9d89e3e 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.h +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.h @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -25,8 +25,8 @@ struct TileContentLoadInfo { const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, - const CesiumUtility::IntrusivePointer - maybeAssetDepot, + const CesiumUtility::IntrusivePointer< + CesiumGltfReader::GltfSharedAssetSystem> maybeAssetDepot, const TilesetContentOptions& contentOptions, const Tile& tile); @@ -43,7 +43,8 @@ struct TileContentLoadInfo { BoundingVolume tileBoundingVolume; std::optional tileContentBoundingVolume; - CesiumUtility::IntrusivePointer pAssetDepot; + CesiumUtility::IntrusivePointer + pAssetDepot; TileRefine tileRefine; diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index f5b1b7b5c..a98af5d23 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -149,11 +149,12 @@ const RasterOverlayCollection& Tileset::getOverlays() const noexcept { return this->_pTilesetContentManager->getRasterOverlayCollection(); } -CesiumGltf::SharedAssetSystem& Tileset::getSharedAssetSystem() noexcept { +CesiumGltfReader::GltfSharedAssetSystem& +Tileset::getSharedAssetSystem() noexcept { return *this->_pTilesetContentManager->getSharedAssetSystem(); } -const CesiumGltf::SharedAssetSystem& +const CesiumGltfReader::GltfSharedAssetSystem& Tileset::getSharedAssetSystem() const noexcept { return *this->_pTilesetContentManager->getSharedAssetSystem(); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index cf0a8f223..ec4bc45e3 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -666,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pAssetDepot(new CesiumGltf::SharedAssetSystem()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::default()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pAssetDepot(new CesiumGltf::SharedAssetSystem()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::default()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pAssetDepot(new CesiumGltf::SharedAssetSystem()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::default()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -994,7 +994,7 @@ void TilesetContentManager::loadTileContent( this->_externals.pAssetAccessor, this->_externals.pPrepareRendererResources, this->_externals.pLogger, - this->_pAssetDepot, + this->_pSharedAssets, tilesetOptions.contentOptions, tile}; @@ -1237,9 +1237,9 @@ TilesetContentManager::getTilesetCredits() const noexcept { return this->_tilesetCredits; } -const CesiumUtility::IntrusivePointer& +const CesiumUtility::IntrusivePointer& TilesetContentManager::getSharedAssetSystem() const noexcept { - return this->_pAssetDepot; + return this->_pSharedAssets; } int32_t TilesetContentManager::getNumberOfTilesLoading() const noexcept { diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 4a4ec286f..12bd65484 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -115,7 +115,8 @@ class TilesetContentManager const std::vector& getTilesetCredits() const noexcept; - const CesiumUtility::IntrusivePointer& + const CesiumUtility::IntrusivePointer< + CesiumGltfReader::GltfSharedAssetSystem>& getSharedAssetSystem() const noexcept; int32_t getNumberOfTilesLoading() const noexcept; @@ -171,7 +172,8 @@ class TilesetContentManager int64_t _tilesDataUsed; // Stores assets that might be shared between tiles. - CesiumUtility::IntrusivePointer _pAssetDepot; + CesiumUtility::IntrusivePointer + _pSharedAssets; CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index 0039edc6e..fa590bdd6 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include @@ -56,7 +56,8 @@ class TilesetJsonLoader : public TilesetContentLoader { private: std::string _baseUrl; CesiumGeospatial::Ellipsoid _ellipsoid; - CesiumUtility::IntrusivePointer _pAssetDepot; + CesiumUtility::IntrusivePointer + _pSharedAssets; /** * @brief The axis that was declared as the "up-axis" for glTF content. diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index f297c54d4..af048b2ea 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1682,7 +1682,7 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CHECK(images.size() == 1); } - CHECK(pManager->getSharedAssetSystem()->image().getDistinctCount() == 2); + CHECK(pManager->getSharedAssetSystem()->pImage->getDistinctCount() == 2); // unload the tile content for (auto& child : containerTile.getChildren()) { diff --git a/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h b/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h deleted file mode 100644 index b8abb23ec..000000000 --- a/CesiumGltf/include/CesiumGltf/SharedAssetSystem.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include - -namespace CesiumGltf { - -struct ImageCesium; - -/** - * @brief Contains assets that are potentially shared across multiple glTF - * models. - */ -class SharedAssetSystem - : public CesiumUtility::ReferenceCountedThreadSafe { -public: - SharedAssetSystem() noexcept; - ~SharedAssetSystem() noexcept; - - SharedAssetSystem(const SharedAssetSystem&) = delete; - void operator=(const SharedAssetSystem& other) = delete; - - /** - * Obtains an existing {@link ImageCesium} or constructs a new one using the provided factory. - */ - template - CesiumAsync::SharedFuture< - CesiumUtility::IntrusivePointer> - getOrFetch( - const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const Factory& factory, - const std::string& uri, - const std::vector& headers) { - return this->_pImages - ->getOrFetch(asyncSystem, pAssetAccessor, factory, uri, headers); - } - - const CesiumAsync::SharedAssetDepot& image(); - -private: - CesiumUtility::IntrusivePointer< - CesiumAsync::SharedAssetDepot> - _pImages; -}; - -} // namespace CesiumGltf diff --git a/CesiumGltf/src/SharedAssetSystem.cpp b/CesiumGltf/src/SharedAssetSystem.cpp deleted file mode 100644 index aac4114ac..000000000 --- a/CesiumGltf/src/SharedAssetSystem.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include - -namespace CesiumGltf { - -SharedAssetSystem::SharedAssetSystem() noexcept - : _pImages(new CesiumAsync::SharedAssetDepot()) {} - -SharedAssetSystem::~SharedAssetSystem() noexcept = default; - -const CesiumAsync::SharedAssetDepot& -SharedAssetSystem::image() { - return *this->_pImages; -} - -} // namespace CesiumGltf diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index a1e941bd7..d8f086429 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -111,12 +111,11 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; /** - * The depot that will be used to store all of the shared assets that might - * appear in this glTF. If not present, assets will not be shared between - * glTFs, even if they're loaded from the same URL. + * The shared asset system that will be used to store all of the shared assets + * that might appear in this glTF. */ - CesiumUtility::IntrusivePointer pSharedAssets = - nullptr; + CesiumUtility::IntrusivePointer pSharedAssets = + GltfSharedAssetSystem::default(); }; /** diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h new file mode 100644 index 000000000..71d6c2536 --- /dev/null +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace CesiumGltf { +struct ImageCesium; +} + +namespace CesiumGltfReader { + +/** + * @brief Contains assets that are potentially shared across multiple glTF + * models. + */ +class GltfSharedAssetSystem + : public CesiumUtility::ReferenceCountedThreadSafe { +public: + static CesiumUtility::IntrusivePointer default(); + + CesiumUtility::IntrusivePointer< + CesiumAsync::SharedAssetDepot> + pImage; +}; + +} // namespace CesiumGltfReader diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 5914cc980..495ea5a37 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -555,7 +555,8 @@ void CesiumGltfReader::GltfReader::postprocessGltf( const std::shared_ptr& pAssetAccessor, const std::string& uri, const std::vector& headers) { - if (options.pSharedAssets == nullptr) { + if (options.pSharedAssets == nullptr || + options.pSharedAssets->pImage == nullptr) { // We don't have a depot, we have to fetch this the old way. return pAssetAccessor->get(asyncSystem, uri, headers) .thenInWorkerThread( @@ -580,7 +581,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( .share(); } else { // We have a depot, this is easy! - return options.pSharedAssets->getOrFetch( + return options.pSharedAssets->pImage->getOrFetch( asyncSystem, pAssetAccessor, ImageAssetFactory(options.ktx2TranscodeTargets), diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp new file mode 100644 index 000000000..0f98f38df --- /dev/null +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -0,0 +1,26 @@ +#include +#include + +namespace CesiumGltfReader { + +namespace { + +CesiumUtility::IntrusivePointer createDefault() { + CesiumUtility::IntrusivePointer p = + new GltfSharedAssetSystem(); + + p->pImage.emplace(); + + return p; +} + +} // namespace + +/*static*/ CesiumUtility::IntrusivePointer + GltfSharedAssetSystem::default() { + static CesiumUtility::IntrusivePointer pDefault = + createDefault(); + return pDefault; +} + +} // namespace CesiumGltfReader From 526e660687d64d98b3f1aea3b8338dafbac0b1db Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 22:19:52 +1100 Subject: [PATCH 49/81] default -> getDefault --- Cesium3DTilesSelection/src/TilesetContentManager.cpp | 6 +++--- CesiumGltfReader/include/CesiumGltfReader/GltfReader.h | 2 +- .../include/CesiumGltfReader/GltfSharedAssetSystem.h | 2 +- CesiumGltfReader/src/GltfSharedAssetSystem.cpp | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index ec4bc45e3..7595bae46 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -666,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::default()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::default()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::default()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index d8f086429..221d39627 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -115,7 +115,7 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * that might appear in this glTF. */ CesiumUtility::IntrusivePointer pSharedAssets = - GltfSharedAssetSystem::default(); + GltfSharedAssetSystem::getDefault(); }; /** diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index 71d6c2536..188b6fb5c 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -16,7 +16,7 @@ namespace CesiumGltfReader { class GltfSharedAssetSystem : public CesiumUtility::ReferenceCountedThreadSafe { public: - static CesiumUtility::IntrusivePointer default(); + static CesiumUtility::IntrusivePointer getDefault(); CesiumUtility::IntrusivePointer< CesiumAsync::SharedAssetDepot> diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index 0f98f38df..584a842f9 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -17,7 +17,7 @@ CesiumUtility::IntrusivePointer createDefault() { } // namespace /*static*/ CesiumUtility::IntrusivePointer - GltfSharedAssetSystem::default() { +GltfSharedAssetSystem::getDefault() { static CesiumUtility::IntrusivePointer pDefault = createDefault(); return pDefault; From df1f887e6d2d2cd599f5fb3168cde67b496ea3d3 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Thu, 10 Oct 2024 11:26:58 -0400 Subject: [PATCH 50/81] Doc comments, rename ImageCesium --- .../IPrepareRendererResources.h | 2 +- .../test/SimplePrepareRendererResource.h | 2 +- .../test/TestTilesetContentManager.cpp | 4 +- CesiumAsync/include/CesiumAsync/SharedAsset.h | 41 ++++++++++++++++--- .../include/CesiumGltf/FeatureIdTextureView.h | 2 +- CesiumGltf/include/CesiumGltf/Image.h | 4 +- .../{ImageCesium.h => ImageAsset.h} | 4 +- .../CesiumGltf/PropertyTexturePropertyView.h | 10 ++--- .../include/CesiumGltf/PropertyTextureView.h | 4 +- CesiumGltf/include/CesiumGltf/TextureView.h | 14 +++---- CesiumGltf/src/PropertyTextureView.cpp | 4 +- CesiumGltf/src/TextureView.cpp | 10 ++--- CesiumGltf/test/TestFeatureIdTextureView.cpp | 2 +- .../test/TestPropertyTexturePropertyView.cpp | 36 ++++++++-------- .../CesiumGltfContent/ImageManipulation.h | 10 ++--- CesiumGltfContent/src/ImageManipulation.cpp | 10 ++--- .../test/TestImageManipulation.cpp | 6 +-- .../include/CesiumGltfReader/GltfReader.h | 4 +- .../CesiumGltfReader/GltfSharedAssetSystem.h | 4 +- .../include/CesiumGltfReader/ImageDecoder.h | 8 ++-- CesiumGltfReader/src/GltfReader.cpp | 12 +++--- .../src/GltfSharedAssetSystem.cpp | 2 +- CesiumGltfReader/src/ImageDecoder.cpp | 4 +- CesiumGltfReader/test/TestGltfReader.cpp | 8 ++-- .../include/CesiumGltfWriter/GltfWriter.h | 4 +- .../IPrepareRasterOverlayRendererResources.h | 4 +- .../CesiumRasterOverlays/RasterOverlayTile.h | 6 +-- .../RasterOverlayTileProvider.h | 2 +- .../src/DebugColorizeTilesRasterOverlay.cpp | 2 +- .../src/QuadtreeRasterOverlayTileProvider.cpp | 10 ++--- .../src/RasterOverlayTileProvider.cpp | 8 ++-- .../src/RasterizedPolygonsOverlay.cpp | 2 +- .../TestQuadtreeRasterOverlayTileProvider.cpp | 4 +- .../test/TestTileMapServiceRasterOverlay.cpp | 2 +- 34 files changed, 140 insertions(+), 111 deletions(-) rename CesiumGltf/include/CesiumGltf/{ImageCesium.h => ImageAsset.h} (97%) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h index 19efe0537..b7977fd62 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h @@ -20,7 +20,7 @@ struct Rectangle; } namespace CesiumGltf { -struct ImageCesium; +struct ImageAsset; struct Model; } // namespace CesiumGltf diff --git a/Cesium3DTilesSelection/test/SimplePrepareRendererResource.h b/Cesium3DTilesSelection/test/SimplePrepareRendererResource.h index 19126357c..e980a0057 100644 --- a/Cesium3DTilesSelection/test/SimplePrepareRendererResource.h +++ b/Cesium3DTilesSelection/test/SimplePrepareRendererResource.h @@ -68,7 +68,7 @@ class SimplePrepareRendererResource } virtual void* prepareRasterInLoadThread( - CesiumGltf::ImageCesium& /*image*/, + CesiumGltf::ImageAsset& /*image*/, const std::any& /*rendererOptions*/) override { return new AllocationResult{totalAllocation}; } diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index af048b2ea..72f809743 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1298,8 +1298,8 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CesiumAsync::Future loadTileImage(RasterOverlayTile& overlayTile) override { - CesiumUtility::IntrusivePointer pImage; - CesiumGltf::ImageCesium& image = pImage.emplace(); + CesiumUtility::IntrusivePointer pImage; + CesiumGltf::ImageAsset& image = pImage.emplace(); image.width = 1; image.height = 1; image.channels = 1; diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 3a34cfda8..39b03c3e0 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -19,28 +19,57 @@ namespace CesiumAsync { * @tparam T The type that is _deriving_ from this class. For example, you * should declare your class as * `class MyClass : public SharedAsset { ... };` + * + * @remarks @parblock + * A `SharedAsset` can be in one of three possible states: + * + * **Independent Asset** + * An independent asset isn't affiliated with an asset depot at all. + * Its lifetime is controlled exclusively by IntrusivePointer / reference + * counting. When the asset's reference count goes to zero, it deletes itself. + * An independent asset's `_pDepot` is nullptr. + * + * **Active Depot Asset** + * This is an asset that is owned by an asset depot and that is in use, meaning + * it has a reference count greater than zero. The asset depot owns the asset + * via an `std::unique_ptr`, not via adding to the reference count. So when the + * reference count goes to zero, only the asset depot itself still has a + * reference to it, so it becomes an inactive depot asset. + * + * **Inactive Depot Asset** + * This is also an asset that is owned by the asset depot, but there are no + * other references to it (it has a reference count of zero). It is found in the + * asset depot's `deletionCandidates` list. When a reference to it is added, it + * is removed from `deletionCandidates` and it becomes an active depot asset. + * @endparblock */ template class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { public: - // Assets can be copied, but the fresh instance has no references and is not - // in the asset depot. + /** + * Assets can be copied, but the fresh instance has no references and is not + * in the asset depot. + */ SharedAsset(const SharedAsset& rhs) : ExtensibleObject(rhs), _referenceCount(0), _pDepot(nullptr), _uniqueAssetId() {} - // After a move construction, the content of the asset is moved to the new - // instance, but the asset depot still references the old instance. + /** + * After a move construction, the content of the asset is moved to the new + * instance, but the asset depot still references the old instance. + */ SharedAsset(SharedAsset&& rhs) : ExtensibleObject(std::move(rhs)), _referenceCount(0), _pDepot(nullptr), _uniqueAssetId() {} - // Assignment does not affect the asset's relationship with the depot, but is - // useful to assign the data in derived classes. + /** + * Assignment does not affect the asset's relationship with the depot, but is + * useful to assign the data in derived classes. + */ SharedAsset& operator=(const SharedAsset& rhs) { CesiumUtility::ExtensibleObject::operator=(rhs); return *this; diff --git a/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h b/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h index 807838618..1cfce2c7d 100644 --- a/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h +++ b/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h @@ -2,7 +2,7 @@ #include "CesiumGltf/FeatureIdTexture.h" #include "CesiumGltf/Image.h" -#include "CesiumGltf/ImageCesium.h" +#include "CesiumGltf/ImageAsset.h" #include "CesiumGltf/KhrTextureTransform.h" #include "CesiumGltf/Texture.h" #include "CesiumGltf/TextureView.h" diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index b2a45c046..9b4fb0d43 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -1,6 +1,6 @@ #pragma once -#include "CesiumGltf/ImageCesium.h" +#include "CesiumGltf/ImageAsset.h" #include "CesiumGltf/ImageSpec.h" #include "CesiumGltf/Library.h" @@ -14,6 +14,6 @@ struct CESIUMGLTF_API Image final : public ImageSpec { * part of the glTF spec. When an image is loaded from a URL, multiple `Image` * instances may all point to the same `ImageCesium` instance. */ - CesiumUtility::IntrusivePointer pCesium; + CesiumUtility::IntrusivePointer pCesium; }; } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/ImageCesium.h b/CesiumGltf/include/CesiumGltf/ImageAsset.h similarity index 97% rename from CesiumGltf/include/CesiumGltf/ImageCesium.h rename to CesiumGltf/include/CesiumGltf/ImageAsset.h index 6feedf656..3772500a6 100644 --- a/CesiumGltf/include/CesiumGltf/ImageCesium.h +++ b/CesiumGltf/include/CesiumGltf/ImageAsset.h @@ -30,8 +30,8 @@ struct CESIUMGLTF_API ImageCesiumMipPosition { * @brief Holds {@link Image} properties that are specific to the glTF loader * rather than part of the glTF spec. */ -struct CESIUMGLTF_API ImageCesium final - : public CesiumAsync::SharedAsset { +struct CESIUMGLTF_API ImageAsset final + : public CesiumAsync::SharedAsset { /** * @brief The width of the image in pixels. */ diff --git a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h index f3dcd168e..d35b46e7a 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h @@ -1,6 +1,6 @@ #pragma once -#include "CesiumGltf/ImageCesium.h" +#include "CesiumGltf/ImageAsset.h" #include "CesiumGltf/KhrTextureTransform.h" #include "CesiumGltf/PropertyTextureProperty.h" #include "CesiumGltf/PropertyTransformations.h" @@ -290,7 +290,7 @@ class PropertyTexturePropertyView * @param property The {@link PropertyTextureProperty} * @param classProperty The {@link ClassProperty} this property conforms to. * @param sampler The {@link Sampler} used by the property. - * @param image The {@link ImageCesium} used by the property. + * @param image The {@link ImageAsset} used by the property. * @param channels The value of {@link PropertyTextureProperty::channels}. * @param options The options for constructing the view. */ @@ -298,7 +298,7 @@ class PropertyTexturePropertyView const PropertyTextureProperty& property, const ClassProperty& classProperty, const Sampler& sampler, - const ImageCesium& image, + const ImageAsset& image, const TextureViewOptions& options = TextureViewOptions()) noexcept : PropertyView(classProperty, property), TextureView( @@ -523,7 +523,7 @@ class PropertyTexturePropertyView * @param property The {@link PropertyTextureProperty} * @param classProperty The {@link ClassProperty} this property conforms to. * @param sampler The {@link Sampler} used by the property. - * @param image The {@link ImageCesium} used by the property. + * @param image The {@link ImageAsset} used by the property. * @param channels The value of {@link PropertyTextureProperty::channels}. * @param options The options for constructing the view. */ @@ -531,7 +531,7 @@ class PropertyTexturePropertyView const PropertyTextureProperty& property, const ClassProperty& classProperty, const Sampler& sampler, - const ImageCesium& image, + const ImageAsset& image, const TextureViewOptions& options = TextureViewOptions()) noexcept : PropertyView(classProperty, property), TextureView( diff --git a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h index 6fc8ec158..42a18619e 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h @@ -748,7 +748,7 @@ class PropertyTextureView { return PropertyTexturePropertyView(status); } - const CesiumUtility::IntrusivePointer& pImage = + const CesiumUtility::IntrusivePointer& pImage = _pModel->images[imageIndex].pCesium; const std::vector& channels = propertyTextureProperty.channels; @@ -781,7 +781,7 @@ class PropertyTextureView { PropertyViewStatusType checkChannels( const std::vector& channels, - const ImageCesium& image) const noexcept; + const ImageAsset& image) const noexcept; const Model* _pModel; const PropertyTexture* _pPropertyTexture; diff --git a/CesiumGltf/include/CesiumGltf/TextureView.h b/CesiumGltf/include/CesiumGltf/TextureView.h index 22f677cef..ef681dd74 100644 --- a/CesiumGltf/include/CesiumGltf/TextureView.h +++ b/CesiumGltf/include/CesiumGltf/TextureView.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -113,10 +113,10 @@ class TextureView { /** * @brief Constructs a view of the texture specified by the given {@link Sampler} - * and {@link ImageCesium}. + * and {@link ImageAsset}. * * @param sampler The {@link Sampler} used by the texture. - * @param image The {@link ImageCesium} used by the texture. + * @param image The {@link ImageAsset} used by the texture. * @param textureCoordinateSetIndex The set index for the `TEXCOORD_n` * attribute used to sample this texture. * @param pKhrTextureTransformExtension A pointer to the KHR_texture_transform @@ -125,7 +125,7 @@ class TextureView { */ TextureView( const Sampler& sampler, - const ImageCesium& image, + const ImageAsset& image, int64_t textureCoordinateSetIndex, const ExtensionKhrTextureTransform* pKhrTextureTransformExtension = nullptr, @@ -173,7 +173,7 @@ class TextureView { * This will be nullptr if the texture view runs into * problems during construction. */ - const ImageCesium* getImage() const noexcept { + const ImageAsset* getImage() const noexcept { if (this->_pImageCopy) { return this->_pImageCopy.get(); } @@ -211,12 +211,12 @@ class TextureView { TextureViewStatus _textureViewStatus; const Sampler* _pSampler; - CesiumUtility::IntrusivePointer _pImage; + CesiumUtility::IntrusivePointer _pImage; int64_t _texCoordSetIndex; bool _applyTextureTransform; std::optional _textureTransform; - CesiumUtility::IntrusivePointer _pImageCopy; + CesiumUtility::IntrusivePointer _pImageCopy; }; } // namespace CesiumGltf diff --git a/CesiumGltf/src/PropertyTextureView.cpp b/CesiumGltf/src/PropertyTextureView.cpp index 1b0da8aa2..16ad1fae4 100644 --- a/CesiumGltf/src/PropertyTextureView.cpp +++ b/CesiumGltf/src/PropertyTextureView.cpp @@ -80,7 +80,7 @@ PropertyTextureView::checkImage(const int32_t imageIndex) const noexcept { return PropertyTexturePropertyViewStatus::ErrorInvalidImage; } - const CesiumUtility::IntrusivePointer& pImage = + const CesiumUtility::IntrusivePointer& pImage = _pModel->images[static_cast(imageIndex)].pCesium; if (pImage->width < 1 || pImage->height < 1) { @@ -96,7 +96,7 @@ PropertyTextureView::checkImage(const int32_t imageIndex) const noexcept { PropertyViewStatusType PropertyTextureView::checkChannels( const std::vector& channels, - const ImageCesium& image) const noexcept { + const ImageAsset& image) const noexcept { if (channels.size() <= 0 || channels.size() > 4) { return PropertyTexturePropertyViewStatus::ErrorInvalidChannels; } diff --git a/CesiumGltf/src/TextureView.cpp b/CesiumGltf/src/TextureView.cpp index c41dfae0f..84d1507cc 100644 --- a/CesiumGltf/src/TextureView.cpp +++ b/CesiumGltf/src/TextureView.cpp @@ -72,7 +72,7 @@ TextureView::TextureView( if (options.makeImageCopy) { this->_pImageCopy = - this->_pImage ? new ImageCesium(*this->_pImage) : nullptr; + this->_pImage ? new ImageAsset(*this->_pImage) : nullptr; } this->_textureViewStatus = TextureViewStatus::Valid; @@ -80,13 +80,13 @@ TextureView::TextureView( TextureView::TextureView( const Sampler& sampler, - const ImageCesium& image, + const ImageAsset& image, int64_t textureCoordinateSetIndex, const ExtensionKhrTextureTransform* pKhrTextureTransformExtension, const TextureViewOptions& options) noexcept : _textureViewStatus(TextureViewStatus::ErrorUninitialized), _pSampler(&sampler), - _pImage(new ImageCesium(image)), + _pImage(new ImageAsset(image)), _texCoordSetIndex(textureCoordinateSetIndex), _applyTextureTransform(options.applyKhrTextureTransformExtension), _textureTransform(std::nullopt), @@ -106,7 +106,7 @@ TextureView::TextureView( if (options.makeImageCopy) { this->_pImageCopy = - this->_pImage ? new ImageCesium(*this->_pImage) : nullptr; + this->_pImage ? new ImageAsset(*this->_pImage) : nullptr; } this->_textureViewStatus = TextureViewStatus::Valid; @@ -132,7 +132,7 @@ std::vector TextureView::sampleNearestPixel( u = applySamplerWrapS(u, this->_pSampler->wrapS); v = applySamplerWrapT(v, this->_pSampler->wrapT); - const ImageCesium& image = + const ImageAsset& image = this->_pImageCopy != nullptr ? *this->_pImageCopy : *this->_pImage; // For nearest filtering, std::floor is used instead of std::round. diff --git a/CesiumGltf/test/TestFeatureIdTextureView.cpp b/CesiumGltf/test/TestFeatureIdTextureView.cpp index 12bee9135..3acd813ac 100644 --- a/CesiumGltf/test/TestFeatureIdTextureView.cpp +++ b/CesiumGltf/test/TestFeatureIdTextureView.cpp @@ -414,7 +414,7 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { std::vector emptyData; image.pCesium->pixelData.swap(emptyData); - const ImageCesium* pImage = view.getImage(); + const ImageAsset* pImage = view.getImage(); REQUIRE(pImage); REQUIRE(pImage->width == image.pCesium->width); REQUIRE(pImage->height == image.pCesium->height); diff --git a/CesiumGltf/test/TestPropertyTexturePropertyView.cpp b/CesiumGltf/test/TestPropertyTexturePropertyView.cpp index ae3a815fb..eb71c2ac3 100644 --- a/CesiumGltf/test/TestPropertyTexturePropertyView.cpp +++ b/CesiumGltf/test/TestPropertyTexturePropertyView.cpp @@ -27,7 +27,7 @@ void checkTextureValues( convertPropertyComponentTypeToString(componentType); Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = static_cast(sizeof(T)); @@ -99,7 +99,7 @@ void checkTextureValues( classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = static_cast(sizeof(T)); @@ -173,7 +173,7 @@ void checkNormalizedTextureValues( classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = static_cast(sizeof(T)); @@ -243,7 +243,7 @@ void checkTextureArrayValues( classProperty.count = count; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = @@ -329,7 +329,7 @@ void checkTextureArrayValues( classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = @@ -429,7 +429,7 @@ void checkNormalizedTextureArrayValues( classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = @@ -1397,7 +1397,7 @@ TEST_CASE("Check that PropertyTextureProperty values override class property " classProperty.max = 10.0f; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1474,7 +1474,7 @@ TEST_CASE("Check that non-adjacent channels resolve to expected output") { classProperty.componentType = ClassProperty::ComponentType::UINT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1516,7 +1516,7 @@ TEST_CASE("Check that non-adjacent channels resolve to expected output") { classProperty.componentType = ClassProperty::ComponentType::UINT16; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1557,7 +1557,7 @@ TEST_CASE("Check that non-adjacent channels resolve to expected output") { classProperty.componentType = ClassProperty::ComponentType::UINT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1603,7 +1603,7 @@ TEST_CASE("Check that non-adjacent channels resolve to expected output") { classProperty.array = true; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1660,7 +1660,7 @@ TEST_CASE("Check sampling with different sampler values") { classProperty.type = ClassProperty::Type::SCALAR; classProperty.componentType = ClassProperty::ComponentType::UINT8; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -1793,7 +1793,7 @@ TEST_CASE("Test PropertyTextureProperty constructs with " sampler.wrapS = Sampler::WrapS::REPEAT; sampler.wrapT = Sampler::WrapT::REPEAT; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -1866,7 +1866,7 @@ TEST_CASE("Test normalized PropertyTextureProperty constructs with " sampler.wrapS = Sampler::WrapS::REPEAT; sampler.wrapT = Sampler::WrapT::REPEAT; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -1940,7 +1940,7 @@ TEST_CASE("Test PropertyTextureProperty constructs with " sampler.wrapS = Sampler::WrapS::REPEAT; sampler.wrapT = Sampler::WrapT::REPEAT; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -1963,7 +1963,7 @@ TEST_CASE("Test PropertyTextureProperty constructs with " std::vector emptyData; image.pixelData.swap(emptyData); - const ImageCesium* pImage = view.getImage(); + const ImageAsset* pImage = view.getImage(); REQUIRE(pImage); REQUIRE(pImage->width == image.width); REQUIRE(pImage->height == image.height); @@ -2007,7 +2007,7 @@ TEST_CASE("Test normalized PropertyTextureProperty constructs with " sampler.wrapS = Sampler::WrapS::REPEAT; sampler.wrapT = Sampler::WrapT::REPEAT; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -2030,7 +2030,7 @@ TEST_CASE("Test normalized PropertyTextureProperty constructs with " std::vector emptyData; image.pixelData.swap(emptyData); - const ImageCesium* pImage = view.getImage(); + const ImageAsset* pImage = view.getImage(); REQUIRE(pImage); REQUIRE(pImage->width == image.width); REQUIRE(pImage->height == image.height); diff --git a/CesiumGltfContent/include/CesiumGltfContent/ImageManipulation.h b/CesiumGltfContent/include/CesiumGltfContent/ImageManipulation.h index 82b3e2466..2f350629b 100644 --- a/CesiumGltfContent/include/CesiumGltfContent/ImageManipulation.h +++ b/CesiumGltfContent/include/CesiumGltfContent/ImageManipulation.h @@ -7,7 +7,7 @@ // Forward declarations namespace CesiumGltf { -struct ImageCesium; +struct ImageAsset; } namespace CesiumGltfContent { @@ -91,9 +91,9 @@ class CESIUMGLTFCONTENT_API ImageManipulation { * incompatible formats. */ static bool blitImage( - CesiumGltf::ImageCesium& target, + CesiumGltf::ImageAsset& target, const PixelRectangle& targetPixels, - const CesiumGltf::ImageCesium& source, + const CesiumGltf::ImageAsset& source, const PixelRectangle& sourcePixels); /** @@ -103,7 +103,7 @@ class CESIUMGLTFCONTENT_API ImageManipulation { * @return The byte buffer containing the image. If the buffer is empty, the * image could not be written. */ - static std::vector savePng(const CesiumGltf::ImageCesium& image); + static std::vector savePng(const CesiumGltf::ImageAsset& image); /** * @brief Saves an image to an existing byte buffer in PNG format. @@ -114,7 +114,7 @@ class CESIUMGLTFCONTENT_API ImageManipulation { * could not be written. */ static void - savePng(const CesiumGltf::ImageCesium& image, std::vector& output); + savePng(const CesiumGltf::ImageAsset& image, std::vector& output); }; } // namespace CesiumGltfContent diff --git a/CesiumGltfContent/src/ImageManipulation.cpp b/CesiumGltfContent/src/ImageManipulation.cpp index c8de11e06..9bd129a3a 100644 --- a/CesiumGltfContent/src/ImageManipulation.cpp +++ b/CesiumGltfContent/src/ImageManipulation.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -43,9 +43,9 @@ void ImageManipulation::unsafeBlitImage( } bool ImageManipulation::blitImage( - CesiumGltf::ImageCesium& target, + CesiumGltf::ImageAsset& target, const PixelRectangle& targetPixels, - const CesiumGltf::ImageCesium& source, + const CesiumGltf::ImageAsset& source, const PixelRectangle& sourcePixels) { if (sourcePixels.x < 0 || sourcePixels.y < 0 || sourcePixels.width < 0 || @@ -134,7 +134,7 @@ void writePngToVector(void* context, void* data, int size) { } // namespace /*static*/ void ImageManipulation::savePng( - const CesiumGltf::ImageCesium& image, + const CesiumGltf::ImageAsset& image, std::vector& output) { if (image.bytesPerChannel != 1) { // Only 8-bit images can be written. @@ -152,7 +152,7 @@ void writePngToVector(void* context, void* data, int size) { } /*static*/ std::vector -ImageManipulation::savePng(const CesiumGltf::ImageCesium& image) { +ImageManipulation::savePng(const CesiumGltf::ImageAsset& image) { std::vector result; savePng(image, result); return result; diff --git a/CesiumGltfContent/test/TestImageManipulation.cpp b/CesiumGltfContent/test/TestImageManipulation.cpp index 38833411b..227e9a8c6 100644 --- a/CesiumGltfContent/test/TestImageManipulation.cpp +++ b/CesiumGltfContent/test/TestImageManipulation.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -128,7 +128,7 @@ TEST_CASE("ImageManipulation::unsafeBlitImage subset of source") { } TEST_CASE("ImageManipulation::blitImage") { - ImageCesium target; + ImageAsset target; target.bytesPerChannel = 2; target.channels = 4; target.width = 15; @@ -139,7 +139,7 @@ TEST_CASE("ImageManipulation::blitImage") { target.bytesPerChannel), std::byte(1)); - ImageCesium source; + ImageAsset source; source.bytesPerChannel = 2; source.channels = 4; source.width = 10; diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index 221d39627..f534ed6b9 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -217,7 +217,7 @@ class CESIUMGLTFREADER_API GltfReader { * @return A string describing the error, if unable to generate mipmaps. */ static std::optional - generateMipMaps(CesiumGltf::ImageCesium& image) { + generateMipMaps(CesiumGltf::ImageAsset& image) { return ImageDecoder::generateMipMaps(image); } diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index 188b6fb5c..46c4fb4f0 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -4,7 +4,7 @@ #include namespace CesiumGltf { -struct ImageCesium; +struct ImageAsset; } namespace CesiumGltfReader { @@ -19,7 +19,7 @@ class GltfSharedAssetSystem static CesiumUtility::IntrusivePointer getDefault(); CesiumUtility::IntrusivePointer< - CesiumAsync::SharedAssetDepot> + CesiumAsync::SharedAssetDepot> pImage; }; diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h index 76b783657..b2d60d2df 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -1,4 +1,4 @@ -#include "CesiumGltf/ImageCesium.h" +#include "CesiumGltf/ImageAsset.h" #include "CesiumGltfReader/Library.h" #include @@ -18,11 +18,11 @@ namespace CesiumGltfReader { struct CESIUMGLTFREADER_API ImageReaderResult { /** - * @brief The {@link ImageCesium} that was read. + * @brief The {@link ImageAsset} that was read. * * This will be `std::nullopt` if the image could not be read. */ - CesiumUtility::IntrusivePointer pImage; + CesiumUtility::IntrusivePointer pImage; /** * @brief Error messages that occurred while trying to read the image. @@ -63,7 +63,7 @@ class ImageDecoder { * @return A string describing the error, if unable to generate mipmaps. */ static std::optional - generateMipMaps(CesiumGltf::ImageCesium& image); + generateMipMaps(CesiumGltf::ImageAsset& image); }; } // namespace CesiumGltfReader diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 495ea5a37..03e2b5017 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -43,13 +43,13 @@ using namespace CesiumUtility; namespace { /** - * Used to construct an ImageCesium. + * Used to construct an ImageAsset. */ struct ImageAssetFactory { ImageAssetFactory(const Ktx2TranscodeTargets& ktx2TranscodeTargets_) : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} - CesiumUtility::IntrusivePointer + CesiumUtility::IntrusivePointer createFrom(const gsl::span& data) const { ImageReaderResult imageResult = ImageDecoder::readImage(data, this->ktx2TranscodeTargets); @@ -565,7 +565,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( const IAssetResponse* pResponse = pRequest->response(); - CesiumUtility::IntrusivePointer pAsset = + CesiumUtility::IntrusivePointer pAsset = nullptr; if (pResponse) { @@ -590,12 +590,12 @@ void CesiumGltfReader::GltfReader::postprocessGltf( } }; - SharedFuture> future = + SharedFuture> future = getAsset(asyncSystem, pAssetAccessor, uri, tHeaders); resolvedBuffers.push_back(future.thenInWorkerThread( [pImage = - &image](const IntrusivePointer& pLoadedImage) { + &image](const IntrusivePointer& pLoadedImage) { std::string imageUri = *pImage->uri; pImage->uri = std::nullopt; diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index 584a842f9..a5c803535 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -1,4 +1,4 @@ -#include +#include #include namespace CesiumGltfReader { diff --git a/CesiumGltfReader/src/ImageDecoder.cpp b/CesiumGltfReader/src/ImageDecoder.cpp index a55549473..664f7910c 100644 --- a/CesiumGltfReader/src/ImageDecoder.cpp +++ b/CesiumGltfReader/src/ImageDecoder.cpp @@ -77,7 +77,7 @@ ImageReaderResult ImageDecoder::readImage( ImageReaderResult result; - CesiumGltf::ImageCesium& image = result.pImage.emplace(); + CesiumGltf::ImageAsset& image = result.pImage.emplace(); if (isKtx(data)) { ktxTexture2* pTexture = nullptr; @@ -364,7 +364,7 @@ ImageReaderResult ImageDecoder::readImage( } /*static*/ -std::optional ImageDecoder::generateMipMaps(ImageCesium& image) { +std::optional ImageDecoder::generateMipMaps(ImageAsset& image) { if (!image.mipPositions.empty() || image.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { // No error message needed, since this is not technically a failure. diff --git a/CesiumGltfReader/test/TestGltfReader.cpp b/CesiumGltfReader/test/TestGltfReader.cpp index 01fa6b0dd..18561464d 100644 --- a/CesiumGltfReader/test/TestGltfReader.cpp +++ b/CesiumGltfReader/test/TestGltfReader.cpp @@ -574,7 +574,7 @@ TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { GltfReader::readImage(data, Ktx2TranscodeTargets{}); REQUIRE(imageResult.pImage); - const ImageCesium& image = *imageResult.pImage; + const ImageAsset& image = *imageResult.pImage; REQUIRE(image.mipPositions.size() == 1); CHECK(image.mipPositions[0].byteOffset == 0); CHECK(image.mipPositions[0].byteSize > 0); @@ -594,7 +594,7 @@ TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { GltfReader::readImage(data, Ktx2TranscodeTargets{}); REQUIRE(imageResult.pImage); - const ImageCesium& image = *imageResult.pImage; + const ImageAsset& image = *imageResult.pImage; REQUIRE(image.mipPositions.size() == 0); CHECK(image.pixelData.size() > 0); } @@ -608,7 +608,7 @@ TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { GltfReader::readImage(data, Ktx2TranscodeTargets{}); REQUIRE(imageResult.pImage); - const ImageCesium& image = *imageResult.pImage; + const ImageAsset& image = *imageResult.pImage; REQUIRE(image.mipPositions.size() == 9); CHECK(image.mipPositions[0].byteSize > 0); CHECK( @@ -689,7 +689,7 @@ TEST_CASE("Decodes images with data uris") { REQUIRE(model.images.size() == 1); - const ImageCesium& image = *model.images.front().pCesium; + const ImageAsset& image = *model.images.front().pCesium; CHECK(image.width == 256); CHECK(image.height == 256); diff --git a/CesiumGltfWriter/include/CesiumGltfWriter/GltfWriter.h b/CesiumGltfWriter/include/CesiumGltfWriter/GltfWriter.h index da7efde9a..97a2e6df5 100644 --- a/CesiumGltfWriter/include/CesiumGltfWriter/GltfWriter.h +++ b/CesiumGltfWriter/include/CesiumGltfWriter/GltfWriter.h @@ -75,7 +75,7 @@ class CESIUMGLTFWRITER_API GltfWriter { * @brief Serializes the provided model into a glTF JSON byte vector. * * @details Ignores internal data such as {@link CesiumGltf::BufferCesium} - * and {@link CesiumGltf::ImageCesium} when serializing the glTF. Internal + * and {@link CesiumGltf::ImageAsset} when serializing the glTF. Internal * data must either be converted to data uris or saved as external files. The * buffer.uri and image.uri fields must be set accordingly prior to calling * this function. @@ -93,7 +93,7 @@ class CESIUMGLTFWRITER_API GltfWriter { * * @details The first buffer object implicitly refers to the GLB binary chunk * and should not have a uri. Ignores internal data such as - * {@link CesiumGltf::BufferCesium} and {@link CesiumGltf::ImageCesium}. + * {@link CesiumGltf::BufferCesium} and {@link CesiumGltf::ImageAsset}. * * @param model The model. * @param bufferData The buffer data to store in the GLB binary chunk. diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/IPrepareRasterOverlayRendererResources.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/IPrepareRasterOverlayRendererResources.h index d9d88878f..7272fb16d 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/IPrepareRasterOverlayRendererResources.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/IPrepareRasterOverlayRendererResources.h @@ -5,7 +5,7 @@ #include namespace CesiumGltf { -struct ImageCesium; +struct ImageAsset; } namespace CesiumRasterOverlays { @@ -28,7 +28,7 @@ class CESIUMRASTEROVERLAYS_API IPrepareRasterOverlayRendererResources { * `pLoadThreadResult` parameter. */ virtual void* prepareRasterInLoadThread( - CesiumGltf::ImageCesium& image, + CesiumGltf::ImageAsset& image, const std::any& rendererOptions) = 0; /** diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h index f27c3adcc..48c2c13a5 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTile.h @@ -192,7 +192,7 @@ class RasterOverlayTile final * * @return The image data. */ - CesiumUtility::IntrusivePointer + CesiumUtility::IntrusivePointer getImage() const noexcept { return this->_pImage; } @@ -205,7 +205,7 @@ class RasterOverlayTile final * * @return The image data. */ - CesiumUtility::IntrusivePointer getImage() noexcept { + CesiumUtility::IntrusivePointer getImage() noexcept { return this->_pImage; } @@ -259,7 +259,7 @@ class RasterOverlayTile final CesiumGeometry::Rectangle _rectangle; std::vector _tileCredits; LoadState _state; - CesiumUtility::IntrusivePointer _pImage; + CesiumUtility::IntrusivePointer _pImage; void* _pRendererResources; MoreDetailAvailable _moreDetailAvailable; }; diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h index daf143391..db1113b5d 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h @@ -32,7 +32,7 @@ struct CESIUMRASTEROVERLAYS_API LoadedRasterOverlayImage { * This will be an empty optional if the loading failed. In this case, * the `errors` vector will contain the corresponding error messages. */ - CesiumUtility::IntrusivePointer pImage{nullptr}; + CesiumUtility::IntrusivePointer pImage{nullptr}; /** * @brief The projected rectangle defining the bounds of this image. diff --git a/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp b/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp index fb728490c..d3a9e96ef 100644 --- a/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp +++ b/CesiumRasterOverlays/src/DebugColorizeTilesRasterOverlay.cpp @@ -42,7 +42,7 @@ class DebugTileProvider : public RasterOverlayTileProvider { result.rectangle = overlayTile.getRectangle(); - ImageCesium& image = result.pImage.emplace(); + ImageAsset& image = result.pImage.emplace(); image.width = 1; image.height = 1; image.channels = 4; diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index 9e4c95333..1c4499757 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -381,7 +381,7 @@ QuadtreeRasterOverlayTileProvider::getQuadtreeTile( namespace { PixelRectangle computePixelRectangle( - const ImageCesium& image, + const ImageAsset& image, const Rectangle& totalRectangle, const Rectangle& partRectangle) { // Pixel coordinates are measured from the top left. @@ -417,9 +417,9 @@ PixelRectangle computePixelRectangle( // source image where the source subset rectangle overlaps the target // rectangle is copied to the target image. void blitImage( - ImageCesium& target, + ImageAsset& target, const Rectangle& targetRectangle, - const ImageCesium& source, + const ImageAsset& source, const Rectangle& sourceRectangle, const std::optional& sourceSubset) { const Rectangle sourceToCopy = sourceSubset.value_or(sourceRectangle); @@ -478,7 +478,7 @@ QuadtreeRasterOverlayTileProvider::loadTileImage( } } return LoadedRasterOverlayImage{ - new ImageCesium(), + new ImageAsset(), Rectangle(), {}, std::move(errors), @@ -681,7 +681,7 @@ QuadtreeRasterOverlayTileProvider::combineImages( result.moreDetailAvailable = false; result.errorList = std::move(errors); - ImageCesium& target = result.pImage.emplace(); + ImageAsset& target = result.pImage.emplace(); target.bytesPerChannel = measurements.bytesPerChannel; target.channels = measurements.channels; target.width = measurements.widthPixels; diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 591c2c0c5..381497083 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -167,7 +167,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( if (pResponse->data().empty()) { if (options.allowEmptyImages) { return LoadedRasterOverlayImage{ - new CesiumGltf::ImageCesium(), + new CesiumGltf::ImageAsset(), options.rectangle, std::move(options.credits), {}, @@ -214,7 +214,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( namespace { struct LoadResult { RasterOverlayTile::LoadState state = RasterOverlayTile::LoadState::Unloaded; - CesiumUtility::IntrusivePointer pImage = nullptr; + CesiumUtility::IntrusivePointer pImage = nullptr; CesiumGeometry::Rectangle rectangle = {}; std::vector credits = {}; void* pRendererResources = nullptr; @@ -268,7 +268,7 @@ static LoadResult createLoadResultFromLoadedImage( "Warnings while loading image for tile"); } - CesiumGltf::ImageCesium& image = *loadedImage.pImage; + CesiumGltf::ImageAsset& image = *loadedImage.pImage; const int32_t bytesPerPixel = image.channels * image.bytesPerChannel; const int64_t requiredBytes = @@ -351,7 +351,7 @@ CesiumAsync::Future RasterOverlayTileProvider::doLoad( pTile->setState(result.state); if (pTile->getImage() != nullptr) { - ImageCesium& imageCesium = *pTile->getImage(); + ImageAsset& 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, diff --git a/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp b/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp index d8da4114c..8b8868ba9 100644 --- a/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp +++ b/CesiumRasterOverlays/src/RasterizedPolygonsOverlay.cpp @@ -24,7 +24,7 @@ void rasterizePolygons( const std::vector& cartographicPolygons, bool invertSelection) { - CesiumGltf::ImageCesium& image = loaded.pImage.emplace(); + CesiumGltf::ImageAsset& image = loaded.pImage.emplace(); std::byte insideColor; std::byte outsideColor; diff --git a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp index 705f90fe8..1abd0189d 100644 --- a/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/test/TestQuadtreeRasterOverlayTileProvider.cpp @@ -172,7 +172,7 @@ TEST_CASE("QuadtreeRasterOverlayTileProvider getTile") { REQUIRE(pTile->getImage()); - const ImageCesium& image = *pTile->getImage(); + const ImageAsset& image = *pTile->getImage(); CHECK(image.width > 0); CHECK(image.height > 0); CHECK(image.pixelData.size() > 0); @@ -228,7 +228,7 @@ TEST_CASE("QuadtreeRasterOverlayTileProvider getTile") { REQUIRE(pTile->getImage()); - const ImageCesium& image = *pTile->getImage(); + const ImageAsset& image = *pTile->getImage(); CHECK(image.width > 0); CHECK(image.height > 0); CHECK(image.pixelData.size() > 0); diff --git a/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp b/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp index 6f3228285..0c9a802cc 100644 --- a/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp +++ b/CesiumRasterOverlays/test/TestTileMapServiceRasterOverlay.cpp @@ -74,7 +74,7 @@ TEST_CASE("TileMapServiceRasterOverlay") { REQUIRE(pTile->getImage()); - const ImageCesium& image = *pTile->getImage(); + const ImageAsset& image = *pTile->getImage(); CHECK(image.width > 0); CHECK(image.height > 0); } From 883e36f100c6a1487bf1103bafb937a66578705b Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Thu, 10 Oct 2024 15:44:42 -0400 Subject: [PATCH 51/81] Rename ImageAssetMipPosition --- CesiumGltf/include/CesiumGltf/Image.h | 2 +- CesiumGltf/include/CesiumGltf/ImageAsset.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index 9b4fb0d43..31ff9bc9d 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -12,7 +12,7 @@ struct CESIUMGLTF_API Image final : public ImageSpec { /** * @brief Holds properties that are specific to the glTF loader rather than * part of the glTF spec. When an image is loaded from a URL, multiple `Image` - * instances may all point to the same `ImageCesium` instance. + * instances may all point to the same `ImageAsset` instance. */ CesiumUtility::IntrusivePointer pCesium; }; diff --git a/CesiumGltf/include/CesiumGltf/ImageAsset.h b/CesiumGltf/include/CesiumGltf/ImageAsset.h index 3772500a6..f7614c437 100644 --- a/CesiumGltf/include/CesiumGltf/ImageAsset.h +++ b/CesiumGltf/include/CesiumGltf/ImageAsset.h @@ -14,7 +14,7 @@ namespace CesiumGltf { /** * @brief The byte range within a buffer where this mip exists. */ -struct CESIUMGLTF_API ImageCesiumMipPosition { +struct CESIUMGLTF_API ImageAssetMipPosition { /** * @brief The byte index where this mip begins. */ @@ -67,7 +67,7 @@ struct CESIUMGLTF_API ImageAsset final * biggest and etc. If this is empty, assume the entire buffer is a single * image, the mip map will need to be generated on the client in this case. */ - std::vector mipPositions; + std::vector mipPositions; /** * @brief The pixel data. From 38b48112701ca7f726add957ba1e22cfe33b1d61 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 11 Oct 2024 12:00:55 -0400 Subject: [PATCH 52/81] Pass factory to asset depot constructor --- .../src/TilesetContentManager.cpp | 12 ++++++-- .../include/CesiumAsync/SharedAssetDepot.h | 19 +++++++++---- .../include/CesiumGltfReader/GltfReader.h | 2 +- .../CesiumGltfReader/GltfSharedAssetSystem.h | 9 +++++- .../include/CesiumGltfReader/ImageDecoder.h | 21 ++++++++++++++ CesiumGltfReader/src/GltfReader.cpp | 28 ++----------------- .../src/GltfSharedAssetSystem.cpp | 12 +++++--- 7 files changed, 62 insertions(+), 41 deletions(-) diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 7595bae46..f797d4dab 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -666,7 +666,9 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault( + CesiumGltfReader::AssetSystemOptions{ + tilesetOptions.contentOptions.ktx2TranscodeTargets})), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +698,9 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault( + CesiumGltfReader::AssetSystemOptions{ + tilesetOptions.contentOptions.ktx2TranscodeTargets})), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +852,9 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault( + CesiumGltfReader::AssetSystemOptions{ + tilesetOptions.contentOptions.ktx2TranscodeTargets})), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 0c3932122..a162bb2ef 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -20,6 +20,12 @@ namespace CesiumAsync { template class SharedAsset; +template class AssetFactory { +public: + virtual CesiumUtility::IntrusivePointer + createFrom(const gsl::span& data) const = 0; +}; + /** * A depot for {@link SharedAsset} instances, which are potentially shared between multiple objects. * @tparam AssetType The type of asset stored in this depot. This should usually @@ -44,7 +50,8 @@ class CESIUMASYNC_API SharedAssetDepot */ int64_t staleAssetSizeLimit = 16 * 1024 * 1024; - SharedAssetDepot() = default; + SharedAssetDepot(std::unique_ptr> _factory) + : factory(std::move(_factory)) {} ~SharedAssetDepot() { // It's possible the assets will outlive the depot, if they're still in use. @@ -102,12 +109,10 @@ class CESIUMASYNC_API SharedAssetDepot * If the asset has already started loading in this depot but hasn't finished, * its future will be returned. */ - template CesiumAsync::SharedFuture> getOrFetch( const CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, - const Factory& factory, const std::string& uri, const std::vector& headers) { // We need to avoid: @@ -138,8 +143,7 @@ class CESIUMASYNC_API SharedAssetDepot CesiumAsync::Future> future = pAssetAccessor->get(asyncSystem, uri, headers) .thenInWorkerThread( - [factory = std::move(factory)]( - std::shared_ptr&& pRequest) + [this](std::shared_ptr&& pRequest) -> CesiumUtility::IntrusivePointer { const CesiumAsync::IAssetResponse* pResponse = pRequest->response(); @@ -147,7 +151,7 @@ class CESIUMASYNC_API SharedAssetDepot return nullptr; } - return factory.createFrom(pResponse->data()); + return this->factory->createFrom(pResponse->data()); }) // Do this in main thread since we're messing with the collections. .thenInMainThread( @@ -271,6 +275,9 @@ class CESIUMASYNC_API SharedAssetDepot // Mutex for modifying the deletionCandidates list. mutable std::mutex deletionCandidatesMutex; + // The factory used to create new AssetType instances. + std::unique_ptr> factory; + friend class SharedAsset; }; diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index f534ed6b9..26f39f8b5 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -115,7 +115,7 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * that might appear in this glTF. */ CesiumUtility::IntrusivePointer pSharedAssets = - GltfSharedAssetSystem::getDefault(); + nullptr; }; /** diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index 46c4fb4f0..ae26a023e 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -2,6 +2,7 @@ #include #include +#include namespace CesiumGltf { struct ImageAsset; @@ -9,6 +10,11 @@ struct ImageAsset; namespace CesiumGltfReader { +class AssetSystemOptions { +public: + CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; +}; + /** * @brief Contains assets that are potentially shared across multiple glTF * models. @@ -16,7 +22,8 @@ namespace CesiumGltfReader { class GltfSharedAssetSystem : public CesiumUtility::ReferenceCountedThreadSafe { public: - static CesiumUtility::IntrusivePointer getDefault(); + static CesiumUtility::IntrusivePointer + getDefault(const AssetSystemOptions& options); CesiumUtility::IntrusivePointer< CesiumAsync::SharedAssetDepot> diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h index b2d60d2df..0c5ae99f6 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -1,6 +1,7 @@ #include "CesiumGltf/ImageAsset.h" #include "CesiumGltfReader/Library.h" +#include #include #include @@ -66,4 +67,24 @@ class ImageDecoder { generateMipMaps(CesiumGltf::ImageAsset& image); }; +/** + * Used to construct an ImageAsset. + */ +struct ImageAssetFactory : CesiumAsync::AssetFactory { + ImageAssetFactory( + const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets_) + : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} + + CesiumUtility::IntrusivePointer + createFrom(const gsl::span& data) const override { + CesiumGltfReader::ImageReaderResult imageResult = + ImageDecoder::readImage(data, this->ktx2TranscodeTargets); + // TODO: report warnings and errors! + return imageResult.pImage; + } + +private: + const CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; +}; + } // namespace CesiumGltfReader diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 03e2b5017..fae0bfdfd 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -41,26 +41,6 @@ using namespace CesiumJsonReader; using namespace CesiumUtility; namespace { - -/** - * Used to construct an ImageAsset. - */ -struct ImageAssetFactory { - ImageAssetFactory(const Ktx2TranscodeTargets& ktx2TranscodeTargets_) - : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} - - CesiumUtility::IntrusivePointer - createFrom(const gsl::span& data) const { - ImageReaderResult imageResult = - ImageDecoder::readImage(data, this->ktx2TranscodeTargets); - // TODO: report warnings and errors! - return imageResult.pImage; - } - -private: - const Ktx2TranscodeTargets ktx2TranscodeTargets; -}; - #pragma pack(push, 1) struct GlbHeader { uint32_t magic; @@ -581,12 +561,8 @@ void CesiumGltfReader::GltfReader::postprocessGltf( .share(); } else { // We have a depot, this is easy! - return options.pSharedAssets->pImage->getOrFetch( - asyncSystem, - pAssetAccessor, - ImageAssetFactory(options.ktx2TranscodeTargets), - uri, - headers); + return options.pSharedAssets->pImage + ->getOrFetch(asyncSystem, pAssetAccessor, uri, headers); } }; diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index a5c803535..93a94dd9f 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -1,15 +1,19 @@ #include #include +#include +#include namespace CesiumGltfReader { namespace { -CesiumUtility::IntrusivePointer createDefault() { +CesiumUtility::IntrusivePointer +createDefault(const AssetSystemOptions& options) { CesiumUtility::IntrusivePointer p = new GltfSharedAssetSystem(); - p->pImage.emplace(); + p->pImage.emplace( + std::make_unique(options.ktx2TranscodeTargets)); return p; } @@ -17,9 +21,9 @@ CesiumUtility::IntrusivePointer createDefault() { } // namespace /*static*/ CesiumUtility::IntrusivePointer -GltfSharedAssetSystem::getDefault() { +GltfSharedAssetSystem::getDefault(const AssetSystemOptions& options) { static CesiumUtility::IntrusivePointer pDefault = - createDefault(); + createDefault(options); return pDefault; } From b220031fb017c7ded1bcbfd754cf5480d91f2cb9 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 11 Oct 2024 14:31:23 -0400 Subject: [PATCH 53/81] Doubly linked list for deletion candidates --- CesiumAsync/include/CesiumAsync/SharedAsset.h | 8 +++- .../include/CesiumAsync/SharedAssetDepot.h | 41 +++++++++++-------- .../include/CesiumUtility/DoublyLinkedList.h | 11 +++++ 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 39b03c3e0..2a82e605c 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -88,7 +89,8 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { void addReference() const /*noexcept*/ { const int32_t prevReferences = this->_referenceCount++; if (this->_pDepot && prevReferences <= 0) { - this->_pDepot->unmarkDeletionCandidate(static_cast(this)); + this->_pDepot->unmarkDeletionCandidate( + *const_cast*>(this)); } } @@ -105,7 +107,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { SharedAssetDepot* pDepot = this->_pDepot; if (pDepot) { // Let the depot manage this object's lifetime. - pDepot->markDeletionCandidate(static_cast(this)); + pDepot->markDeletionCandidate(*const_cast*>(this)); } else { // No depot, so destroy this object directly. delete static_cast(this); @@ -141,6 +143,8 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { SharedAssetDepot* _pDepot{nullptr}; std::string _uniqueAssetId{}; + CesiumUtility::DoublyLinkedListPointers> _deletionListPointers; + // The size of this asset when it was counted by the depot. This is stored so // that the exact same size can be subtracted later. int64_t _sizeInDepot{0}; diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index a162bb2ef..3054e0952 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -208,24 +209,23 @@ class CESIUMASYNC_API SharedAssetDepot * Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void markDeletionCandidate(const AssetType* pAsset) { + void markDeletionCandidate(SharedAsset& asset) { std::lock_guard lock(this->deletionCandidatesMutex); - AssetType* pMutableAsset = const_cast(pAsset); - pMutableAsset->_sizeInDepot = pMutableAsset->getSizeBytes(); - this->totalDeletionCandidateMemoryUsage += pMutableAsset->_sizeInDepot; + asset._sizeInDepot = static_cast(asset).getSizeBytes(); + this->totalDeletionCandidateMemoryUsage += asset._sizeInDepot; - this->deletionCandidates.push_back(pMutableAsset); + this->deletionCandidates.insertAtTail(asset); if (this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { std::lock_guard assetsLock(this->assetsMutex); // Delete the deletion candidates until we're below the limit. - while (!this->deletionCandidates.empty() && + while (this->deletionCandidates.size() > 0 && this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { - const AssetType* pOldAsset = this->deletionCandidates.front(); - this->deletionCandidates.pop_front(); + SharedAsset* pOldAsset = this->deletionCandidates.head(); + this->deletionCandidates.remove(*pOldAsset); this->totalDeletionCandidateMemoryUsage -= pOldAsset->_sizeInDepot; @@ -240,19 +240,21 @@ class CESIUMASYNC_API SharedAssetDepot * Unmarks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void unmarkDeletionCandidate(const AssetType* pAsset) { + void unmarkDeletionCandidate(SharedAsset& asset) { std::lock_guard lock(this->deletionCandidatesMutex); - auto it = std::find( - this->deletionCandidates.begin(), - this->deletionCandidates.end(), - pAsset); + // We should only be deleting this element if it's in the deletionCandidates + // list. + bool isFound = !asset._deletionListPointers.isOrphan() || + (this->deletionCandidates.size() == 1 && + this->deletionCandidates.head()->_uniqueAssetId == + asset.getUniqueAssetId()); - CESIUM_ASSERT(it != this->deletionCandidates.end()); + CESIUM_ASSERT(isFound); - if (it != this->deletionCandidates.end()) { - this->totalDeletionCandidateMemoryUsage -= (*it)->_sizeInDepot; - this->deletionCandidates.erase(it); + if (isFound) { + this->totalDeletionCandidateMemoryUsage -= asset._sizeInDepot; + this->deletionCandidates.remove(asset); } } @@ -268,7 +270,10 @@ class CESIUMASYNC_API SharedAssetDepot // List of assets that are being considered for deletion, in the order that // they were added. - std::list deletionCandidates; + CesiumUtility::DoublyLinkedList< + SharedAsset, + &SharedAsset::_deletionListPointers> + deletionCandidates; // The total amount of memory used by all assets in the deletionCandidates // list. std::atomic totalDeletionCandidateMemoryUsage; diff --git a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h index 4785b7947..8d52b3abc 100644 --- a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h +++ b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h @@ -34,6 +34,17 @@ template class DoublyLinkedListPointers final { return *this; } + /** + * @brief Is this item an orphan? + * + * Items are considered orphaned if their `pPrevious` and `pNext` pointers are + * both `nullptr`. This means that the item is either the only item in its + * list, or not in a list at all. + */ + bool isOrphan() const { + return this->pNext == nullptr && this->pPrevious == nullptr; + } + private: template < class TElement, From 083cb92db29fa844f5a50cd6c708497b703b03ae Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 25 Oct 2024 00:45:53 -0400 Subject: [PATCH 54/81] WIP refactoring. --- CesiumAsync/include/CesiumAsync/SharedAsset.h | 42 ++-- .../include/CesiumAsync/SharedAssetDepot.h | 208 +++++++++++------- CesiumAsync/src/SharedAssetDepot.cpp | 70 ++++++ .../include/CesiumGeometry/QuadtreeTileID.h | 6 +- CesiumGeometry/src/QuadtreeTileID.cpp | 15 ++ .../CesiumGltfReader/GltfSharedAssetSystem.h | 17 +- CesiumGltfReader/src/GltfReader.cpp | 5 +- .../src/GltfSharedAssetSystem.cpp | 6 +- .../include/CesiumUtility/DoublyLinkedList.h | 17 +- CesiumUtility/include/CesiumUtility/Hash.h | 9 + CesiumUtility/src/Hash.cpp | 81 +++++++ 11 files changed, 359 insertions(+), 117 deletions(-) create mode 100644 CesiumAsync/src/SharedAssetDepot.cpp create mode 100644 CesiumUtility/include/CesiumUtility/Hash.h create mode 100644 CesiumUtility/src/Hash.cpp diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 2a82e605c..4f7964030 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -9,6 +9,8 @@ namespace CesiumAsync { +template class SharedAssetDepot; + /** * @brief An asset that is potentially shared between multiple objects, such as * an image shared between multiple glTF models. This is intended to be the base @@ -52,10 +54,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { * in the asset depot. */ SharedAsset(const SharedAsset& rhs) - : ExtensibleObject(rhs), - _referenceCount(0), - _pDepot(nullptr), - _uniqueAssetId() {} + : ExtensibleObject(rhs), _referenceCount(0), _pDepot(nullptr) {} /** * After a move construction, the content of the asset is moved to the new @@ -64,8 +63,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { SharedAsset(SharedAsset&& rhs) : ExtensibleObject(std::move(rhs)), _referenceCount(0), - _pDepot(nullptr), - _uniqueAssetId() {} + _pDepot(nullptr) {} /** * Assignment does not affect the asset's relationship with the depot, but is @@ -89,8 +87,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { void addReference() const /*noexcept*/ { const int32_t prevReferences = this->_referenceCount++; if (this->_pDepot && prevReferences <= 0) { - this->_pDepot->unmarkDeletionCandidate( - *const_cast*>(this)); + this->_pDepot->unmarkDeletionCandidate(*static_cast(this)); } } @@ -104,10 +101,10 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { CESIUM_ASSERT(this->_referenceCount > 0); const int32_t references = --this->_referenceCount; if (references == 0) { - SharedAssetDepot* pDepot = this->_pDepot; + IDepotOwningAsset* pDepot = this->_pDepot; if (pDepot) { // Let the depot manage this object's lifetime. - pDepot->markDeletionCandidate(*const_cast*>(this)); + pDepot->markDeletionCandidate(*static_cast(this)); } else { // No depot, so destroy this object directly. delete static_cast(this); @@ -119,20 +116,13 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { * @brief Gets the shared asset depot that owns this asset, or nullptr if this * asset is independent of an asset depot. */ - const SharedAssetDepot* getDepot() const { return this->_pDepot; } + const IDepotOwningAsset* getDepot() const { return this->_pDepot; } /** * @brief Gets the shared asset depot that owns this asset, or nullptr if this * asset is independent of an asset depot. */ - SharedAssetDepot* getDepot() { return this->_pDepot; } - - /** - * @brief Gets the unique ID of this asset, if it {@link isShareable}. - * - * If this asset is not shareable, this method will return an empty string. - */ - const std::string& getUniqueAssetId() const { return this->_uniqueAssetId; } + IDepotOwningAsset* getDepot() { return this->_pDepot; } protected: SharedAsset() = default; @@ -140,17 +130,19 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { private: mutable std::atomic _referenceCount{0}; - SharedAssetDepot* _pDepot{nullptr}; - std::string _uniqueAssetId{}; + IDepotOwningAsset* _pDepot{nullptr}; - CesiumUtility::DoublyLinkedListPointers> _deletionListPointers; +protected: + mutable CesiumUtility::DoublyLinkedListPointers _deletionListPointers; +private: // The size of this asset when it was counted by the depot. This is stored so // that the exact same size can be subtracted later. - int64_t _sizeInDepot{0}; + mutable int64_t _sizeInDepot{0}; - // To allow the depot to modify _pDepot, _uniqueAssetId, and _sizeInDepot. - friend class SharedAssetDepot; + // To allow the depot to modify _pDepot and _sizeInDepot. + template + friend class SharedAssetDepot; }; } // namespace CesiumAsync diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 3054e0952..20f7786d3 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -8,7 +8,10 @@ #include #include +#include + #include +#include #include #include #include @@ -27,15 +30,63 @@ template class AssetFactory { createFrom(const gsl::span& data) const = 0; }; +struct NetworkAssetKey { + /** + * @brief The URL from which this network asset is downloaded. + */ + std::string url; + + /** + * @brief The HTTP headers used in requesting this asset. + */ + std::vector headers; + + bool operator==(const NetworkAssetKey& rhs) const noexcept; + + /** + * @brief Request this asset from the network using the provided asset + * accessor. + * + * @param asyncSystem The async system. + * @param pAssetAccessor The asset accessor. + * @return A future that resolves to the request once it is complete. + */ + Future> loadFromNetwork( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const; + + /** + * @brief Request this asset from the network using the provided asset + * accessor and return the downloaded bytes. + * + * @param asyncSystem The async system. + * @param pAssetAccessor The asset accessor. + * @return A future that resolves to the downloaded bytes if the request is + * successful, or a string describing the error if one occurred. + */ + Future, std::string>> + loadBytesFromNetwork( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const; +}; + +template class IDepotOwningAsset { +public: + virtual ~IDepotOwningAsset() {} + virtual void markDeletionCandidate(const AssetType& asset) = 0; + virtual void unmarkDeletionCandidate(const AssetType& asset) = 0; +}; + /** * A depot for {@link SharedAsset} instances, which are potentially shared between multiple objects. * @tparam AssetType The type of asset stored in this depot. This should usually * be derived from {@link SharedAsset}. */ -template +template class CESIUMASYNC_API SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< - SharedAssetDepot> { + SharedAssetDepot>, + public IDepotOwningAsset { public: /** * @brief The maximum total byte usage of assets that have been loaded but are @@ -51,19 +102,23 @@ class CESIUMASYNC_API SharedAssetDepot */ int64_t staleAssetSizeLimit = 16 * 1024 * 1024; - SharedAssetDepot(std::unique_ptr> _factory) - : factory(std::move(_factory)) {} + using FactorySignature = + CesiumAsync::Future>( + const AsyncSystem& asyncSystem, + const AssetKey& key); + + SharedAssetDepot(std::function factory) + : _factory(std::move(factory)) {} ~SharedAssetDepot() { // It's possible the assets will outlive the depot, if they're still in use. - std::lock_guard lock(this->assetsMutex); + std::lock_guard lock(this->_assetsMutex); - for (auto& pair : this->assets) { + for (auto& pair : this->_assets) { // Transfer ownership to the reference counting system. CesiumUtility::IntrusivePointer pCounted = pair.second.release(); pCounted->_pDepot = nullptr; - pCounted->_uniqueAssetId.clear(); } } @@ -72,12 +127,12 @@ class CESIUMASYNC_API SharedAssetDepot * or returns the existing asset if present. */ CesiumUtility::IntrusivePointer store( - const std::string& assetId, + const AssetKey& assetId, const CesiumUtility::IntrusivePointer& pAsset) { - std::lock_guard lock(this->assetsMutex); + std::lock_guard lock(this->_assetsMutex); - auto findIt = this->assets.find(assetId); - if (findIt != this->assets.end()) { + auto findIt = this->_assets.find(assetId); + if (findIt != this->_assets.end()) { // This asset ID already exists in the depot, so we can't add this asset. return findIt->second.get(); } @@ -85,16 +140,17 @@ class CESIUMASYNC_API SharedAssetDepot // If this asset ID is pending, but hasn't completed yet, where did this // asset come from? It shouldn't happen. CESIUM_ASSERT( - this->pendingAssets.find(assetId) == this->pendingAssets.end()); + this->_pendingAssets.find(assetId) == this->_pendingAssets.end()); pAsset->_pDepot = this; - pAsset->_uniqueAssetId = assetId; + this->_assetKeys[pAsset.get()] = assetId; // Now that this asset is owned by the depot, we exclusively // control its lifetime with a std::unique_ptr. std::unique_ptr pOwnedAsset(pAsset.get()); - auto [addIt, added] = this->assets.emplace(assetId, std::move(pOwnedAsset)); + auto [addIt, added] = + this->_assets.emplace(assetId, std::move(pOwnedAsset)); // We should always add successfully, because we verified it didn't already // exist. A failed emplace is disastrous because our unique_ptr will end up @@ -113,19 +169,17 @@ class CESIUMASYNC_API SharedAssetDepot CesiumAsync::SharedFuture> getOrFetch( const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const std::string& uri, - const std::vector& headers) { + const AssetKey& assetKey) { // We need to avoid: // - Two assets starting to load before the first asset has updated the // pendingAssets map // - An asset starting to load after the previous load has been removed from // the pendingAssets map, but before the completed asset has been added to // the assets map. - std::lock_guard lock(this->assetsMutex); + std::lock_guard lock(this->_assetsMutex); - auto existingIt = this->assets.find(uri); - if (existingIt != this->assets.end()) { + auto existingIt = this->_assets.find(assetKey); + if (existingIt != this->_assets.end()) { // We've already loaded an asset with this ID - we can just use that. return asyncSystem .createResolvedFuture(CesiumUtility::IntrusivePointer( @@ -133,8 +187,8 @@ class CESIUMASYNC_API SharedAssetDepot .share(); } - auto pendingIt = this->pendingAssets.find(uri); - if (pendingIt != this->pendingAssets.end()) { + auto pendingIt = this->_pendingAssets.find(assetKey); + if (pendingIt != this->_pendingAssets.end()) { // Return the existing future - the caller can chain off of it. return pendingIt->second; } @@ -142,35 +196,25 @@ class CESIUMASYNC_API SharedAssetDepot // We haven't loaded or started to load this asset yet. // Let's do that now. CesiumAsync::Future> future = - pAssetAccessor->get(asyncSystem, uri, headers) + this->_factory(asyncSystem, assetKey) .thenInWorkerThread( - [this](std::shared_ptr&& pRequest) - -> CesiumUtility::IntrusivePointer { - const CesiumAsync::IAssetResponse* pResponse = - pRequest->response(); - if (!pResponse) { - return nullptr; - } - - return this->factory->createFrom(pResponse->data()); - }) - // Do this in main thread since we're messing with the collections. - .thenInMainThread( - [uri, + [assetKey, this](CesiumUtility::IntrusivePointer&& pResult) -> CesiumUtility::IntrusivePointer { + std::lock_guard lock(this->_assetsMutex); + // Remove pending asset. - this->pendingAssets.erase(uri); + this->_pendingAssets.erase(assetKey); // Store the new asset in the depot. - return this->store(uri, pResult); + return this->store(assetKey, pResult); }); auto [it, added] = - this->pendingAssets.emplace(uri, std::move(future).share()); + this->_pendingAssets.emplace(assetKey, std::move(future).share()); // Should always be added successfully, because we checked above that the - // URI doesn't exist in the map yet. + // asset key doesn't exist in the map yet. CESIUM_ASSERT(added); return it->second; @@ -179,26 +223,26 @@ class CESIUMASYNC_API SharedAssetDepot /** * @brief Returns the total number of distinct assets contained in this depot. */ - size_t getDistinctCount() const { return this->assets.size(); } + size_t getDistinctCount() const { return this->_assets.size(); } /** * @brief Returns the number of active references to assets in this depot. */ size_t getUsageCount() const { size_t count = 0; - for (const auto& [key, item] : assets) { + for (const auto& [key, item] : _assets) { count += item->_referenceCount; } return count; } size_t getDeletionCandidateCount() const { - std::lock_guard lock(this->deletionCandidatesMutex); - return this->deletionCandidates.size(); + std::lock_guard lock(this->_deletionCandidatesMutex); + return this->_deletionCandidates.size(); } int64_t getDeletionCandidateTotalSizeBytes() const { - return this->totalDeletionCandidateMemoryUsage; + return this->_totalDeletionCandidateMemoryUsage; } private: @@ -209,29 +253,34 @@ class CESIUMASYNC_API SharedAssetDepot * Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void markDeletionCandidate(SharedAsset& asset) { - std::lock_guard lock(this->deletionCandidatesMutex); + void markDeletionCandidate(const AssetType& asset) { + std::lock_guard lock(this->_deletionCandidatesMutex); - asset._sizeInDepot = static_cast(asset).getSizeBytes(); - this->totalDeletionCandidateMemoryUsage += asset._sizeInDepot; + asset._sizeInDepot = asset.getSizeBytes(); + this->_totalDeletionCandidateMemoryUsage += asset._sizeInDepot; - this->deletionCandidates.insertAtTail(asset); + this->_deletionCandidates.insertAtTail(const_cast(asset)); - if (this->totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { - std::lock_guard assetsLock(this->assetsMutex); + if (this->_totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { + std::lock_guard assetsLock(this->_assetsMutex); // Delete the deletion candidates until we're below the limit. - while (this->deletionCandidates.size() > 0 && - this->totalDeletionCandidateMemoryUsage > + while (this->_deletionCandidates.size() > 0 && + this->_totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { - SharedAsset* pOldAsset = this->deletionCandidates.head(); - this->deletionCandidates.remove(*pOldAsset); + AssetType* pOldAsset = this->_deletionCandidates.head(); + this->_deletionCandidates.remove(const_cast(*pOldAsset)); - this->totalDeletionCandidateMemoryUsage -= pOldAsset->_sizeInDepot; + this->_totalDeletionCandidateMemoryUsage -= pOldAsset->_sizeInDepot; - // This will actually delete the asset. CESIUM_ASSERT(pOldAsset->_referenceCount == 0); - this->assets.erase(pOldAsset->getUniqueAssetId()); + + auto keyIt = this->_assetKeys.find(pOldAsset); + CESIUM_ASSERT(keyIt != this->_assetKeys.end()); + if (keyIt != this->_assetKeys.end()) { + // This will actually delete the asset. + this->_assets.erase(keyIt->second); + } } } } @@ -240,50 +289,57 @@ class CESIUMASYNC_API SharedAssetDepot * Unmarks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. */ - void unmarkDeletionCandidate(SharedAsset& asset) { - std::lock_guard lock(this->deletionCandidatesMutex); + void unmarkDeletionCandidate(const AssetType& asset) { + std::lock_guard lock(this->_deletionCandidatesMutex); // We should only be deleting this element if it's in the deletionCandidates // list. bool isFound = !asset._deletionListPointers.isOrphan() || - (this->deletionCandidates.size() == 1 && - this->deletionCandidates.head()->_uniqueAssetId == - asset.getUniqueAssetId()); + (this->_deletionCandidates.size() == 1 && + this->_deletionCandidates.head() == &asset); CESIUM_ASSERT(isFound); if (isFound) { - this->totalDeletionCandidateMemoryUsage -= asset._sizeInDepot; - this->deletionCandidates.remove(asset); + this->_totalDeletionCandidateMemoryUsage -= asset._sizeInDepot; + this->_deletionCandidates.remove(const_cast(asset)); } } // Assets that have a unique ID that can be used to de-duplicate them. - std::unordered_map> assets; + std::unordered_map> _assets; // Futures for assets that still aren't loaded yet. std::unordered_map< - std::string, + AssetKey, CesiumAsync::SharedFuture>> - pendingAssets; + _pendingAssets; + // Maps assets back to the key by which they are known in this depot. + std::unordered_map _assetKeys; // Mutex for checking or editing the pendingAssets map - std::mutex assetsMutex; + std::mutex _assetsMutex; // List of assets that are being considered for deletion, in the order that - // they were added. - CesiumUtility::DoublyLinkedList< + // they became unused. + CesiumUtility::DoublyLinkedListAdvanced< + AssetType, SharedAsset, &SharedAsset::_deletionListPointers> - deletionCandidates; + _deletionCandidates; // The total amount of memory used by all assets in the deletionCandidates // list. - std::atomic totalDeletionCandidateMemoryUsage; + std::atomic _totalDeletionCandidateMemoryUsage; // Mutex for modifying the deletionCandidates list. - mutable std::mutex deletionCandidatesMutex; + mutable std::mutex _deletionCandidatesMutex; // The factory used to create new AssetType instances. - std::unique_ptr> factory; + std::function _factory; friend class SharedAsset; }; } // namespace CesiumAsync + +template <> struct std::hash { + std::size_t + operator()(const CesiumAsync::NetworkAssetKey& key) const noexcept; +}; diff --git a/CesiumAsync/src/SharedAssetDepot.cpp b/CesiumAsync/src/SharedAssetDepot.cpp new file mode 100644 index 000000000..94398e0d4 --- /dev/null +++ b/CesiumAsync/src/SharedAssetDepot.cpp @@ -0,0 +1,70 @@ +#include +#include + +using namespace CesiumUtility; + +std::size_t std::hash::operator()( + const CesiumAsync::NetworkAssetKey& key) const noexcept { + std::hash hash{}; + + size_t result = hash(key.url); + + for (const CesiumAsync::IAssetAccessor::THeader& header : key.headers) { + result = Hash::combine(result, hash(header.first)); + result = Hash::combine(result, hash(header.second)); + } + + return result; +} + +namespace CesiumAsync { + +bool NetworkAssetKey::operator==(const NetworkAssetKey& rhs) const noexcept { + if (this->url != rhs.url || this->headers.size() != rhs.headers.size()) + return false; + + for (size_t i = 0; i < this->headers.size(); ++i) { + if (this->headers[i].first != rhs.headers[i].first || + this->headers[i].second != rhs.headers[i].second) + return false; + } + + return true; +} + +Future> +NetworkAssetKey::loadFromNetwork( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const { + return pAssetAccessor->get(asyncSystem, this->url, this->headers); +} + +Future, std::string>> +NetworkAssetKey::loadBytesFromNetwork( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const { + return this->loadFromNetwork(asyncSystem, pAssetAccessor) + .thenInWorkerThread( + [](std::shared_ptr&& pRequest) + -> nonstd::expected, std::string> { + const CesiumAsync::IAssetResponse* pResponse = pRequest->response(); + if (!pResponse) { + return nonstd::make_unexpected( + fmt::format("Request for {} failed.", pRequest->url())); + } + + uint16_t statusCode = pResponse->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + return nonstd::make_unexpected(fmt::format( + "Request for {} failed with code {}", + pRequest->url(), + pResponse->statusCode())); + } + + return std::vector( + pResponse->data().begin(), + pResponse->data().end()); + }); +} + +} // namespace CesiumAsync diff --git a/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h b/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h index 71cf1fa42..9d1ebad2d 100644 --- a/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h +++ b/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h @@ -111,10 +111,6 @@ template <> struct hash { * @brief A specialization of the `std::hash` template for * {@link CesiumGeometry::QuadtreeTileID} objects. */ - size_t operator()(const CesiumGeometry::QuadtreeTileID& key) const noexcept { - // TODO: is this hash function any good? Probably not. - std::hash h; - return h(key.level) ^ (h(key.x) << 1) ^ (h(key.y) << 2); - } + size_t operator()(const CesiumGeometry::QuadtreeTileID& key) const noexcept; }; } // namespace std diff --git a/CesiumGeometry/src/QuadtreeTileID.cpp b/CesiumGeometry/src/QuadtreeTileID.cpp index 908b86879..0058a37be 100644 --- a/CesiumGeometry/src/QuadtreeTileID.cpp +++ b/CesiumGeometry/src/QuadtreeTileID.cpp @@ -1,5 +1,7 @@ #include "CesiumGeometry/QuadtreeTilingScheme.h" +#include + namespace CesiumGeometry { uint32_t QuadtreeTileID::computeInvertedY( @@ -9,3 +11,16 @@ uint32_t QuadtreeTileID::computeInvertedY( } } // namespace CesiumGeometry + +namespace std { + +size_t hash::operator()( + const CesiumGeometry::QuadtreeTileID& key) const noexcept { + std::hash h; + size_t result = h(key.level); + result = CesiumUtility::Hash::combine(result, h(key.x)); + result = CesiumUtility::Hash::combine(result, h(key.y)); + return result; +} + +} // namespace std diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index ae26a023e..e831fd01c 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -13,8 +13,16 @@ namespace CesiumGltfReader { class AssetSystemOptions { public: CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; + std::shared_ptr pAssetAccessor; }; +struct NetworkImageAssetKey : public CesiumAsync::NetworkAssetKey { + CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets{}; +}; + +using NetworkImageAssetDepot = + CesiumAsync::SharedAssetDepot; + /** * @brief Contains assets that are potentially shared across multiple glTF * models. @@ -25,9 +33,12 @@ class GltfSharedAssetSystem static CesiumUtility::IntrusivePointer getDefault(const AssetSystemOptions& options); - CesiumUtility::IntrusivePointer< - CesiumAsync::SharedAssetDepot> - pImage; + CesiumUtility::IntrusivePointer pImage; }; } // namespace CesiumGltfReader + +template <> struct std::hash { + std::size_t + operator()(const CesiumGltfReader::NetworkImageAssetKey& key) const noexcept; +}; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index fae0bfdfd..3273f067b 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -561,8 +561,9 @@ void CesiumGltfReader::GltfReader::postprocessGltf( .share(); } else { // We have a depot, this is easy! - return options.pSharedAssets->pImage - ->getOrFetch(asyncSystem, pAssetAccessor, uri, headers); + return options.pSharedAssets->pImage->getOrFetch( + asyncSystem, + NetworkImageAssetKey{uri, headers}); } }; diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index 93a94dd9f..aa925377b 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -13,7 +13,11 @@ createDefault(const AssetSystemOptions& options) { new GltfSharedAssetSystem(); p->pImage.emplace( - std::make_unique(options.ktx2TranscodeTargets)); + std::function([pAccessor = options.pAssetAccessor]( + const CesiumAsync::AsyncSystem& asyncSystem, + const NetworkImageAssetKey& key) { + return key.loadBytesFromNetwork(asyncSystem, pAccessor); + })); return p; } diff --git a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h index 8d52b3abc..39a726dc2 100644 --- a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h +++ b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h @@ -47,9 +47,10 @@ template class DoublyLinkedListPointers final { private: template < - class TElement, - DoublyLinkedListPointers(TElement::*Pointers)> - friend class DoublyLinkedList; + typename TElement, + typename TElementBase, + DoublyLinkedListPointers(TElementBase::*Pointers)> + friend class DoublyLinkedListAdvanced; T* pNext; T* pPrevious; @@ -65,8 +66,11 @@ template class DoublyLinkedListPointers final { * @tparam (T::*Pointers) A member pointer to the field that holds the links to * the previous and next nodes. */ -template (T::*Pointers)> -class DoublyLinkedList final { +template < + typename T, + typename TPointerBase, + DoublyLinkedListPointers(TPointerBase::*Pointers)> +class DoublyLinkedListAdvanced final { public: /** * @brief Removes the given node from this list. @@ -255,4 +259,7 @@ class DoublyLinkedList final { T* _pTail = nullptr; }; +template (T::*Pointers)> +using DoublyLinkedList = DoublyLinkedListAdvanced; + } // namespace CesiumUtility diff --git a/CesiumUtility/include/CesiumUtility/Hash.h b/CesiumUtility/include/CesiumUtility/Hash.h new file mode 100644 index 000000000..5f817e330 --- /dev/null +++ b/CesiumUtility/include/CesiumUtility/Hash.h @@ -0,0 +1,9 @@ +#pragma once + +namespace CesiumUtility { + +struct Hash { + static size_t combine(size_t first, size_t second); +}; + +} diff --git a/CesiumUtility/src/Hash.cpp b/CesiumUtility/src/Hash.cpp new file mode 100644 index 000000000..71c5b1a13 --- /dev/null +++ b/CesiumUtility/src/Hash.cpp @@ -0,0 +1,81 @@ +#include + +#include + +namespace CesiumUtility { + +// This function is adapted from Boost v1.86.0, `hash_mix_impl<64>` function. +// +// Copyright 2022 Peter Dimov +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// hash_mix for 64 bit size_t +// +// The general "xmxmx" form of state of the art 64 bit mixers originates +// from Murmur3 by Austin Appleby, which uses the following function as +// its "final mix": +// +// k ^= k >> 33; +// k *= 0xff51afd7ed558ccd; +// k ^= k >> 33; +// k *= 0xc4ceb9fe1a85ec53; +// k ^= k >> 33; +// +// (https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp) +// +// It has subsequently been improved multiple times by different authors +// by changing the constants. The most well known improvement is the +// so-called "variant 13" function by David Stafford: +// +// k ^= k >> 30; +// k *= 0xbf58476d1ce4e5b9; +// k ^= k >> 27; +// k *= 0x94d049bb133111eb; +// k ^= k >> 31; +// +// (https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html) +// +// This mixing function is used in the splitmix64 RNG: +// http://xorshift.di.unimi.it/splitmix64.c +// +// We use Jon Maiga's implementation from +// http://jonkagstrom.com/mx3/mx3_rev2.html +// +// x ^= x >> 32; +// x *= 0xe9846af9b1a615d; +// x ^= x >> 32; +// x *= 0xe9846af9b1a615d; +// x ^= x >> 28; +// +// An equally good alternative is Pelle Evensen's Moremur: +// +// x ^= x >> 27; +// x *= 0x3C79AC492BA7B653; +// x ^= x >> 33; +// x *= 0x1C69B3F74AC4AE35; +// x ^= x >> 27; +// +// (https://mostlymangling.blogspot.com/2019/12/stronger-better-morer-moremur-better.html) +namespace { + +inline std::uint64_t mix(std::uint64_t x) { + std::uint64_t const m = 0xe9846af9b1a615d; + + x ^= x >> 32; + x *= m; + x ^= x >> 32; + x *= m; + x ^= x >> 28; + + return x; +} + +} // namespace + +// This function is adapted from Boost's `hash_combine`. +size_t Hash::combine(size_t first, size_t second) { + return mix(first + 0x9e3779b9 + second); +} + +} // namespace CesiumUtility From 727a68d064e38db44c6c91a2d6878b255405883c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 28 Oct 2024 17:13:18 +1100 Subject: [PATCH 55/81] Introduce AssetEntry. --- .../src/TilesetContentManager.cpp | 12 +- CesiumAsync/include/CesiumAsync/SharedAsset.h | 10 +- .../include/CesiumAsync/SharedAssetDepot.h | 332 +++++++++++------- CesiumAsync/src/SharedAssetDepot.cpp | 9 +- .../CesiumGltfReader/GltfSharedAssetSystem.h | 15 +- CesiumGltfReader/src/GltfReader.cpp | 93 ++--- .../src/GltfSharedAssetSystem.cpp | 65 +++- .../include/CesiumUtility/DoublyLinkedList.h | 28 +- .../include/CesiumUtility/ErrorList.h | 16 + CesiumUtility/include/CesiumUtility/Result.h | 78 ++++ CesiumUtility/src/ErrorList.cpp | 8 + 11 files changed, 451 insertions(+), 215 deletions(-) create mode 100644 CesiumUtility/include/CesiumUtility/Result.h diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index f797d4dab..7595bae46 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -666,9 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault( - CesiumGltfReader::AssetSystemOptions{ - tilesetOptions.contentOptions.ktx2TranscodeTargets})), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -698,9 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault( - CesiumGltfReader::AssetSystemOptions{ - tilesetOptions.contentOptions.ktx2TranscodeTargets})), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -852,9 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault( - CesiumGltfReader::AssetSystemOptions{ - tilesetOptions.contentOptions.ktx2TranscodeTargets})), + _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 4f7964030..876c99252 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -132,15 +132,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { mutable std::atomic _referenceCount{0}; IDepotOwningAsset* _pDepot{nullptr}; -protected: - mutable CesiumUtility::DoublyLinkedListPointers _deletionListPointers; - -private: - // The size of this asset when it was counted by the depot. This is stored so - // that the exact same size can be subtracted later. - mutable int64_t _sizeInDepot{0}; - - // To allow the depot to modify _pDepot and _sizeInDepot. + // To allow the depot to modify _pDepot. template friend class SharedAssetDepot; }; diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 20f7786d3..fcdb80988 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -7,8 +7,7 @@ #include #include #include - -#include +#include #include #include @@ -64,8 +63,7 @@ struct NetworkAssetKey { * @return A future that resolves to the downloaded bytes if the request is * successful, or a string describing the error if one occurred. */ - Future, std::string>> - loadBytesFromNetwork( + Future>> loadBytesFromNetwork( const CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor) const; }; @@ -102,73 +100,36 @@ class CESIUMASYNC_API SharedAssetDepot */ int64_t staleAssetSizeLimit = 16 * 1024 * 1024; - using FactorySignature = - CesiumAsync::Future>( - const AsyncSystem& asyncSystem, - const AssetKey& key); + using FactorySignature = CesiumAsync::Future< + CesiumUtility::Result>>( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const AssetKey& key); SharedAssetDepot(std::function factory) : _factory(std::move(factory)) {} ~SharedAssetDepot() { // It's possible the assets will outlive the depot, if they're still in use. - std::lock_guard lock(this->_assetsMutex); - for (auto& pair : this->_assets) { // Transfer ownership to the reference counting system. CesiumUtility::IntrusivePointer pCounted = - pair.second.release(); + pair.second->pAsset.release(); pCounted->_pDepot = nullptr; } } - /** - * Stores the AssetType in this depot and returns a reference to it, - * or returns the existing asset if present. - */ - CesiumUtility::IntrusivePointer store( - const AssetKey& assetId, - const CesiumUtility::IntrusivePointer& pAsset) { - std::lock_guard lock(this->_assetsMutex); - - auto findIt = this->_assets.find(assetId); - if (findIt != this->_assets.end()) { - // This asset ID already exists in the depot, so we can't add this asset. - return findIt->second.get(); - } - - // If this asset ID is pending, but hasn't completed yet, where did this - // asset come from? It shouldn't happen. - CESIUM_ASSERT( - this->_pendingAssets.find(assetId) == this->_pendingAssets.end()); - - pAsset->_pDepot = this; - this->_assetKeys[pAsset.get()] = assetId; - - // Now that this asset is owned by the depot, we exclusively - // control its lifetime with a std::unique_ptr. - std::unique_ptr pOwnedAsset(pAsset.get()); - - auto [addIt, added] = - this->_assets.emplace(assetId, std::move(pOwnedAsset)); - - // We should always add successfully, because we verified it didn't already - // exist. A failed emplace is disastrous because our unique_ptr will end up - // destroying the user's object, which may still be in use. - CESIUM_ASSERT(added); - - return addIt->second.get(); - } - /** * Fetches an asset that has a {@link AssetFactory} defined and constructs it if possible. * If the asset is already in this depot, it will be returned instead. * If the asset has already started loading in this depot but hasn't finished, * its future will be returned. */ - CesiumAsync::SharedFuture> - getOrFetch( - const CesiumAsync::AsyncSystem& asyncSystem, + SharedFuture< + CesiumUtility::Result>> + getOrCreate( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, const AssetKey& assetKey) { // We need to avoid: // - Two assets starting to load before the first asset has updated the @@ -176,48 +137,85 @@ class CESIUMASYNC_API SharedAssetDepot // - An asset starting to load after the previous load has been removed from // the pendingAssets map, but before the completed asset has been added to // the assets map. - std::lock_guard lock(this->_assetsMutex); + + // Calling the factory function while holding the mutex unnecessarily + // limits parallelism. It can even lead to a bug in the scenario where the + // `thenInWorkerThread` continuation is invoked immediately in the current + // thread, before `thenInWorkerThread` itself returns. That would result + // in an attempt to lock the mutex recursively, which is not allowed. + + // So we jump through some hoops here to publish "this thread is working + // on it", then unlock the mutex, and _then_ actually call the factory + // function. + Promise promise = asyncSystem.createPromise(); + + std::unique_lock lock(this->_mutex); auto existingIt = this->_assets.find(assetKey); if (existingIt != this->_assets.end()) { - // We've already loaded an asset with this ID - we can just use that. - return asyncSystem - .createResolvedFuture(CesiumUtility::IntrusivePointer( - existingIt->second.get())) - .share(); - } - - auto pendingIt = this->_pendingAssets.find(assetKey); - if (pendingIt != this->_pendingAssets.end()) { - // Return the existing future - the caller can chain off of it. - return pendingIt->second; + // We've already loaded (or are loading) an asset with this ID - we can + // just use that. + const AssetEntry& entry = *existingIt->second; + if (entry.maybePendingAsset) { + // Asset is currently loading. + return *entry.maybePendingAsset; + } else { + // Asset is already loaded (or failed to load). + return asyncSystem.createResolvedFuture(entry.toResult()).share(); + } } // We haven't loaded or started to load this asset yet. // Let's do that now. - CesiumAsync::Future> future = - this->_factory(asyncSystem, assetKey) + CesiumUtility::IntrusivePointer> + pDepot = this; + CesiumUtility::IntrusivePointer pEntry = + new AssetEntry(assetKey); + + auto future = + promise.getFuture() + .thenImmediately([pDepot, pEntry, asyncSystem, pAssetAccessor]() { + return pDepot->_factory(asyncSystem, pAssetAccessor, pEntry->key); + }) .thenInWorkerThread( - [assetKey, - this](CesiumUtility::IntrusivePointer&& pResult) - -> CesiumUtility::IntrusivePointer { - std::lock_guard lock(this->_assetsMutex); + [pDepot, + pEntry](CesiumUtility::Result< + CesiumUtility::IntrusivePointer>&& result) { + std::lock_guard lock(pDepot->_mutex); + + if (result.pValue) { + result.pValue->_pDepot = pDepot.get(); + pDepot->_assetsByPointer[result.pValue.get()] = + pEntry.get(); + } + + // Now that this asset is owned by the depot, we exclusively + // control its lifetime with a std::unique_ptr. + pEntry->pAsset = + std::unique_ptr(result.pValue.get()); + pEntry->errorsAndWarnings = std::move(result.errors); + pEntry->maybePendingAsset.reset(); + + return pEntry->toResult(); + }); - // Remove pending asset. - this->_pendingAssets.erase(assetKey); + SharedFuture< + CesiumUtility::Result>> + sharedFuture = std::move(future).share(); - // Store the new asset in the depot. - return this->store(assetKey, pResult); - }); + pEntry->maybePendingAsset = sharedFuture; - auto [it, added] = - this->_pendingAssets.emplace(assetKey, std::move(future).share()); + auto [it, added] = this->_assets.emplace(assetKey, pEntry); // Should always be added successfully, because we checked above that the // asset key doesn't exist in the map yet. CESIUM_ASSERT(added); - return it->second; + // Unlock the mutex and then call the factory function. + lock.unlock(); + promise.resolve(); + + return sharedFuture; } /** @@ -251,85 +249,173 @@ class CESIUMASYNC_API SharedAssetDepot /** * Marks the given asset as a candidate for deletion. - * Should only be called by {@link SharedAsset}. + * Should only be called by {@link SharedAsset}. May be called from any thread. */ - void markDeletionCandidate(const AssetType& asset) { - std::lock_guard lock(this->_deletionCandidatesMutex); + void markDeletionCandidate(const AssetType& asset) override { + std::lock_guard lock(this->_mutex); - asset._sizeInDepot = asset.getSizeBytes(); - this->_totalDeletionCandidateMemoryUsage += asset._sizeInDepot; + auto it = this->_assetsByPointer.find(const_cast(&asset)); + CESIUM_ASSERT(it != this->_assetsByPointer.end()); + if (it == this->_assetsByPointer.end()) { + return; + } - this->_deletionCandidates.insertAtTail(const_cast(asset)); + CESIUM_ASSERT(it->second != nullptr); - if (this->_totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { - std::lock_guard assetsLock(this->_assetsMutex); + AssetEntry& entry = *it->second; + entry.sizeInDeletionList = asset.getSizeBytes(); + this->_totalDeletionCandidateMemoryUsage += entry.sizeInDeletionList; + + this->_deletionCandidates.insertAtTail(entry); + if (this->_totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { // Delete the deletion candidates until we're below the limit. while (this->_deletionCandidates.size() > 0 && this->_totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { - AssetType* pOldAsset = this->_deletionCandidates.head(); - this->_deletionCandidates.remove(const_cast(*pOldAsset)); + AssetEntry* pOldEntry = this->_deletionCandidates.head(); + this->_deletionCandidates.remove(*pOldEntry); - this->_totalDeletionCandidateMemoryUsage -= pOldAsset->_sizeInDepot; + this->_totalDeletionCandidateMemoryUsage -= + pOldEntry->sizeInDeletionList; - CESIUM_ASSERT(pOldAsset->_referenceCount == 0); + CESIUM_ASSERT( + pOldEntry->pAsset == nullptr || + pOldEntry->pAsset->_referenceCount == 0); - auto keyIt = this->_assetKeys.find(pOldAsset); - CESIUM_ASSERT(keyIt != this->_assetKeys.end()); - if (keyIt != this->_assetKeys.end()) { - // This will actually delete the asset. - this->_assets.erase(keyIt->second); + if (pOldEntry->pAsset) { + this->_assetsByPointer.erase(pOldEntry->pAsset.get()); } + + // This will actually delete the asset. + this->_assets.erase(pOldEntry->key); } } } /** * Unmarks the given asset as a candidate for deletion. - * Should only be called by {@link SharedAsset}. + * Should only be called by {@link SharedAsset}. May be called from any thread. */ - void unmarkDeletionCandidate(const AssetType& asset) { - std::lock_guard lock(this->_deletionCandidatesMutex); + void unmarkDeletionCandidate(const AssetType& asset) override { + std::lock_guard lock(this->_mutex); + + auto it = this->_assetsByPointer.find(const_cast(&asset)); + CESIUM_ASSERT(it != this->_assetsByPointer.end()); + if (it == this->_assetsByPointer.end()) { + return; + } - // We should only be deleting this element if it's in the deletionCandidates - // list. - bool isFound = !asset._deletionListPointers.isOrphan() || - (this->_deletionCandidates.size() == 1 && - this->_deletionCandidates.head() == &asset); + CESIUM_ASSERT(it->second != nullptr); + + AssetEntry& entry = *it->second; + bool isFound = this->_deletionCandidates.contains(entry); CESIUM_ASSERT(isFound); if (isFound) { - this->_totalDeletionCandidateMemoryUsage -= asset._sizeInDepot; - this->_deletionCandidates.remove(const_cast(asset)); + this->_totalDeletionCandidateMemoryUsage -= entry.sizeInDeletionList; + this->_deletionCandidates.remove(entry); } } - // Assets that have a unique ID that can be used to de-duplicate them. - std::unordered_map> _assets; - // Futures for assets that still aren't loaded yet. - std::unordered_map< - AssetKey, - CesiumAsync::SharedFuture>> - _pendingAssets; - // Maps assets back to the key by which they are known in this depot. - std::unordered_map _assetKeys; - // Mutex for checking or editing the pendingAssets map - std::mutex _assetsMutex; + /** + * @brief An entry for an asset owned by this depot. This is reference counted + * so that we can keep it alive during async operations. + */ + struct AssetEntry + : public CesiumUtility::ReferenceCountedThreadSafe { + AssetEntry(AssetKey&& key_) + : CesiumUtility::ReferenceCountedThreadSafe(), + key(std::move(key_)), + pAsset(), + maybePendingAsset(), + errorsAndWarnings(), + sizeInDeletionList(0), + deletionListPointers() {} + + AssetEntry(const AssetKey& key_) : AssetEntry(AssetKey(key_)) {} + + /** + * @brief The unique key identifying this asset. + */ + AssetKey key; + + /** + * @brief A pointer to the asset. This may be nullptr if the asset is still + * being loaded, or if it failed to load. + */ + std::unique_ptr pAsset; + + /** + * @brief If this asset is currently loading, this field holds a shared + * future that will resolve when the asset load is complete. This field will + * be empty if the asset finished loading, including if it failed to load. + */ + std::optional>>> + maybePendingAsset; + + /** + * @brief The errors and warnings that occurred while loading this asset. + * This will not contain any errors or warnings if the asset has not + * finished loading yet. + */ + CesiumUtility::ErrorList errorsAndWarnings; + + /** + * @brief The size of this asset when it was added to the + * _deletionCandidates list. This is stored so that the exact same size can + * be subtracted later. The value of this field is undefined if the asset is + * not currently in the _deletionCandidates list. + */ + int64_t sizeInDeletionList; + + /** + * @brief The next and previous pointers to entries in the + * _deletionCandidates list. + */ + CesiumUtility::DoublyLinkedListPointers deletionListPointers; + + CesiumUtility::Result> + toResult() const { + return CesiumUtility::Result>( + pAsset.get(), + errorsAndWarnings); + } + + SharedFuture< + CesiumUtility::Result>> + toFuture(const AsyncSystem& asyncSystem) const { + if (this->maybePendingAsset) { + return *this->maybePendingAsset; + } else { + return asyncSystem.createResolvedFuture(this->toResult()).share(); + } + } + }; + + // Maps asset keys to AssetEntry instances. This collection owns the asset + // entries. + std::unordered_map> + _assets; + + // Maps asset pointers to AssetEntry instances. The values in this map refer + // to instances owned by the _assets map. + std::unordered_map _assetsByPointer; // List of assets that are being considered for deletion, in the order that // they became unused. - CesiumUtility::DoublyLinkedListAdvanced< - AssetType, - SharedAsset, - &SharedAsset::_deletionListPointers> + CesiumUtility::DoublyLinkedList _deletionCandidates; - // The total amount of memory used by all assets in the deletionCandidates + + // The total amount of memory used by all assets in the _deletionCandidates // list. std::atomic _totalDeletionCandidateMemoryUsage; - // Mutex for modifying the deletionCandidates list. - mutable std::mutex _deletionCandidatesMutex; + + // Mutex serializing access to _assets, _assetsByPointer, _deletionCandidates, + // and any AssetEntry owned by this depot. + std::mutex _mutex; // The factory used to create new AssetType instances. std::function _factory; diff --git a/CesiumAsync/src/SharedAssetDepot.cpp b/CesiumAsync/src/SharedAssetDepot.cpp index 94398e0d4..4cdeff3eb 100644 --- a/CesiumAsync/src/SharedAssetDepot.cpp +++ b/CesiumAsync/src/SharedAssetDepot.cpp @@ -39,23 +39,22 @@ NetworkAssetKey::loadFromNetwork( return pAssetAccessor->get(asyncSystem, this->url, this->headers); } -Future, std::string>> -NetworkAssetKey::loadBytesFromNetwork( +Future>> NetworkAssetKey::loadBytesFromNetwork( const CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor) const { return this->loadFromNetwork(asyncSystem, pAssetAccessor) .thenInWorkerThread( [](std::shared_ptr&& pRequest) - -> nonstd::expected, std::string> { + -> Result> { const CesiumAsync::IAssetResponse* pResponse = pRequest->response(); if (!pResponse) { - return nonstd::make_unexpected( + return ErrorList::error( fmt::format("Request for {} failed.", pRequest->url())); } uint16_t statusCode = pResponse->statusCode(); if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - return nonstd::make_unexpected(fmt::format( + return ErrorList::error(fmt::format( "Request for {} failed with code {}", pRequest->url(), pResponse->statusCode())); diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index e831fd01c..9d92566fb 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -10,14 +10,14 @@ struct ImageAsset; namespace CesiumGltfReader { -class AssetSystemOptions { -public: - CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; - std::shared_ptr pAssetAccessor; -}; - struct NetworkImageAssetKey : public CesiumAsync::NetworkAssetKey { CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets{}; + + CesiumAsync::Future>> + load( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const; }; using NetworkImageAssetDepot = @@ -30,8 +30,7 @@ using NetworkImageAssetDepot = class GltfSharedAssetSystem : public CesiumUtility::ReferenceCountedThreadSafe { public: - static CesiumUtility::IntrusivePointer - getDefault(const AssetSystemOptions& options); + static CesiumUtility::IntrusivePointer getDefault(); CesiumUtility::IntrusivePointer pImage; }; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 3273f067b..9de379d18 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -490,6 +490,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( struct ExternalBufferLoadResult { bool success = false; std::string bufferUri; + ErrorList warningsAndErrors; }; std::vector> resolvedBuffers; @@ -534,54 +535,47 @@ void CesiumGltfReader::GltfReader::postprocessGltf( const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, const std::string& uri, - const std::vector& headers) { - if (options.pSharedAssets == nullptr || - options.pSharedAssets->pImage == nullptr) { - // We don't have a depot, we have to fetch this the old way. - return pAssetAccessor->get(asyncSystem, uri, headers) - .thenInWorkerThread( - [uri, - options](std::shared_ptr&& pRequest) { - const IAssetResponse* pResponse = - pRequest->response(); - - CesiumUtility::IntrusivePointer pAsset = - nullptr; - - if (pResponse) { - gsl::span bytes = - pResponse->data(); - pAsset = - ImageAssetFactory(options.ktx2TranscodeTargets) - .createFrom(bytes); - } - - return pAsset; - }) - .share(); - } else { - // We have a depot, this is easy! - return options.pSharedAssets->pImage->getOrFetch( - asyncSystem, - NetworkImageAssetKey{uri, headers}); - } - }; + const std::vector& headers) + -> SharedFuture>> { + NetworkImageAssetKey assetKey{ + uri, + headers, + options.ktx2TranscodeTargets}; + + if (options.pSharedAssets == nullptr || + options.pSharedAssets->pImage == nullptr) { + // We don't have a depot, so fetch this asset directly. + return assetKey.load(asyncSystem, pAssetAccessor).share(); + } else { + // We have a depot, so fetch this asset via that depot. + return options.pSharedAssets->pImage->getOrCreate( + asyncSystem, + pAssetAccessor, + assetKey); + } + }; - SharedFuture> future = + SharedFuture>> future = getAsset(asyncSystem, pAssetAccessor, uri, tHeaders); resolvedBuffers.push_back(future.thenInWorkerThread( - [pImage = - &image](const IntrusivePointer& pLoadedImage) { + [pImage = &image]( + const Result>& loadedImage) { std::string imageUri = *pImage->uri; pImage->uri = std::nullopt; - if (pLoadedImage) { - pImage->pCesium = pLoadedImage; - return ExternalBufferLoadResult{true, imageUri}; + if (loadedImage.pValue) { + pImage->pCesium = loadedImage.pValue; + return ExternalBufferLoadResult{ + true, + imageUri, + loadedImage.errors}; } - return ExternalBufferLoadResult{false, imageUri}; + return ExternalBufferLoadResult{ + false, + imageUri, + loadedImage.errors}; })); } } @@ -594,10 +588,29 @@ void CesiumGltfReader::GltfReader::postprocessGltf( for (auto& bufferResult : loadResults) { if (!bufferResult.success) { pResult->warnings.push_back( - "Could not load the external gltf buffer: " + + "Could not load the external glTF buffer: " + bufferResult.bufferUri); } + + if (!bufferResult.warningsAndErrors.errors.empty()) { + pResult->warnings.emplace_back(fmt::format( + "Errors while loading external glTF buffer: {}\n- {}", + bufferResult.bufferUri, + CesiumUtility::joinToString( + bufferResult.warningsAndErrors.errors, + "\n- "))); + } + + if (!bufferResult.warningsAndErrors.warnings.empty()) { + pResult->warnings.emplace_back(fmt::format( + "Warnings while loading external glTF buffer: {}\n- {}", + bufferResult.bufferUri, + CesiumUtility::joinToString( + bufferResult.warningsAndErrors.warnings, + "\n- "))); + } } + return std::move(*pResult.release()); }); } diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index aa925377b..b1ef39016 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -2,21 +2,42 @@ #include #include #include +#include -namespace CesiumGltfReader { +using namespace CesiumAsync; +using namespace CesiumGltf; +using namespace CesiumGltfReader; +using namespace CesiumUtility; + +std::size_t std::hash::operator()( + const NetworkImageAssetKey& key) const noexcept { + std::hash baseHash{}; + std::hash ktxHash{}; + + size_t result = baseHash(key); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_R)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RG)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RGB)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RGBA)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_R)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RG)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RGB)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RGBA)); + return result; +} namespace { -CesiumUtility::IntrusivePointer -createDefault(const AssetSystemOptions& options) { +CesiumUtility::IntrusivePointer createDefault() { CesiumUtility::IntrusivePointer p = new GltfSharedAssetSystem(); - p->pImage.emplace( - std::function([pAccessor = options.pAssetAccessor]( - const CesiumAsync::AsyncSystem& asyncSystem, - const NetworkImageAssetKey& key) { - return key.loadBytesFromNetwork(asyncSystem, pAccessor); + p->pImage.emplace(std::function( + [](const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const NetworkImageAssetKey& key) + -> Future>> { + return key.load(asyncSystem, pAssetAccessor); })); return p; @@ -24,10 +45,34 @@ createDefault(const AssetSystemOptions& options) { } // namespace +namespace CesiumGltfReader { + +Future>> NetworkImageAssetKey::load( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const { + return this->loadBytesFromNetwork(asyncSystem, pAssetAccessor) + .thenInWorkerThread([ktx2TranscodeTargets = this->ktx2TranscodeTargets]( + Result>&& result) { + if (!result.value) { + return Result>(result.errors); + } + + ImageReaderResult imageResult = + ImageDecoder::readImage(*result.value, ktx2TranscodeTargets); + + result.errors.merge( + ErrorList{imageResult.errors, imageResult.warnings}); + + return Result>( + imageResult.pImage, + std::move(result.errors)); + }); +} + /*static*/ CesiumUtility::IntrusivePointer -GltfSharedAssetSystem::getDefault(const AssetSystemOptions& options) { +GltfSharedAssetSystem::getDefault() { static CesiumUtility::IntrusivePointer pDefault = - createDefault(options); + createDefault(); return pDefault; } diff --git a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h index 39a726dc2..af6715e68 100644 --- a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h +++ b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h @@ -34,17 +34,6 @@ template class DoublyLinkedListPointers final { return *this; } - /** - * @brief Is this item an orphan? - * - * Items are considered orphaned if their `pPrevious` and `pNext` pointers are - * both `nullptr`. This means that the item is either the only item in its - * list, or not in a list at all. - */ - bool isOrphan() const { - return this->pNext == nullptr && this->pPrevious == nullptr; - } - private: template < typename TElement, @@ -253,6 +242,23 @@ class DoublyLinkedListAdvanced final { return pNode ? this->previous(*pNode) : this->_pTail; } + /** + * @brief Determines if this list contains a given node in constant time. In + * order to avoid a full list scan, this method assumes that if the node has + * any next or previous node, then it is contained, then it is contained in + * this list. Do not use this method to determine which of multiple lists + * contain this node. + * + * @param node The node to check. + * @return True if this node is the head of the list, or if the node has next + * or previous nodes. False if the node does not have next or previous nodes + * and it is not the head of this list. + */ + bool contains(const T& node) const { + return this->next(node) != nullptr || this->previous(node) != nullptr || + this->_pHead == &node; + } + private: size_t _size = 0; T* _pHead = nullptr; diff --git a/CesiumUtility/include/CesiumUtility/ErrorList.h b/CesiumUtility/include/CesiumUtility/ErrorList.h index 3626368e6..f544073d1 100644 --- a/CesiumUtility/include/CesiumUtility/ErrorList.h +++ b/CesiumUtility/include/CesiumUtility/ErrorList.h @@ -15,6 +15,22 @@ namespace CesiumUtility { * or glTF content */ struct CESIUMUTILITY_API ErrorList { + /** + * @brief Creates an {@link ErrorList} containing a single error. + * + * @param errorMessage The error message. + * @return The new list containing the single error. + */ + static ErrorList error(std::string errorMessage); + + /** + * @brief Creates an {@link ErrorList} containing a single warning. + * + * @param warningMessage The warning message. + * @return The new list containing the single warning. + */ + static ErrorList warning(std::string warningMessage); + /** * @brief Merge the errors and warnings from other ErrorList together * diff --git a/CesiumUtility/include/CesiumUtility/Result.h b/CesiumUtility/include/CesiumUtility/Result.h new file mode 100644 index 000000000..1ba5e1671 --- /dev/null +++ b/CesiumUtility/include/CesiumUtility/Result.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include + +namespace CesiumUtility { + +/** + * @brief Holds the result of an operation. If the operation succeeds, it will + * provide a value. It may also provide errors and warnings. + * + * @tparam T The type of value included in the result. + */ +template struct Result { + Result(T value_) noexcept : value(std::move(value_)), errors() {} + + Result(T value_, ErrorList errors_) noexcept + : value(std::move(value_)), errors(std::move(errors_)) {} + + Result(ErrorList errors_) noexcept : value(), errors(std::move(errors_)) {} + + /** + * @brief The value, if the operation succeeded to the point where it can + * provide one. + * + * If a value is not provided because the operation failed, then there should + * be at least one error in {@link errors} indicating what went wrong. + */ + std::optional value; + + /** + * @brief The errors and warnings that occurred during the operation. + * + * If a {@link value} is provided, there should not be any errors in this + * list, but there may be warnings. If a value is not provided, there should + * be at least one error in this list. + */ + ErrorList errors; +}; + +/** + * @brief Holds the result of an operation. If the operation succeeds, it will + * provide a value. It may also provide errors and warnings. + * + * @tparam T The type of value included in the result. + */ +template struct Result> { + Result(CesiumUtility::IntrusivePointer pValue_) noexcept + : pValue(std::move(pValue_)), errors() {} + + Result(CesiumUtility::IntrusivePointer pValue_, ErrorList errors_) noexcept + : pValue(std::move(pValue_)), errors(std::move(errors_)) {} + + Result(ErrorList errors_) noexcept + : pValue(nullptr), errors(std::move(errors_)) {} + + /** + * @brief The value, if the operation succeeded to the point where it can + * provide one. + * + * If a value is not provided because the operation failed, then there should + * be at least one error in {@link errors} indicating what went wrong. + */ + CesiumUtility::IntrusivePointer pValue; + + /** + * @brief The errors and warnings that occurred during the operation. + * + * If a {@link pValue} is provided, there should not be any errors in this + * list, but there may be warnings. If a pValue is not provided, there should + * be at least one error in this list. + */ + ErrorList errors; +}; + +} // namespace CesiumUtility diff --git a/CesiumUtility/src/ErrorList.cpp b/CesiumUtility/src/ErrorList.cpp index 4e157c1fd..b1f3a1257 100644 --- a/CesiumUtility/src/ErrorList.cpp +++ b/CesiumUtility/src/ErrorList.cpp @@ -2,6 +2,14 @@ namespace CesiumUtility { +/*static*/ ErrorList ErrorList::error(std::string errorMessage) { + return ErrorList{{std::move(errorMessage)}, {}}; +} + +/*static*/ ErrorList ErrorList::warning(std::string warningMessage) { + return ErrorList{{}, {std::move(warningMessage)}}; +} + void ErrorList::merge(const ErrorList& errorList) { errors.insert(errors.end(), errorList.errors.begin(), errorList.errors.end()); warnings.insert( From c75831ff2471a4cf49d452e1a8bf0135d894860e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 28 Oct 2024 18:56:15 +1100 Subject: [PATCH 56/81] Fix some problems that only showed up in Unreal. --- CesiumAsync/include/CesiumAsync/SharedAssetDepot.h | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index fcdb80988..6efcd34c1 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -228,18 +228,21 @@ class CESIUMASYNC_API SharedAssetDepot */ size_t getUsageCount() const { size_t count = 0; - for (const auto& [key, item] : _assets) { - count += item->_referenceCount; + for (const auto& [key, pEntry] : _assets) { + if (pEntry->pAsset) { + count += pEntry->pAsset->_referenceCount; + } } return count; } size_t getDeletionCandidateCount() const { - std::lock_guard lock(this->_deletionCandidatesMutex); + std::lock_guard lock(this->_mutex); return this->_deletionCandidates.size(); } int64_t getDeletionCandidateTotalSizeBytes() const { + std::lock_guard lock(this->_mutex); return this->_totalDeletionCandidateMemoryUsage; } @@ -411,11 +414,11 @@ class CESIUMASYNC_API SharedAssetDepot // The total amount of memory used by all assets in the _deletionCandidates // list. - std::atomic _totalDeletionCandidateMemoryUsage; + int64_t _totalDeletionCandidateMemoryUsage; // Mutex serializing access to _assets, _assetsByPointer, _deletionCandidates, // and any AssetEntry owned by this depot. - std::mutex _mutex; + mutable std::mutex _mutex; // The factory used to create new AssetType instances. std::function _factory; From c5533b7eada631c70ef045694b6253d787341fff Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 28 Oct 2024 20:55:29 +1100 Subject: [PATCH 57/81] Cleanup. --- .../include/CesiumAsync/IAssetAccessor.h | 3 +- .../include/CesiumAsync/IDepotOwningAsset.h | 20 +++ .../CesiumAsync/NetworkAssetDescriptor.h | 67 +++++++ CesiumAsync/include/CesiumAsync/SharedAsset.h | 66 +++---- .../include/CesiumAsync/SharedAssetDepot.h | 165 +++++++----------- ...etDepot.cpp => NetworkAssetDescriptor.cpp} | 15 +- CesiumAsync/test/MockAssetAccessor.h | 1 + CesiumGltf/include/CesiumGltf/Image.h | 3 +- .../CesiumGltfReader/GltfSharedAssetSystem.h | 30 +--- .../include/CesiumGltfReader/ImageDecoder.h | 21 --- .../NetworkImageAssetDescriptor.h | 57 ++++++ CesiumGltfReader/src/GltfReader.cpp | 3 +- .../src/GltfSharedAssetSystem.cpp | 45 +---- .../src/NetworkImageAssetDescriptor.cpp | 74 ++++++++ .../CesiumNativeTests/SimpleAssetAccessor.h | 1 + CesiumNativeTests/src/FileAccessor.cpp | 1 + .../CesiumRasterOverlays/RasterOverlay.h | 1 + 17 files changed, 334 insertions(+), 239 deletions(-) create mode 100644 CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h create mode 100644 CesiumAsync/include/CesiumAsync/NetworkAssetDescriptor.h rename CesiumAsync/src/{SharedAssetDepot.cpp => NetworkAssetDescriptor.cpp} (81%) create mode 100644 CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h create mode 100644 CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp diff --git a/CesiumAsync/include/CesiumAsync/IAssetAccessor.h b/CesiumAsync/include/CesiumAsync/IAssetAccessor.h index 415e9e63c..695c395a9 100644 --- a/CesiumAsync/include/CesiumAsync/IAssetAccessor.h +++ b/CesiumAsync/include/CesiumAsync/IAssetAccessor.h @@ -1,6 +1,6 @@ #pragma once -#include "AsyncSystem.h" +#include "Future.h" #include "IAssetRequest.h" #include "Library.h" @@ -12,6 +12,7 @@ #include namespace CesiumAsync { + class AsyncSystem; /** diff --git a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h b/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h new file mode 100644 index 000000000..a912689f9 --- /dev/null +++ b/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h @@ -0,0 +1,20 @@ +#pragma once + +namespace CesiumAsync { + +/** + * @brief An interface representing the depot that owns a {@link SharedAsset}. + * + * {@link SharedAsset} has a pointer to the asset depot that owns it using this + * interface, rather than a complete {@link SharedAssetDepot}, in order to + * "erase" the type of the asset key. This allows SharedAsset to be templatized + * only on the asset type, not on the asset key type. + */ +template class IDepotOwningAsset { +public: + virtual ~IDepotOwningAsset() {} + virtual void markDeletionCandidate(const TAssetType& asset) = 0; + virtual void unmarkDeletionCandidate(const TAssetType& asset) = 0; +}; + +} diff --git a/CesiumAsync/include/CesiumAsync/NetworkAssetDescriptor.h b/CesiumAsync/include/CesiumAsync/NetworkAssetDescriptor.h new file mode 100644 index 000000000..de2c71778 --- /dev/null +++ b/CesiumAsync/include/CesiumAsync/NetworkAssetDescriptor.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace CesiumAsync { + +class AsyncSystem; + +/** + * @brief A description of an asset that can be loaded from the network using an + * {@link IAssetAccessor}. This includes a URL and any headers to be included + * in the request. + */ +struct NetworkAssetDescriptor { + /** + * @brief The URL from which this network asset is downloaded. + */ + std::string url; + + /** + * @brief The HTTP headers used in requesting this asset. + */ + std::vector headers; + + /** + * @brief Determines if this descriptor is identical to another one. + */ + bool operator==(const NetworkAssetDescriptor& rhs) const noexcept; + + /** + * @brief Request this asset from the network using the provided asset + * accessor. + * + * @param asyncSystem The async system. + * @param pAssetAccessor The asset accessor. + * @return A future that resolves to the request once it is complete. + */ + Future> loadFromNetwork( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const; + + /** + * @brief Request this asset from the network using the provided asset + * accessor and return the downloaded bytes. + * + * @param asyncSystem The async system. + * @param pAssetAccessor The asset accessor. + * @return A future that resolves to the downloaded bytes once the request is + * complete. + */ + Future>> loadBytesFromNetwork( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const; +}; + +} // namespace CesiumAsync + +template <> struct std::hash { + std::size_t + operator()(const CesiumAsync::NetworkAssetDescriptor& key) const noexcept; +}; diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 876c99252..3a85b72f5 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -1,9 +1,9 @@ #pragma once -#include -#include +#include +#include +#include #include -#include #include @@ -49,36 +49,6 @@ template class SharedAssetDepot; template class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { public: - /** - * Assets can be copied, but the fresh instance has no references and is not - * in the asset depot. - */ - SharedAsset(const SharedAsset& rhs) - : ExtensibleObject(rhs), _referenceCount(0), _pDepot(nullptr) {} - - /** - * After a move construction, the content of the asset is moved to the new - * instance, but the asset depot still references the old instance. - */ - SharedAsset(SharedAsset&& rhs) - : ExtensibleObject(std::move(rhs)), - _referenceCount(0), - _pDepot(nullptr) {} - - /** - * Assignment does not affect the asset's relationship with the depot, but is - * useful to assign the data in derived classes. - */ - SharedAsset& operator=(const SharedAsset& rhs) { - CesiumUtility::ExtensibleObject::operator=(rhs); - return *this; - } - - SharedAsset& operator=(SharedAsset&& rhs) { - CesiumUtility::ExtensibleObject::operator=(std::move(rhs)); - return *this; - } - /** * @brief Adds a counted reference to this object. Use * {@link CesiumUtility::IntrusivePointer} instead of calling this method @@ -128,6 +98,36 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { SharedAsset() = default; ~SharedAsset() { CESIUM_ASSERT(this->_referenceCount == 0); } + /** + * Assets can be copied, but the fresh instance has no references and is not + * in the asset depot. + */ + SharedAsset(const SharedAsset& rhs) + : ExtensibleObject(rhs), _referenceCount(0), _pDepot(nullptr) {} + + /** + * After a move construction, the content of the asset is moved to the new + * instance, but the asset depot still references the old instance. + */ + SharedAsset(SharedAsset&& rhs) + : ExtensibleObject(std::move(rhs)), + _referenceCount(0), + _pDepot(nullptr) {} + + /** + * Assignment does not affect the asset's relationship with the depot, but is + * useful to assign the data in derived classes. + */ + SharedAsset& operator=(const SharedAsset& rhs) { + CesiumUtility::ExtensibleObject::operator=(rhs); + return *this; + } + + SharedAsset& operator=(SharedAsset&& rhs) { + CesiumUtility::ExtensibleObject::operator=(std::move(rhs)); + return *this; + } + private: mutable std::atomic _referenceCount{0}; IDepotOwningAsset* _pDepot{nullptr}; diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 6efcd34c1..13f2459a0 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include @@ -11,80 +11,27 @@ #include #include -#include #include #include +#include #include -#include #include -#include namespace CesiumAsync { template class SharedAsset; -template class AssetFactory { -public: - virtual CesiumUtility::IntrusivePointer - createFrom(const gsl::span& data) const = 0; -}; - -struct NetworkAssetKey { - /** - * @brief The URL from which this network asset is downloaded. - */ - std::string url; - - /** - * @brief The HTTP headers used in requesting this asset. - */ - std::vector headers; - - bool operator==(const NetworkAssetKey& rhs) const noexcept; - - /** - * @brief Request this asset from the network using the provided asset - * accessor. - * - * @param asyncSystem The async system. - * @param pAssetAccessor The asset accessor. - * @return A future that resolves to the request once it is complete. - */ - Future> loadFromNetwork( - const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor) const; - - /** - * @brief Request this asset from the network using the provided asset - * accessor and return the downloaded bytes. - * - * @param asyncSystem The async system. - * @param pAssetAccessor The asset accessor. - * @return A future that resolves to the downloaded bytes if the request is - * successful, or a string describing the error if one occurred. - */ - Future>> loadBytesFromNetwork( - const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor) const; -}; - -template class IDepotOwningAsset { -public: - virtual ~IDepotOwningAsset() {} - virtual void markDeletionCandidate(const AssetType& asset) = 0; - virtual void unmarkDeletionCandidate(const AssetType& asset) = 0; -}; - /** - * A depot for {@link SharedAsset} instances, which are potentially shared between multiple objects. - * @tparam AssetType The type of asset stored in this depot. This should usually + * @brief A depot for {@link SharedAsset} instances, which are potentially shared between multiple objects. + * + * @tparam TAssetType The type of asset stored in this depot. This should * be derived from {@link SharedAsset}. */ -template +template class CESIUMASYNC_API SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< - SharedAssetDepot>, - public IDepotOwningAsset { + SharedAssetDepot>, + public IDepotOwningAsset { public: /** * @brief The maximum total byte usage of assets that have been loaded but are @@ -101,42 +48,44 @@ class CESIUMASYNC_API SharedAssetDepot int64_t staleAssetSizeLimit = 16 * 1024 * 1024; using FactorySignature = CesiumAsync::Future< - CesiumUtility::Result>>( + CesiumUtility::Result>>( const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, - const AssetKey& key); + const TAssetKey& key); SharedAssetDepot(std::function factory) : _factory(std::move(factory)) {} - ~SharedAssetDepot() { + virtual ~SharedAssetDepot() { // It's possible the assets will outlive the depot, if they're still in use. + // TODO: lock here, just in case? And swap order to release/set depot ptr + // below? for (auto& pair : this->_assets) { // Transfer ownership to the reference counting system. - CesiumUtility::IntrusivePointer pCounted = + CesiumUtility::IntrusivePointer pCounted = pair.second->pAsset.release(); pCounted->_pDepot = nullptr; } } /** - * Fetches an asset that has a {@link AssetFactory} defined and constructs it if possible. - * If the asset is already in this depot, it will be returned instead. - * If the asset has already started loading in this depot but hasn't finished, - * its future will be returned. + * @brief Gets an asset from the depot if it already exists, or creates it + * using the depot's factory if it does not. + * + * @param asyncSystem The async system. + * @param pAssetAccessor The asset accessor to use to download assets, if + * necessary. + * @param assetKey The key uniquely identifying the asset to get or create. + * @return A shared future that resolves when the asset is ready or fails. */ SharedFuture< - CesiumUtility::Result>> + CesiumUtility::Result>> getOrCreate( const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, - const AssetKey& assetKey) { - // We need to avoid: - // - Two assets starting to load before the first asset has updated the - // pendingAssets map - // - An asset starting to load after the previous load has been removed from - // the pendingAssets map, but before the completed asset has been added to - // the assets map. + const TAssetKey& assetKey) { + // We need to take care here to avoid two assets starting to load before the + // first asset has added an entry and set its maybePendingAsset field. // Calling the factory function while holding the mutex unnecessarily // limits parallelism. It can even lead to a bug in the scenario where the @@ -167,7 +116,7 @@ class CESIUMASYNC_API SharedAssetDepot // We haven't loaded or started to load this asset yet. // Let's do that now. - CesiumUtility::IntrusivePointer> + CesiumUtility::IntrusivePointer> pDepot = this; CesiumUtility::IntrusivePointer pEntry = new AssetEntry(assetKey); @@ -177,10 +126,16 @@ class CESIUMASYNC_API SharedAssetDepot .thenImmediately([pDepot, pEntry, asyncSystem, pAssetAccessor]() { return pDepot->_factory(asyncSystem, pAssetAccessor, pEntry->key); }) + .catchImmediately([](std::exception&& e) { + return CesiumUtility::Result< + CesiumUtility::IntrusivePointer>( + CesiumUtility::ErrorList::error( + std::string("Error creating asset: ") + e.what())); + }) .thenInWorkerThread( - [pDepot, - pEntry](CesiumUtility::Result< - CesiumUtility::IntrusivePointer>&& result) { + [pDepot, pEntry]( + CesiumUtility::Result< + CesiumUtility::IntrusivePointer>&& result) { std::lock_guard lock(pDepot->_mutex); if (result.pValue) { @@ -192,7 +147,7 @@ class CESIUMASYNC_API SharedAssetDepot // Now that this asset is owned by the depot, we exclusively // control its lifetime with a std::unique_ptr. pEntry->pAsset = - std::unique_ptr(result.pValue.get()); + std::unique_ptr(result.pValue.get()); pEntry->errorsAndWarnings = std::move(result.errors); pEntry->maybePendingAsset.reset(); @@ -200,7 +155,7 @@ class CESIUMASYNC_API SharedAssetDepot }); SharedFuture< - CesiumUtility::Result>> + CesiumUtility::Result>> sharedFuture = std::move(future).share(); pEntry->maybePendingAsset = sharedFuture; @@ -221,12 +176,17 @@ class CESIUMASYNC_API SharedAssetDepot /** * @brief Returns the total number of distinct assets contained in this depot. */ - size_t getDistinctCount() const { return this->_assets.size(); } + size_t getDistinctCount() const { + std::lock_guard lock(this->_mutex); + return this->_assets.size(); + } /** * @brief Returns the number of active references to assets in this depot. */ size_t getUsageCount() const { + std::lock_guard lock(this->_mutex); + size_t count = 0; for (const auto& [key, pEntry] : _assets) { if (pEntry->pAsset) { @@ -248,16 +208,16 @@ class CESIUMASYNC_API SharedAssetDepot private: // Disable copy - void operator=(const SharedAssetDepot& other) = delete; + void operator=(const SharedAssetDepot& other) = delete; /** * Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. May be called from any thread. */ - void markDeletionCandidate(const AssetType& asset) override { + void markDeletionCandidate(const TAssetType& asset) override { std::lock_guard lock(this->_mutex); - auto it = this->_assetsByPointer.find(const_cast(&asset)); + auto it = this->_assetsByPointer.find(const_cast(&asset)); CESIUM_ASSERT(it != this->_assetsByPointer.end()); if (it == this->_assetsByPointer.end()) { return; @@ -300,10 +260,10 @@ class CESIUMASYNC_API SharedAssetDepot * Unmarks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. May be called from any thread. */ - void unmarkDeletionCandidate(const AssetType& asset) override { + void unmarkDeletionCandidate(const TAssetType& asset) override { std::lock_guard lock(this->_mutex); - auto it = this->_assetsByPointer.find(const_cast(&asset)); + auto it = this->_assetsByPointer.find(const_cast(&asset)); CESIUM_ASSERT(it != this->_assetsByPointer.end()); if (it == this->_assetsByPointer.end()) { return; @@ -328,7 +288,7 @@ class CESIUMASYNC_API SharedAssetDepot */ struct AssetEntry : public CesiumUtility::ReferenceCountedThreadSafe { - AssetEntry(AssetKey&& key_) + AssetEntry(TAssetKey&& key_) : CesiumUtility::ReferenceCountedThreadSafe(), key(std::move(key_)), pAsset(), @@ -337,18 +297,18 @@ class CESIUMASYNC_API SharedAssetDepot sizeInDeletionList(0), deletionListPointers() {} - AssetEntry(const AssetKey& key_) : AssetEntry(AssetKey(key_)) {} + AssetEntry(const TAssetKey& key_) : AssetEntry(TAssetKey(key_)) {} /** * @brief The unique key identifying this asset. */ - AssetKey key; + TAssetKey key; /** * @brief A pointer to the asset. This may be nullptr if the asset is still * being loaded, or if it failed to load. */ - std::unique_ptr pAsset; + std::unique_ptr pAsset; /** * @brief If this asset is currently loading, this field holds a shared @@ -356,7 +316,7 @@ class CESIUMASYNC_API SharedAssetDepot * be empty if the asset finished loading, including if it failed to load. */ std::optional>>> + CesiumUtility::Result>>> maybePendingAsset; /** @@ -380,15 +340,15 @@ class CESIUMASYNC_API SharedAssetDepot */ CesiumUtility::DoublyLinkedListPointers deletionListPointers; - CesiumUtility::Result> + CesiumUtility::Result> toResult() const { - return CesiumUtility::Result>( + return CesiumUtility::Result>( pAsset.get(), errorsAndWarnings); } SharedFuture< - CesiumUtility::Result>> + CesiumUtility::Result>> toFuture(const AsyncSystem& asyncSystem) const { if (this->maybePendingAsset) { return *this->maybePendingAsset; @@ -400,12 +360,12 @@ class CESIUMASYNC_API SharedAssetDepot // Maps asset keys to AssetEntry instances. This collection owns the asset // entries. - std::unordered_map> + std::unordered_map> _assets; // Maps asset pointers to AssetEntry instances. The values in this map refer // to instances owned by the _assets map. - std::unordered_map _assetsByPointer; + std::unordered_map _assetsByPointer; // List of assets that are being considered for deletion, in the order that // they became unused. @@ -423,12 +383,7 @@ class CESIUMASYNC_API SharedAssetDepot // The factory used to create new AssetType instances. std::function _factory; - friend class SharedAsset; + friend class SharedAsset; }; } // namespace CesiumAsync - -template <> struct std::hash { - std::size_t - operator()(const CesiumAsync::NetworkAssetKey& key) const noexcept; -}; diff --git a/CesiumAsync/src/SharedAssetDepot.cpp b/CesiumAsync/src/NetworkAssetDescriptor.cpp similarity index 81% rename from CesiumAsync/src/SharedAssetDepot.cpp rename to CesiumAsync/src/NetworkAssetDescriptor.cpp index 4cdeff3eb..e0e62edae 100644 --- a/CesiumAsync/src/SharedAssetDepot.cpp +++ b/CesiumAsync/src/NetworkAssetDescriptor.cpp @@ -1,10 +1,11 @@ -#include +#include +#include #include using namespace CesiumUtility; -std::size_t std::hash::operator()( - const CesiumAsync::NetworkAssetKey& key) const noexcept { +std::size_t std::hash::operator()( + const CesiumAsync::NetworkAssetDescriptor& key) const noexcept { std::hash hash{}; size_t result = hash(key.url); @@ -19,7 +20,8 @@ std::size_t std::hash::operator()( namespace CesiumAsync { -bool NetworkAssetKey::operator==(const NetworkAssetKey& rhs) const noexcept { +bool NetworkAssetDescriptor::operator==( + const NetworkAssetDescriptor& rhs) const noexcept { if (this->url != rhs.url || this->headers.size() != rhs.headers.size()) return false; @@ -33,13 +35,14 @@ bool NetworkAssetKey::operator==(const NetworkAssetKey& rhs) const noexcept { } Future> -NetworkAssetKey::loadFromNetwork( +NetworkAssetDescriptor::loadFromNetwork( const CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor) const { return pAssetAccessor->get(asyncSystem, this->url, this->headers); } -Future>> NetworkAssetKey::loadBytesFromNetwork( +Future>> +NetworkAssetDescriptor::loadBytesFromNetwork( const CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor) const { return this->loadFromNetwork(asyncSystem, pAssetAccessor) diff --git a/CesiumAsync/test/MockAssetAccessor.h b/CesiumAsync/test/MockAssetAccessor.h index 175ce7909..0a8b1df65 100644 --- a/CesiumAsync/test/MockAssetAccessor.h +++ b/CesiumAsync/test/MockAssetAccessor.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index 31ff9bc9d..3c4eae2cd 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -3,8 +3,7 @@ #include "CesiumGltf/ImageAsset.h" #include "CesiumGltf/ImageSpec.h" #include "CesiumGltf/Library.h" - -#include +#include "CesiumUtility/IntrusivePointer.h" namespace CesiumGltf { /** @copydoc ImageSpec */ diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index 9d92566fb..449cca74d 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -1,28 +1,10 @@ #pragma once #include -#include -#include - -namespace CesiumGltf { -struct ImageAsset; -} +#include namespace CesiumGltfReader { -struct NetworkImageAssetKey : public CesiumAsync::NetworkAssetKey { - CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets{}; - - CesiumAsync::Future>> - load( - const CesiumAsync::AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor) const; -}; - -using NetworkImageAssetDepot = - CesiumAsync::SharedAssetDepot; - /** * @brief Contains assets that are potentially shared across multiple glTF * models. @@ -32,12 +14,10 @@ class GltfSharedAssetSystem public: static CesiumUtility::IntrusivePointer getDefault(); - CesiumUtility::IntrusivePointer pImage; + using ImageDepot = CesiumAsync:: + SharedAssetDepot; + + CesiumUtility::IntrusivePointer pImage; }; } // namespace CesiumGltfReader - -template <> struct std::hash { - std::size_t - operator()(const CesiumGltfReader::NetworkImageAssetKey& key) const noexcept; -}; diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h index 0c5ae99f6..b2d60d2df 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -1,7 +1,6 @@ #include "CesiumGltf/ImageAsset.h" #include "CesiumGltfReader/Library.h" -#include #include #include @@ -67,24 +66,4 @@ class ImageDecoder { generateMipMaps(CesiumGltf::ImageAsset& image); }; -/** - * Used to construct an ImageAsset. - */ -struct ImageAssetFactory : CesiumAsync::AssetFactory { - ImageAssetFactory( - const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets_) - : ktx2TranscodeTargets(ktx2TranscodeTargets_) {} - - CesiumUtility::IntrusivePointer - createFrom(const gsl::span& data) const override { - CesiumGltfReader::ImageReaderResult imageResult = - ImageDecoder::readImage(data, this->ktx2TranscodeTargets); - // TODO: report warnings and errors! - return imageResult.pImage; - } - -private: - const CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets; -}; - } // namespace CesiumGltfReader diff --git a/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h b/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h new file mode 100644 index 000000000..1f5129e4a --- /dev/null +++ b/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace CesiumAsync { +class AsyncSystem; +} + +namespace CesiumGltfReader { + +/** + * @brief A description of an image asset that can be loaded from the network + * using an {@link IAssetAccessor}. This includes a URL, any headers to be + * included in the request, and the set of supported GPU texture formats for + * KTX2 decoding. + */ +struct NetworkImageAssetDescriptor + : public CesiumAsync::NetworkAssetDescriptor { + /** + * @brief The supported GPU texture formats used for KTX2 decoding. + */ + CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets{}; + + /** + * @brief Determines if this descriptor is identical to another one. + */ + bool operator==(const NetworkImageAssetDescriptor& rhs) const noexcept; + + /** + * @brief Request this asset from the network using the provided asset + * accessor and return the loaded {@link ImageAsset}. + * + * @param asyncSystem The async system. + * @param pAssetAccessor The asset accessor. + * @return A future that resolves to the image asset once the request is + * complete. + */ + CesiumAsync::Future>> + load( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const; +}; + +} // namespace CesiumGltfReader + +template <> struct std::hash { + std::size_t operator()( + const CesiumGltfReader::NetworkImageAssetDescriptor& key) const noexcept; +}; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 9de379d18..587bacadd 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -537,7 +536,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( const std::string& uri, const std::vector& headers) -> SharedFuture>> { - NetworkImageAssetKey assetKey{ + NetworkImageAssetDescriptor assetKey{ uri, headers, options.ktx2TranscodeTargets}; diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index b1ef39016..d49f4964f 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -1,31 +1,10 @@ -#include #include -#include -#include -#include using namespace CesiumAsync; using namespace CesiumGltf; using namespace CesiumGltfReader; using namespace CesiumUtility; -std::size_t std::hash::operator()( - const NetworkImageAssetKey& key) const noexcept { - std::hash baseHash{}; - std::hash ktxHash{}; - - size_t result = baseHash(key); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_R)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RG)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RGB)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RGBA)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_R)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RG)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RGB)); - result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RGBA)); - return result; -} - namespace { CesiumUtility::IntrusivePointer createDefault() { @@ -35,7 +14,7 @@ CesiumUtility::IntrusivePointer createDefault() { p->pImage.emplace(std::function( [](const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, - const NetworkImageAssetKey& key) + const NetworkImageAssetDescriptor& key) -> Future>> { return key.load(asyncSystem, pAssetAccessor); })); @@ -47,28 +26,6 @@ CesiumUtility::IntrusivePointer createDefault() { namespace CesiumGltfReader { -Future>> NetworkImageAssetKey::load( - const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor) const { - return this->loadBytesFromNetwork(asyncSystem, pAssetAccessor) - .thenInWorkerThread([ktx2TranscodeTargets = this->ktx2TranscodeTargets]( - Result>&& result) { - if (!result.value) { - return Result>(result.errors); - } - - ImageReaderResult imageResult = - ImageDecoder::readImage(*result.value, ktx2TranscodeTargets); - - result.errors.merge( - ErrorList{imageResult.errors, imageResult.warnings}); - - return Result>( - imageResult.pImage, - std::move(result.errors)); - }); -} - /*static*/ CesiumUtility::IntrusivePointer GltfSharedAssetSystem::getDefault() { static CesiumUtility::IntrusivePointer pDefault = diff --git a/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp b/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp new file mode 100644 index 000000000..3ea2d7109 --- /dev/null +++ b/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp @@ -0,0 +1,74 @@ +#include +#include +#include + +using namespace CesiumAsync; +using namespace CesiumGltf; +using namespace CesiumGltfReader; +using namespace CesiumUtility; + +namespace CesiumGltfReader { + +bool NetworkImageAssetDescriptor::operator==( + const NetworkImageAssetDescriptor& rhs) const noexcept { + if (!NetworkAssetDescriptor::operator==(rhs)) + return false; + + return this->ktx2TranscodeTargets.ETC1S_R == + rhs.ktx2TranscodeTargets.ETC1S_R && + this->ktx2TranscodeTargets.ETC1S_RG == + this->ktx2TranscodeTargets.ETC1S_RG && + this->ktx2TranscodeTargets.ETC1S_RGB == + this->ktx2TranscodeTargets.ETC1S_RGB && + this->ktx2TranscodeTargets.ETC1S_RGBA == + this->ktx2TranscodeTargets.ETC1S_RGBA && + this->ktx2TranscodeTargets.UASTC_R == + rhs.ktx2TranscodeTargets.UASTC_R && + this->ktx2TranscodeTargets.UASTC_RG == + this->ktx2TranscodeTargets.UASTC_RG && + this->ktx2TranscodeTargets.UASTC_RGB == + this->ktx2TranscodeTargets.UASTC_RGB && + this->ktx2TranscodeTargets.UASTC_RGBA == + this->ktx2TranscodeTargets.UASTC_RGBA; +} + +Future>> NetworkImageAssetDescriptor::load( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) const { + return this->loadBytesFromNetwork(asyncSystem, pAssetAccessor) + .thenInWorkerThread([ktx2TranscodeTargets = this->ktx2TranscodeTargets]( + Result>&& result) { + if (!result.value) { + return Result>(result.errors); + } + + ImageReaderResult imageResult = + ImageDecoder::readImage(*result.value, ktx2TranscodeTargets); + + result.errors.merge( + ErrorList{imageResult.errors, imageResult.warnings}); + + return Result>( + imageResult.pImage, + std::move(result.errors)); + }); +} + +} // namespace CesiumGltfReader + +std::size_t std::hash::operator()( + const NetworkImageAssetDescriptor& key) const noexcept { + std::hash baseHash{}; + std::hash ktxHash{}; + + size_t result = baseHash(key); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_R)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RG)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RGB)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.ETC1S_RGBA)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_R)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RG)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RGB)); + result = Hash::combine(result, ktxHash(key.ktx2TranscodeTargets.UASTC_RGBA)); + return result; +} diff --git a/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h b/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h index 4c0943a50..2bfd636cc 100644 --- a/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h +++ b/CesiumNativeTests/include/CesiumNativeTests/SimpleAssetAccessor.h @@ -3,6 +3,7 @@ #include "SimpleAssetRequest.h" #include "SimpleAssetResponse.h" +#include #include #include diff --git a/CesiumNativeTests/src/FileAccessor.cpp b/CesiumNativeTests/src/FileAccessor.cpp index b59a53963..0484a27d2 100644 --- a/CesiumNativeTests/src/FileAccessor.cpp +++ b/CesiumNativeTests/src/FileAccessor.cpp @@ -1,3 +1,4 @@ +#include #include #include #include diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlay.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlay.h index a7e681d7c..47a64f3dd 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlay.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlay.h @@ -3,6 +3,7 @@ #include "Library.h" #include "RasterOverlayLoadFailureDetails.h" +#include #include #include #include From d16e3ef94a133d061edf4bf71303753e77fbf271 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 28 Oct 2024 21:37:38 +1100 Subject: [PATCH 58/81] Add doc. --- .../include/CesiumGltfReader/GltfSharedAssetSystem.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index 449cca74d..1f04987af 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -17,6 +17,9 @@ class GltfSharedAssetSystem using ImageDepot = CesiumAsync:: SharedAssetDepot; + /** + * @brief The asset depot for images. + */ CesiumUtility::IntrusivePointer pImage; }; From 49f1d6319c2987af92f680ab319bbdc221733be1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 09:24:43 +1100 Subject: [PATCH 59/81] Introduce TilesetSharedAssetSystem. --- .../include/Cesium3DTilesSelection/Tileset.h | 7 ++-- .../TilesetSharedAssetSystem.h | 18 ++++++++++ .../src/TileContentLoadInfo.cpp | 6 ++-- .../src/TileContentLoadInfo.h | 9 +++-- Cesium3DTilesSelection/src/Tileset.cpp | 6 ++-- .../src/TilesetContentManager.cpp | 12 +++---- .../src/TilesetContentManager.h | 8 ++--- .../src/TilesetJsonLoader.h | 5 ++- .../src/TilesetSharedAssetSystem.cpp | 34 +++++++++++++++++++ .../CesiumGltfReader/GltfSharedAssetSystem.h | 2 ++ 10 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetSharedAssetSystem.h create mode 100644 Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index b8f9699bf..840d331a8 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -23,10 +23,12 @@ #include namespace Cesium3DTilesSelection { + class TilesetContentManager; class TilesetMetadata; class TilesetHeightQuery; class TilesetHeightRequest; +class TilesetSharedAssetSystem; /** * @brief A + +namespace Cesium3DTilesSelection { + +/** + * @brief Contains assets that are potentially shared across multiple Tilesets. + */ +class TilesetSharedAssetSystem + : public CesiumGltfReader::GltfSharedAssetSystem { +public: + static CesiumUtility::IntrusivePointer getDefault(); + + virtual ~TilesetSharedAssetSystem() = default; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp index f643f71e4..015e3e84c 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp @@ -9,8 +9,8 @@ TileContentLoadInfo::TileContentLoadInfo( const std::shared_ptr& pPrepareRendererResources_, const std::shared_ptr& pLogger_, - const CesiumUtility::IntrusivePointer< - CesiumGltfReader::GltfSharedAssetSystem> pAssetDepot_, + const CesiumUtility::IntrusivePointer + pSharedAssetSystem_, const TilesetContentOptions& contentOptions_, const Tile& tile) : asyncSystem(asyncSystem_), @@ -20,7 +20,7 @@ TileContentLoadInfo::TileContentLoadInfo( tileID(tile.getTileID()), tileBoundingVolume(tile.getBoundingVolume()), tileContentBoundingVolume(tile.getContentBoundingVolume()), - pAssetDepot{pAssetDepot_}, + pSharedAssetSystem{pSharedAssetSystem_}, tileRefine(tile.getRefine()), tileGeometricError(tile.getGeometricError()), tileTransform(tile.getTransform()), diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.h b/Cesium3DTilesSelection/src/TileContentLoadInfo.h index 4c9d89e3e..8b2d51d33 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.h +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.h @@ -6,10 +6,10 @@ #include #include #include +#include #include #include #include -#include #include #include @@ -25,8 +25,8 @@ struct TileContentLoadInfo { const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, - const CesiumUtility::IntrusivePointer< - CesiumGltfReader::GltfSharedAssetSystem> maybeAssetDepot, + const CesiumUtility::IntrusivePointer + pSharedAssetSystem, const TilesetContentOptions& contentOptions, const Tile& tile); @@ -43,8 +43,7 @@ struct TileContentLoadInfo { BoundingVolume tileBoundingVolume; std::optional tileContentBoundingVolume; - CesiumUtility::IntrusivePointer - pAssetDepot; + CesiumUtility::IntrusivePointer pSharedAssetSystem; TileRefine tileRefine; diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index a98af5d23..0b7df6cd0 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -149,13 +149,11 @@ const RasterOverlayCollection& Tileset::getOverlays() const noexcept { return this->_pTilesetContentManager->getRasterOverlayCollection(); } -CesiumGltfReader::GltfSharedAssetSystem& -Tileset::getSharedAssetSystem() noexcept { +TilesetSharedAssetSystem& Tileset::getSharedAssetSystem() noexcept { return *this->_pTilesetContentManager->getSharedAssetSystem(); } -const CesiumGltfReader::GltfSharedAssetSystem& -Tileset::getSharedAssetSystem() const noexcept { +const TilesetSharedAssetSystem& Tileset::getSharedAssetSystem() const noexcept { return *this->_pTilesetContentManager->getSharedAssetSystem(); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 7595bae46..d4bc7ed0d 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -565,8 +565,8 @@ postProcessContentInWorkerThread( tileLoadInfo.contentOptions.ktx2TranscodeTargets; gltfOptions.applyTextureTransform = tileLoadInfo.contentOptions.applyTextureTransform; - if (tileLoadInfo.pAssetDepot) { - gltfOptions.pSharedAssets = tileLoadInfo.pAssetDepot; + if (tileLoadInfo.pSharedAssetSystem) { + gltfOptions.pSharedAssets = tileLoadInfo.pSharedAssetSystem; } auto asyncSystem = tileLoadInfo.asyncSystem; @@ -666,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), + _pSharedAssets(TilesetSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), + _pSharedAssets(TilesetSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(CesiumGltfReader::GltfSharedAssetSystem::getDefault()), + _pSharedAssets(TilesetSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -1237,7 +1237,7 @@ TilesetContentManager::getTilesetCredits() const noexcept { return this->_tilesetCredits; } -const CesiumUtility::IntrusivePointer& +const CesiumUtility::IntrusivePointer& TilesetContentManager::getSharedAssetSystem() const noexcept { return this->_pSharedAssets; } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 12bd65484..e3d32b2da 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -18,6 +18,8 @@ namespace Cesium3DTilesSelection { +class TilesetSharedAssetSystem; + class TilesetContentManager : public CesiumUtility::ReferenceCountedNonThreadSafe< TilesetContentManager> { @@ -115,8 +117,7 @@ class TilesetContentManager const std::vector& getTilesetCredits() const noexcept; - const CesiumUtility::IntrusivePointer< - CesiumGltfReader::GltfSharedAssetSystem>& + const CesiumUtility::IntrusivePointer& getSharedAssetSystem() const noexcept; int32_t getNumberOfTilesLoading() const noexcept; @@ -172,8 +173,7 @@ class TilesetContentManager int64_t _tilesDataUsed; // Stores assets that might be shared between tiles. - CesiumUtility::IntrusivePointer - _pSharedAssets; + CesiumUtility::IntrusivePointer _pSharedAssets; CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index fa590bdd6..55a5e8ae4 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -4,9 +4,9 @@ #include #include +#include #include #include -#include #include @@ -56,8 +56,7 @@ class TilesetJsonLoader : public TilesetContentLoader { private: std::string _baseUrl; CesiumGeospatial::Ellipsoid _ellipsoid; - CesiumUtility::IntrusivePointer - _pSharedAssets; + CesiumUtility::IntrusivePointer _pSharedAssets; /** * @brief The axis that was declared as the "up-axis" for glTF content. diff --git a/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp b/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp new file mode 100644 index 000000000..3f6c0806b --- /dev/null +++ b/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp @@ -0,0 +1,34 @@ +#include + +using namespace Cesium3DTilesSelection; +using namespace CesiumAsync; +using namespace CesiumGltf; +using namespace CesiumGltfReader; +using namespace CesiumUtility; + +namespace { + +CesiumUtility::IntrusivePointer createDefault() { + CesiumUtility::IntrusivePointer p = + new TilesetSharedAssetSystem(); + + CesiumUtility::IntrusivePointer pGltf = + GltfSharedAssetSystem::getDefault(); + + p->pImage = pGltf->pImage; + + return p; +} + +} // namespace + +namespace Cesium3DTilesSelection { + +/*static*/ CesiumUtility::IntrusivePointer +TilesetSharedAssetSystem::getDefault() { + static CesiumUtility::IntrusivePointer pDefault = + createDefault(); + return pDefault; +} + +} // namespace Cesium3DTilesSelection diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h index 1f04987af..f28eaafa1 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfSharedAssetSystem.h @@ -14,6 +14,8 @@ class GltfSharedAssetSystem public: static CesiumUtility::IntrusivePointer getDefault(); + virtual ~GltfSharedAssetSystem() = default; + using ImageDepot = CesiumAsync:: SharedAssetDepot; From d43edcea856e5cee48c9ad19eb33abc2c9f5e08e Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 11:38:15 +1100 Subject: [PATCH 60/81] Add ResultPointer. --- .../src/TileContentLoadInfo.cpp | 2 +- .../src/TileContentLoadInfo.h | 2 +- .../include/CesiumAsync/SharedAssetDepot.h | 30 ++++++++----------- CesiumGltfReader/src/GltfReader.cpp | 7 ++--- .../src/GltfSharedAssetSystem.cpp | 2 +- .../src/NetworkImageAssetDescriptor.cpp | 6 ++-- CesiumUtility/include/CesiumUtility/Result.h | 8 +++++ 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp index 015e3e84c..136051632 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.cpp @@ -9,7 +9,7 @@ TileContentLoadInfo::TileContentLoadInfo( const std::shared_ptr& pPrepareRendererResources_, const std::shared_ptr& pLogger_, - const CesiumUtility::IntrusivePointer + const CesiumUtility::IntrusivePointer& pSharedAssetSystem_, const TilesetContentOptions& contentOptions_, const Tile& tile) diff --git a/Cesium3DTilesSelection/src/TileContentLoadInfo.h b/Cesium3DTilesSelection/src/TileContentLoadInfo.h index 8b2d51d33..261693b87 100644 --- a/Cesium3DTilesSelection/src/TileContentLoadInfo.h +++ b/Cesium3DTilesSelection/src/TileContentLoadInfo.h @@ -25,7 +25,7 @@ struct TileContentLoadInfo { const std::shared_ptr& pPrepareRendererResources, const std::shared_ptr& pLogger, - const CesiumUtility::IntrusivePointer + const CesiumUtility::IntrusivePointer& pSharedAssetSystem, const TilesetContentOptions& contentOptions, const Tile& tile); diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 13f2459a0..e4e3321dd 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -47,11 +47,11 @@ class CESIUMASYNC_API SharedAssetDepot */ int64_t staleAssetSizeLimit = 16 * 1024 * 1024; - using FactorySignature = CesiumAsync::Future< - CesiumUtility::Result>>( - const AsyncSystem& asyncSystem, - const std::shared_ptr& pAssetAccessor, - const TAssetKey& key); + using FactorySignature = + CesiumAsync::Future>( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const TAssetKey& key); SharedAssetDepot(std::function factory) : _factory(std::move(factory)) {} @@ -78,9 +78,7 @@ class CESIUMASYNC_API SharedAssetDepot * @param assetKey The key uniquely identifying the asset to get or create. * @return A shared future that resolves when the asset is ready or fails. */ - SharedFuture< - CesiumUtility::Result>> - getOrCreate( + SharedFuture> getOrCreate( const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, const TAssetKey& assetKey) { @@ -154,9 +152,8 @@ class CESIUMASYNC_API SharedAssetDepot return pEntry->toResult(); }); - SharedFuture< - CesiumUtility::Result>> - sharedFuture = std::move(future).share(); + SharedFuture> sharedFuture = + std::move(future).share(); pEntry->maybePendingAsset = sharedFuture; @@ -315,8 +312,7 @@ class CESIUMASYNC_API SharedAssetDepot * future that will resolve when the asset load is complete. This field will * be empty if the asset finished loading, including if it failed to load. */ - std::optional>>> + std::optional>> maybePendingAsset; /** @@ -340,15 +336,13 @@ class CESIUMASYNC_API SharedAssetDepot */ CesiumUtility::DoublyLinkedListPointers deletionListPointers; - CesiumUtility::Result> - toResult() const { - return CesiumUtility::Result>( + CesiumUtility::ResultPointer toResult() const { + return CesiumUtility::ResultPointer( pAsset.get(), errorsAndWarnings); } - SharedFuture< - CesiumUtility::Result>> + SharedFuture> toFuture(const AsyncSystem& asyncSystem) const { if (this->maybePendingAsset) { return *this->maybePendingAsset; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 587bacadd..7e5f9fabe 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -535,7 +535,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( const std::shared_ptr& pAssetAccessor, const std::string& uri, const std::vector& headers) - -> SharedFuture>> { + -> SharedFuture> { NetworkImageAssetDescriptor assetKey{ uri, headers, @@ -554,12 +554,11 @@ void CesiumGltfReader::GltfReader::postprocessGltf( } }; - SharedFuture>> future = + SharedFuture> future = getAsset(asyncSystem, pAssetAccessor, uri, tHeaders); resolvedBuffers.push_back(future.thenInWorkerThread( - [pImage = &image]( - const Result>& loadedImage) { + [pImage = &image](const ResultPointer& loadedImage) { std::string imageUri = *pImage->uri; pImage->uri = std::nullopt; diff --git a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp index d49f4964f..ffe97c6cd 100644 --- a/CesiumGltfReader/src/GltfSharedAssetSystem.cpp +++ b/CesiumGltfReader/src/GltfSharedAssetSystem.cpp @@ -15,7 +15,7 @@ CesiumUtility::IntrusivePointer createDefault() { [](const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, const NetworkImageAssetDescriptor& key) - -> Future>> { + -> Future> { return key.load(asyncSystem, pAssetAccessor); })); diff --git a/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp b/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp index 3ea2d7109..37c1c377f 100644 --- a/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp +++ b/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp @@ -32,14 +32,14 @@ bool NetworkImageAssetDescriptor::operator==( this->ktx2TranscodeTargets.UASTC_RGBA; } -Future>> NetworkImageAssetDescriptor::load( +Future> NetworkImageAssetDescriptor::load( const AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor) const { return this->loadBytesFromNetwork(asyncSystem, pAssetAccessor) .thenInWorkerThread([ktx2TranscodeTargets = this->ktx2TranscodeTargets]( Result>&& result) { if (!result.value) { - return Result>(result.errors); + return ResultPointer(result.errors); } ImageReaderResult imageResult = @@ -48,7 +48,7 @@ Future>> NetworkImageAssetDescriptor::load( result.errors.merge( ErrorList{imageResult.errors, imageResult.warnings}); - return Result>( + return ResultPointer( imageResult.pImage, std::move(result.errors)); }); diff --git a/CesiumUtility/include/CesiumUtility/Result.h b/CesiumUtility/include/CesiumUtility/Result.h index 1ba5e1671..71789e20a 100644 --- a/CesiumUtility/include/CesiumUtility/Result.h +++ b/CesiumUtility/include/CesiumUtility/Result.h @@ -75,4 +75,12 @@ template struct Result> { ErrorList errors; }; +/** + * @brief A convenient shortcut for + * `CesiumUtility::Result>`. + * + * @tparam T The type of object that the IntrusivePointer points to. + */ +template using ResultPointer = Result>; + } // namespace CesiumUtility From 7df4dc43b7ce9ad3a700fdc3d6002a0c5db900fe Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 13:39:41 +1100 Subject: [PATCH 61/81] Keep depot alive if any live assets it manages are alive. --- .../include/CesiumAsync/SharedAssetDepot.h | 59 +++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index e4e3321dd..8704dabbc 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -54,18 +54,37 @@ class CESIUMASYNC_API SharedAssetDepot const TAssetKey& key); SharedAssetDepot(std::function factory) - : _factory(std::move(factory)) {} + : _assets(), + _assetsByPointer(), + _deletionCandidates(), + _totalDeletionCandidateMemoryUsage(0), + _mutex(), + _factory(std::move(factory)), + _pKeepAlive(nullptr) {} virtual ~SharedAssetDepot() { - // It's possible the assets will outlive the depot, if they're still in use. - // TODO: lock here, just in case? And swap order to release/set depot ptr - // below? - for (auto& pair : this->_assets) { - // Transfer ownership to the reference counting system. - CesiumUtility::IntrusivePointer pCounted = - pair.second->pAsset.release(); - pCounted->_pDepot = nullptr; - } + // Ideally, when the depot is destroyed, all the assets it owns would become + // independent assets. But this is extremely difficult to manage in a + // thread-safe manner. + + // Since we're in the destructor, we can be sure no one has a reference to + // this instance anymore. That means that no other thread can be executing + // `getOrCreate`, and no async asset creations are in progress. + + // However, if assets owned by this depot are still alive, then other + // threads can still be calling addReference / releaseReference on some of + // our assets even while we're running the depot's destructor. Which means + // that we can end up in `markDeletionCandidate` at the same time the + // destructor is running. And in fact it's possible for a `SharedAsset` with + // especially poor timing to call into a `SharedAssetDepot` just after it is + // destroyed. + + // To avoid this, we use the _pKeepAlive field to maintain an artificial + // reference to this depot whenever it owns live assets. This should keep + // this destructor from being called except when all of its assets are also + // in the _deletionCandidates list. + + CESIUM_ASSERT(this->_assets.size() == this->_deletionCandidates.size()); } /** @@ -149,6 +168,11 @@ class CESIUMASYNC_API SharedAssetDepot pEntry->errorsAndWarnings = std::move(result.errors); pEntry->maybePendingAsset.reset(); + // The asset is initially live because we have an + // IntrusivePointer to it right here. So make sure the depot + // stays alive, too. + pDepot->_pKeepAlive = pDepot; + return pEntry->toResult(); }); @@ -251,6 +275,12 @@ class CESIUMASYNC_API SharedAssetDepot this->_assets.erase(pOldEntry->key); } } + + // If this depot is not managing any live assets, then we no longer need to + // keep it alive. + if (this->_assets.size() == this->_deletionCandidates.size()) { + this->_pKeepAlive.reset(); + } } /** @@ -277,6 +307,9 @@ class CESIUMASYNC_API SharedAssetDepot this->_totalDeletionCandidateMemoryUsage -= entry.sizeInDeletionList; this->_deletionCandidates.remove(entry); } + + // This depot is now managing at least one live asset, so keep it alive. + this->_pKeepAlive = this; } /** @@ -377,6 +410,12 @@ class CESIUMASYNC_API SharedAssetDepot // The factory used to create new AssetType instances. std::function _factory; + // This instance keeps a reference to itself whenever it is managing active + // assets, preventing it from being destroyed even if all other references to + // it are dropped. + CesiumUtility::IntrusivePointer> + _pKeepAlive; + friend class SharedAsset; }; From feaf448dc361d28df1a39e0c7af66035ccec7e84 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 14:07:09 +1100 Subject: [PATCH 62/81] Flesh out utility functions slightly. --- .../test/TestTilesetContentManager.cpp | 19 +++++++++++- .../include/CesiumAsync/SharedAssetDepot.h | 31 ++++++++++--------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 72f809743..4070f8dc4 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1682,11 +1682,28 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CHECK(images.size() == 1); } - CHECK(pManager->getSharedAssetSystem()->pImage->getDistinctCount() == 2); + CHECK( + pManager->getSharedAssetSystem() + ->pImage->getInactiveAssetTotalSizeBytes() == 0); + CHECK(pManager->getSharedAssetSystem()->pImage->getAssetCount() == 2); + CHECK(pManager->getSharedAssetSystem()->pImage->getActiveAssetCount() == 2); + CHECK( + pManager->getSharedAssetSystem()->pImage->getInactiveAssetCount() == 0); // unload the tile content for (auto& child : containerTile.getChildren()) { pManager->unloadTileContent(child); } + + // Both of the assets will become inactive, and one of them will be + // destroyed, in order to bring the total under the limit. + CHECK( + pManager->getSharedAssetSystem() + ->pImage->getInactiveAssetTotalSizeBytes() <= + pManager->getSharedAssetSystem()->pImage->staleAssetSizeLimit); + CHECK(pManager->getSharedAssetSystem()->pImage->getAssetCount() == 1); + CHECK(pManager->getSharedAssetSystem()->pImage->getActiveAssetCount() == 0); + CHECK( + pManager->getSharedAssetSystem()->pImage->getInactiveAssetCount() == 1); } } diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 8704dabbc..02fd1e6ee 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -195,34 +195,37 @@ class CESIUMASYNC_API SharedAssetDepot } /** - * @brief Returns the total number of distinct assets contained in this depot. + * @brief Returns the total number of distinct assets contained in this depot, + * including both active and inactive assets. */ - size_t getDistinctCount() const { + size_t getAssetCount() const { std::lock_guard lock(this->_mutex); return this->_assets.size(); } /** - * @brief Returns the number of active references to assets in this depot. + * @brief Gets the number of assets owned by this depot that are active, + * meaning that they are currently being used in one or more places. */ - size_t getUsageCount() const { + size_t getActiveAssetCount() const { std::lock_guard lock(this->_mutex); - - size_t count = 0; - for (const auto& [key, pEntry] : _assets) { - if (pEntry->pAsset) { - count += pEntry->pAsset->_referenceCount; - } - } - return count; + return this->_assets.size() - this->_deletionCandidates.size(); } - size_t getDeletionCandidateCount() const { + /** + * @brief Gets the number of assets owned by this depot that are inactive, + * meaning that they are not currently being used. + */ + size_t getInactiveAssetCount() const { std::lock_guard lock(this->_mutex); return this->_deletionCandidates.size(); } - int64_t getDeletionCandidateTotalSizeBytes() const { + /** + * @brief Gets the total bytes used by inactive (unused) assets owned by this + * depot. + */ + int64_t getInactiveAssetTotalSizeBytes() const { std::lock_guard lock(this->_mutex); return this->_totalDeletionCandidateMemoryUsage; } From c73f5b5f0517f7c4c38528779501db19bfc3c3f1 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 14:09:34 +1100 Subject: [PATCH 63/81] staleAssetSizeLimit -> inactiveAssetSizeLimitBytes --- Cesium3DTilesSelection/test/TestTilesetContentManager.cpp | 2 +- CesiumAsync/include/CesiumAsync/SharedAssetDepot.h | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 4070f8dc4..ca08932f4 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -1700,7 +1700,7 @@ TEST_CASE("Test the tileset content manager's post processing for gltf") { CHECK( pManager->getSharedAssetSystem() ->pImage->getInactiveAssetTotalSizeBytes() <= - pManager->getSharedAssetSystem()->pImage->staleAssetSizeLimit); + pManager->getSharedAssetSystem()->pImage->inactiveAssetSizeLimitBytes); CHECK(pManager->getSharedAssetSystem()->pImage->getAssetCount() == 1); CHECK(pManager->getSharedAssetSystem()->pImage->getActiveAssetCount() == 0); CHECK( diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 02fd1e6ee..c1e8012fe 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -45,7 +45,7 @@ class CESIUMASYNC_API SharedAssetDepot * * Default is 16MiB. */ - int64_t staleAssetSizeLimit = 16 * 1024 * 1024; + int64_t inactiveAssetSizeLimitBytes = 16 * 1024 * 1024; using FactorySignature = CesiumAsync::Future>( @@ -255,11 +255,12 @@ class CESIUMASYNC_API SharedAssetDepot this->_deletionCandidates.insertAtTail(entry); - if (this->_totalDeletionCandidateMemoryUsage > this->staleAssetSizeLimit) { + if (this->_totalDeletionCandidateMemoryUsage > + this->inactiveAssetSizeLimitBytes) { // Delete the deletion candidates until we're below the limit. while (this->_deletionCandidates.size() > 0 && this->_totalDeletionCandidateMemoryUsage > - this->staleAssetSizeLimit) { + this->inactiveAssetSizeLimitBytes) { AssetEntry* pOldEntry = this->_deletionCandidates.head(); this->_deletionCandidates.remove(*pOldEntry); From ff87332b7cc12e1838b3f47e13d9ceea2539dbe4 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 14:28:05 +1100 Subject: [PATCH 64/81] Formatting. --- CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h | 2 +- CesiumUtility/include/CesiumUtility/Hash.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h b/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h index a912689f9..1b02211c7 100644 --- a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h +++ b/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h @@ -17,4 +17,4 @@ template class IDepotOwningAsset { virtual void unmarkDeletionCandidate(const TAssetType& asset) = 0; }; -} +} // namespace CesiumAsync diff --git a/CesiumUtility/include/CesiumUtility/Hash.h b/CesiumUtility/include/CesiumUtility/Hash.h index 5f817e330..c30cb2ab2 100644 --- a/CesiumUtility/include/CesiumUtility/Hash.h +++ b/CesiumUtility/include/CesiumUtility/Hash.h @@ -6,4 +6,4 @@ struct Hash { static size_t combine(size_t first, size_t second); }; -} +} // namespace CesiumUtility From fd601bceafae84fc51a575bda76ef23551bd34be Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 14:31:21 +1100 Subject: [PATCH 65/81] Fix clang errors. --- CesiumUtility/include/CesiumUtility/Hash.h | 4 +++- CesiumUtility/src/Hash.cpp | 8 +++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CesiumUtility/include/CesiumUtility/Hash.h b/CesiumUtility/include/CesiumUtility/Hash.h index c30cb2ab2..74ca7abfc 100644 --- a/CesiumUtility/include/CesiumUtility/Hash.h +++ b/CesiumUtility/include/CesiumUtility/Hash.h @@ -1,9 +1,11 @@ #pragma once +#include + namespace CesiumUtility { struct Hash { - static size_t combine(size_t first, size_t second); + static std::size_t combine(std::size_t first, std::size_t second); }; } // namespace CesiumUtility diff --git a/CesiumUtility/src/Hash.cpp b/CesiumUtility/src/Hash.cpp index 71c5b1a13..927614105 100644 --- a/CesiumUtility/src/Hash.cpp +++ b/CesiumUtility/src/Hash.cpp @@ -1,7 +1,5 @@ #include -#include - namespace CesiumUtility { // This function is adapted from Boost v1.86.0, `hash_mix_impl<64>` function. @@ -59,8 +57,8 @@ namespace CesiumUtility { // (https://mostlymangling.blogspot.com/2019/12/stronger-better-morer-moremur-better.html) namespace { -inline std::uint64_t mix(std::uint64_t x) { - std::uint64_t const m = 0xe9846af9b1a615d; +inline std::size_t mix(std::size_t x) { + std::size_t const m = 0xe9846af9b1a615d; x ^= x >> 32; x *= m; @@ -74,7 +72,7 @@ inline std::uint64_t mix(std::uint64_t x) { } // namespace // This function is adapted from Boost's `hash_combine`. -size_t Hash::combine(size_t first, size_t second) { +std::size_t Hash::combine(std::size_t first, std::size_t second) { return mix(first + 0x9e3779b9 + second); } From e9fac59ef293b9eaf8a6e633b1d2446e26cfeff5 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 15:07:15 +1100 Subject: [PATCH 66/81] Fix incorrect comparison. --- CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp b/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp index 37c1c377f..2284bff7c 100644 --- a/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp +++ b/CesiumGltfReader/src/NetworkImageAssetDescriptor.cpp @@ -17,19 +17,19 @@ bool NetworkImageAssetDescriptor::operator==( return this->ktx2TranscodeTargets.ETC1S_R == rhs.ktx2TranscodeTargets.ETC1S_R && this->ktx2TranscodeTargets.ETC1S_RG == - this->ktx2TranscodeTargets.ETC1S_RG && + rhs.ktx2TranscodeTargets.ETC1S_RG && this->ktx2TranscodeTargets.ETC1S_RGB == - this->ktx2TranscodeTargets.ETC1S_RGB && + rhs.ktx2TranscodeTargets.ETC1S_RGB && this->ktx2TranscodeTargets.ETC1S_RGBA == - this->ktx2TranscodeTargets.ETC1S_RGBA && + rhs.ktx2TranscodeTargets.ETC1S_RGBA && this->ktx2TranscodeTargets.UASTC_R == rhs.ktx2TranscodeTargets.UASTC_R && this->ktx2TranscodeTargets.UASTC_RG == - this->ktx2TranscodeTargets.UASTC_RG && + rhs.ktx2TranscodeTargets.UASTC_RG && this->ktx2TranscodeTargets.UASTC_RGB == - this->ktx2TranscodeTargets.UASTC_RGB && + rhs.ktx2TranscodeTargets.UASTC_RGB && this->ktx2TranscodeTargets.UASTC_RGBA == - this->ktx2TranscodeTargets.UASTC_RGBA; + rhs.ktx2TranscodeTargets.UASTC_RGBA; } Future> NetworkImageAssetDescriptor::load( From 0961978f0b6887cbe22f4373094f4b7e250461c9 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 15:42:06 +1100 Subject: [PATCH 67/81] Fix a clang warning. --- CesiumGltfReader/src/GltfReader.cpp | 46 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 7e5f9fabe..f2a5fcbc1 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -504,23 +504,35 @@ void CesiumGltfReader::GltfReader::postprocessGltf( resolvedBuffers.push_back( pAssetAccessor ->get(asyncSystem, Uri::resolve(baseUrl, *buffer.uri), tHeaders) - .thenInWorkerThread( - [pBuffer = - &buffer](std::shared_ptr&& pRequest) { - const IAssetResponse* pResponse = pRequest->response(); - - std::string bufferUri = *pBuffer->uri; - - if (pResponse) { - pBuffer->uri = std::nullopt; - pBuffer->cesium.data = std::vector( - pResponse->data().begin(), - pResponse->data().end()); - return ExternalBufferLoadResult{true, bufferUri}; - } - - return ExternalBufferLoadResult{false, bufferUri}; - })); + .thenInWorkerThread([pBuffer = + &buffer](std::shared_ptr&& + pRequest) { + std::string bufferUri = *pBuffer->uri; + + const IAssetResponse* pResponse = pRequest->response(); + if (!pResponse) { + return ExternalBufferLoadResult{ + false, + bufferUri, + ErrorList::error("Request failed.")}; + } + + uint16_t statusCode = pResponse->statusCode(); + if (statusCode != 0 && + (statusCode < 200 || statusCode >= 300)) { + return ExternalBufferLoadResult{ + false, + bufferUri, + ErrorList::error( + fmt::format("Received status code {}.", statusCode))}; + } + + pBuffer->uri = std::nullopt; + pBuffer->cesium.data = std::vector( + pResponse->data().begin(), + pResponse->data().end()); + return ExternalBufferLoadResult{true, bufferUri, ErrorList()}; + })); } } From ce212af3782d778a8bb5538fcd080202a4b99b43 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 16:01:34 +1100 Subject: [PATCH 68/81] Use brace initialization for base class. --- CesiumGltfReader/src/GltfReader.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index f2a5fcbc1..f045e7890 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -549,8 +549,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( const std::vector& headers) -> SharedFuture> { NetworkImageAssetDescriptor assetKey{ - uri, - headers, + {uri, headers}, options.ktx2TranscodeTargets}; if (options.pSharedAssets == nullptr || From 1eee18f71ceca783ce00f234a6a212a960ae2f61 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 20:56:04 +1100 Subject: [PATCH 69/81] Fix recursive mutex locking bug. --- .../include/CesiumAsync/IDepotOwningAsset.h | 27 +++- CesiumAsync/include/CesiumAsync/SharedAsset.h | 48 ++++--- .../include/CesiumAsync/SharedAssetDepot.h | 65 ++++++--- CesiumAsync/test/TestSharedAssetDepot.cpp | 125 ++++++++++++++++++ 4 files changed, 221 insertions(+), 44 deletions(-) create mode 100644 CesiumAsync/test/TestSharedAssetDepot.cpp diff --git a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h b/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h index 1b02211c7..08962d34d 100644 --- a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h +++ b/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h @@ -4,6 +4,8 @@ namespace CesiumAsync { /** * @brief An interface representing the depot that owns a {@link SharedAsset}. + * This interface is an implementation detail of the shared asset system and + * should not be used directly. * * {@link SharedAsset} has a pointer to the asset depot that owns it using this * interface, rather than a complete {@link SharedAssetDepot}, in order to @@ -13,8 +15,29 @@ namespace CesiumAsync { template class IDepotOwningAsset { public: virtual ~IDepotOwningAsset() {} - virtual void markDeletionCandidate(const TAssetType& asset) = 0; - virtual void unmarkDeletionCandidate(const TAssetType& asset) = 0; + + /** + * @brief Marks the given asset as a candidate for deletion. + * Should only be called by {@link SharedAsset}. May be called from any thread. + * + * @param asset The asset to mark for deletion. + * @param threadOwnsDepotLock True if the calling thread already owns the + * depot lock; otherwise, false. + */ + virtual void + markDeletionCandidate(const TAssetType& asset, bool threadOwnsDepotLock) = 0; + + /** + * @brief Unmarks the given asset as a candidate for deletion. + * Should only be called by {@link SharedAsset}. May be called from any thread. + * + * @param asset The asset to unmark for deletion. + * @param threadOwnsDepotLock True if the calling thread already owns the + * depot lock; otherwise, false. + */ + virtual void unmarkDeletionCandidate( + const TAssetType& asset, + bool threadOwnsDepotLock) = 0; }; } // namespace CesiumAsync diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 3a85b72f5..7c1ed0bd6 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -54,12 +54,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { * {@link CesiumUtility::IntrusivePointer} instead of calling this method * directly. */ - void addReference() const /*noexcept*/ { - const int32_t prevReferences = this->_referenceCount++; - if (this->_pDepot && prevReferences <= 0) { - this->_pDepot->unmarkDeletionCandidate(*static_cast(this)); - } - } + void addReference() const noexcept { this->addReference(false); } /** * @brief Removes a counted reference from this object. When the last @@ -67,20 +62,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { * {@link CesiumUtility::IntrusivePointer} instead of calling this method * directly. */ - void releaseReference() const /*noexcept*/ { - CESIUM_ASSERT(this->_referenceCount > 0); - const int32_t references = --this->_referenceCount; - if (references == 0) { - IDepotOwningAsset* pDepot = this->_pDepot; - if (pDepot) { - // Let the depot manage this object's lifetime. - pDepot->markDeletionCandidate(*static_cast(this)); - } else { - // No depot, so destroy this object directly. - delete static_cast(this); - } - } - } + void releaseReference() const noexcept { this->releaseReference(false); } /** * @brief Gets the shared asset depot that owns this asset, or nullptr if this @@ -129,6 +111,32 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { } private: + void addReference(bool threadOwnsDepotLock) const noexcept { + const int32_t prevReferences = this->_referenceCount++; + if (this->_pDepot && prevReferences <= 0) { + this->_pDepot->unmarkDeletionCandidate( + *static_cast(this), + threadOwnsDepotLock); + } + } + + void releaseReference(bool threadOwnsDepotLock) const noexcept { + CESIUM_ASSERT(this->_referenceCount > 0); + const int32_t references = --this->_referenceCount; + if (references == 0) { + IDepotOwningAsset* pDepot = this->_pDepot; + if (pDepot) { + // Let the depot manage this object's lifetime. + pDepot->markDeletionCandidate( + *static_cast(this), + threadOwnsDepotLock); + } else { + // No depot, so destroy this object directly. + delete static_cast(this); + } + } + } + mutable std::atomic _referenceCount{0}; IDepotOwningAsset* _pDepot{nullptr}; diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index c1e8012fe..2a2579537 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -126,8 +126,8 @@ class CESIUMASYNC_API SharedAssetDepot // Asset is currently loading. return *entry.maybePendingAsset; } else { - // Asset is already loaded (or failed to load). - return asyncSystem.createResolvedFuture(entry.toResult()).share(); + return asyncSystem.createResolvedFuture(entry.toResultUnderLock()) + .share(); } } @@ -173,7 +173,7 @@ class CESIUMASYNC_API SharedAssetDepot // stays alive, too. pDepot->_pKeepAlive = pDepot; - return pEntry->toResult(); + return pEntry->toResultUnderLock(); }); SharedFuture> sharedFuture = @@ -235,12 +235,24 @@ class CESIUMASYNC_API SharedAssetDepot void operator=(const SharedAssetDepot& other) = delete; /** - * Marks the given asset as a candidate for deletion. + * @brief Marks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. May be called from any thread. + * + * @param asset The asset to mark for deletion. + * @param threadOwnsDepotLock True if the calling thread already owns the + * depot lock; otherwise, false. */ - void markDeletionCandidate(const TAssetType& asset) override { - std::lock_guard lock(this->_mutex); + void markDeletionCandidate(const TAssetType& asset, bool threadOwnsDepotLock) + override { + if (threadOwnsDepotLock) { + this->markDeletionCandidateUnderLock(asset); + } else { + std::lock_guard lock(this->_mutex); + this->markDeletionCandidateUnderLock(asset); + } + } + void markDeletionCandidateUnderLock(const TAssetType& asset) { auto it = this->_assetsByPointer.find(const_cast(&asset)); CESIUM_ASSERT(it != this->_assetsByPointer.end()); if (it == this->_assetsByPointer.end()) { @@ -288,12 +300,25 @@ class CESIUMASYNC_API SharedAssetDepot } /** - * Unmarks the given asset as a candidate for deletion. + * @brief Unmarks the given asset as a candidate for deletion. * Should only be called by {@link SharedAsset}. May be called from any thread. + * + * @param asset The asset to unmark for deletion. + * @param threadOwnsDepotLock True if the calling thread already owns the + * depot lock; otherwise, false. */ - void unmarkDeletionCandidate(const TAssetType& asset) override { - std::lock_guard lock(this->_mutex); + void unmarkDeletionCandidate( + const TAssetType& asset, + bool threadOwnsDepotLock) override { + if (threadOwnsDepotLock) { + this->unmarkDeletionCandidateUnderLock(asset); + } else { + std::lock_guard lock(this->_mutex); + this->unmarkDeletionCandidateUnderLock(asset); + } + } + void unmarkDeletionCandidateUnderLock(const TAssetType& asset) { auto it = this->_assetsByPointer.find(const_cast(&asset)); CESIUM_ASSERT(it != this->_assetsByPointer.end()); if (it == this->_assetsByPointer.end()) { @@ -373,19 +398,15 @@ class CESIUMASYNC_API SharedAssetDepot */ CesiumUtility::DoublyLinkedListPointers deletionListPointers; - CesiumUtility::ResultPointer toResult() const { - return CesiumUtility::ResultPointer( - pAsset.get(), - errorsAndWarnings); - } - - SharedFuture> - toFuture(const AsyncSystem& asyncSystem) const { - if (this->maybePendingAsset) { - return *this->maybePendingAsset; - } else { - return asyncSystem.createResolvedFuture(this->toResult()).share(); - } + CesiumUtility::ResultPointer toResultUnderLock() const { + // This method is called while the calling thread already owns the depot + // mutex. So we must take care not to lock it again, which could happen if + // the asset is currently unreferenced and we naively create an + // IntrusivePointer for it. + pAsset->addReference(true); + CesiumUtility::IntrusivePointer p = pAsset.get(); + pAsset->releaseReference(true); + return CesiumUtility::ResultPointer(p, errorsAndWarnings); } }; diff --git a/CesiumAsync/test/TestSharedAssetDepot.cpp b/CesiumAsync/test/TestSharedAssetDepot.cpp new file mode 100644 index 000000000..1e5e71413 --- /dev/null +++ b/CesiumAsync/test/TestSharedAssetDepot.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include + +#include + +using namespace CesiumAsync; +using namespace CesiumNativeTests; +using namespace CesiumUtility; + +namespace { + +class TestAsset : public SharedAsset { +public: + std::string someValue; + + int64_t getSizeBytes() const { return this->someValue.size(); } +}; + +IntrusivePointer> createDepot() { + return new SharedAssetDepot( + [](const AsyncSystem& asyncSystem, + const std::shared_ptr& /* pAssetAccessor */, + const std::string& assetKey) { + IntrusivePointer p = new TestAsset(); + p->someValue = assetKey; + return asyncSystem.createResolvedFuture(ResultPointer(p)); + }); +} + +} // namespace + +TEST_CASE("SharedAssetDepot") { + std::shared_ptr pTaskProcessor = + std::make_shared(); + AsyncSystem asyncSystem(pTaskProcessor); + + SECTION("getOrCreate can create assets") { + auto pDepot = createDepot(); + + ResultPointer assetOne = + pDepot->getOrCreate(asyncSystem, nullptr, "one").waitInMainThread(); + + REQUIRE(assetOne.pValue != nullptr); + } + + SECTION("getOrCreate returns the same asset when called a second time with " + "the same key") { + auto pDepot = createDepot(); + + auto futureOne = pDepot->getOrCreate(asyncSystem, nullptr, "one"); + auto futureTwo = pDepot->getOrCreate(asyncSystem, nullptr, "one"); + + ResultPointer assetOne = futureOne.waitInMainThread(); + ResultPointer assetTwo = futureTwo.waitInMainThread(); + + REQUIRE(assetOne.pValue != nullptr); + CHECK(assetOne.pValue == assetTwo.pValue); + } + + SECTION("unreferenced assets become inactive") { + auto pDepot = createDepot(); + + ResultPointer assetOne = + pDepot->getOrCreate(asyncSystem, nullptr, "one").waitInMainThread(); + + CHECK(pDepot->getAssetCount() == 1); + CHECK(pDepot->getActiveAssetCount() == 1); + CHECK(pDepot->getInactiveAssetCount() == 0); + + assetOne.pValue.reset(); + + CHECK(pDepot->getAssetCount() == 1); + CHECK(pDepot->getActiveAssetCount() == 0); + CHECK(pDepot->getInactiveAssetCount() == 1); + } + + SECTION("re-referenced assets become active again") { + auto pDepot = createDepot(); + + ResultPointer assetOne = + pDepot->getOrCreate(asyncSystem, nullptr, "one").waitInMainThread(); + + CHECK(pDepot->getAssetCount() == 1); + CHECK(pDepot->getActiveAssetCount() == 1); + CHECK(pDepot->getInactiveAssetCount() == 0); + + assetOne.pValue.reset(); + + CHECK(pDepot->getAssetCount() == 1); + CHECK(pDepot->getActiveAssetCount() == 0); + CHECK(pDepot->getInactiveAssetCount() == 1); + + ResultPointer assetTwo = + pDepot->getOrCreate(asyncSystem, nullptr, "one").waitInMainThread(); + + CHECK(pDepot->getAssetCount() == 1); + CHECK(pDepot->getActiveAssetCount() == 1); + CHECK(pDepot->getInactiveAssetCount() == 0); + } + + SECTION("inactive assets are deleted when size threshold is exceeded") { + auto pDepot = createDepot(); + + pDepot->inactiveAssetSizeLimitBytes = std::string("one").size() + 1; + + ResultPointer assetOne = + pDepot->getOrCreate(asyncSystem, nullptr, "one").waitInMainThread(); + ResultPointer assetTwo = + pDepot->getOrCreate(asyncSystem, nullptr, "two").waitInMainThread(); + + assetOne.pValue.reset(); + + CHECK(pDepot->getAssetCount() == 2); + CHECK(pDepot->getActiveAssetCount() == 1); + CHECK(pDepot->getInactiveAssetCount() == 1); + + assetTwo.pValue.reset(); + + CHECK(pDepot->getAssetCount() == 1); + CHECK(pDepot->getActiveAssetCount() == 0); + CHECK(pDepot->getInactiveAssetCount() == 1); + } +} From 323de96de28f80fc54f4331aeaba59ff03f09648 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 21:11:53 +1100 Subject: [PATCH 70/81] Fix clang warning. --- CesiumAsync/test/TestSharedAssetDepot.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumAsync/test/TestSharedAssetDepot.cpp b/CesiumAsync/test/TestSharedAssetDepot.cpp index 1e5e71413..86102312a 100644 --- a/CesiumAsync/test/TestSharedAssetDepot.cpp +++ b/CesiumAsync/test/TestSharedAssetDepot.cpp @@ -15,7 +15,7 @@ class TestAsset : public SharedAsset { public: std::string someValue; - int64_t getSizeBytes() const { return this->someValue.size(); } + int64_t getSizeBytes() const { return int64_t(this->someValue.size()); } }; IntrusivePointer> createDepot() { From a9d6c6a316710a8387d1dcb6445deac1c27e3a44 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 29 Oct 2024 21:17:07 +1100 Subject: [PATCH 71/81] Fix another clang warning. --- CesiumAsync/test/TestSharedAssetDepot.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CesiumAsync/test/TestSharedAssetDepot.cpp b/CesiumAsync/test/TestSharedAssetDepot.cpp index 86102312a..accd6421c 100644 --- a/CesiumAsync/test/TestSharedAssetDepot.cpp +++ b/CesiumAsync/test/TestSharedAssetDepot.cpp @@ -103,7 +103,8 @@ TEST_CASE("SharedAssetDepot") { SECTION("inactive assets are deleted when size threshold is exceeded") { auto pDepot = createDepot(); - pDepot->inactiveAssetSizeLimitBytes = std::string("one").size() + 1; + pDepot->inactiveAssetSizeLimitBytes = + int64_t(std::string("one").size() + 1); ResultPointer assetOne = pDepot->getOrCreate(asyncSystem, nullptr, "one").waitInMainThread(); From c654653062dfb633cedd0903563cce18c5999e63 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 29 Oct 2024 15:16:22 -0400 Subject: [PATCH 72/81] Fix small typo --- CesiumUtility/include/CesiumUtility/DoublyLinkedList.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h index af6715e68..83099ce31 100644 --- a/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h +++ b/CesiumUtility/include/CesiumUtility/DoublyLinkedList.h @@ -245,9 +245,8 @@ class DoublyLinkedListAdvanced final { /** * @brief Determines if this list contains a given node in constant time. In * order to avoid a full list scan, this method assumes that if the node has - * any next or previous node, then it is contained, then it is contained in - * this list. Do not use this method to determine which of multiple lists - * contain this node. + * any next or previous node, then it is contained in this list. Do not use + * this method to determine which of multiple lists contain this node. * * @param node The node to check. * @return True if this node is the head of the list, or if the node has next From 33d494863457adf5187b07639fff44ef44020640 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 09:40:04 +1100 Subject: [PATCH 73/81] Image::pCesium -> pAsset --- Cesium3DTilesSelection/src/Tile.cpp | 4 +- .../src/TilesetContentManager.cpp | 6 +- CesiumGltf/include/CesiumGltf/Image.h | 7 +- .../include/CesiumGltf/PropertyTextureView.h | 2 +- CesiumGltf/src/PropertyTextureView.cpp | 2 +- CesiumGltf/src/TextureView.cpp | 2 +- CesiumGltf/test/TestFeatureIdTextureView.cpp | 188 +++++++++--------- CesiumGltf/test/TestPropertyTextureView.cpp | 56 +++--- CesiumGltfReader/src/GltfReader.cpp | 6 +- CesiumGltfReader/src/decodeDataUrls.cpp | 2 +- CesiumGltfReader/test/TestGltfReader.cpp | 10 +- .../src/QuantizedMeshLoader.cpp | 14 +- .../test/TestAddRasterOverlayToGltf.cpp | 4 +- 13 files changed, 151 insertions(+), 152 deletions(-) diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index a9eefaadd..d48fdefe2 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -175,8 +175,8 @@ int64_t Tile::computeByteSize() const noexcept { // sizeBytes is set in TilesetContentManager::ContentKindSetter, if not // sooner (e.g., by the renderer implementation). - if (image.pCesium) { - bytes += image.pCesium->sizeBytes; + if (image.pAsset) { + bytes += image.pAsset->sizeBytes; } } } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index d4bc7ed0d..3f953f7f7 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -49,15 +49,15 @@ struct ContentKindSetter { void operator()(CesiumGltf::Model&& model) { for (CesiumGltf::Image& image : model.images) { - if (!image.pCesium) + if (!image.pAsset) continue; // If the image size hasn't been overridden, store the pixelData // size now. We'll be adding this number to our total memory usage soon, // and remove it when the tile is later unloaded, and we must use // the same size in each case. - if (image.pCesium->sizeBytes < 0) { - image.pCesium->sizeBytes = int64_t(image.pCesium->pixelData.size()); + if (image.pAsset->sizeBytes < 0) { + image.pAsset->sizeBytes = int64_t(image.pAsset->pixelData.size()); } } diff --git a/CesiumGltf/include/CesiumGltf/Image.h b/CesiumGltf/include/CesiumGltf/Image.h index 3c4eae2cd..a27bf02cc 100644 --- a/CesiumGltf/include/CesiumGltf/Image.h +++ b/CesiumGltf/include/CesiumGltf/Image.h @@ -9,10 +9,9 @@ namespace CesiumGltf { /** @copydoc ImageSpec */ struct CESIUMGLTF_API Image final : public ImageSpec { /** - * @brief Holds properties that are specific to the glTF loader rather than - * part of the glTF spec. When an image is loaded from a URL, multiple `Image` - * instances may all point to the same `ImageAsset` instance. + * @brief The loaded image asset. When an image is loaded from a URL, multiple + * `Image` instances may all point to the same `ImageAsset` instance. */ - CesiumUtility::IntrusivePointer pCesium; + CesiumUtility::IntrusivePointer pAsset; }; } // namespace CesiumGltf diff --git a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h index 42a18619e..4b535c7b2 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h @@ -749,7 +749,7 @@ class PropertyTextureView { } const CesiumUtility::IntrusivePointer& pImage = - _pModel->images[imageIndex].pCesium; + _pModel->images[imageIndex].pAsset; const std::vector& channels = propertyTextureProperty.channels; status = checkChannels(channels, *pImage); diff --git a/CesiumGltf/src/PropertyTextureView.cpp b/CesiumGltf/src/PropertyTextureView.cpp index 16ad1fae4..de2634b7b 100644 --- a/CesiumGltf/src/PropertyTextureView.cpp +++ b/CesiumGltf/src/PropertyTextureView.cpp @@ -81,7 +81,7 @@ PropertyTextureView::checkImage(const int32_t imageIndex) const noexcept { } const CesiumUtility::IntrusivePointer& pImage = - _pModel->images[static_cast(imageIndex)].pCesium; + _pModel->images[static_cast(imageIndex)].pAsset; if (pImage->width < 1 || pImage->height < 1) { return PropertyTexturePropertyViewStatus::ErrorEmptyImage; diff --git a/CesiumGltf/src/TextureView.cpp b/CesiumGltf/src/TextureView.cpp index 84d1507cc..dee12ce1d 100644 --- a/CesiumGltf/src/TextureView.cpp +++ b/CesiumGltf/src/TextureView.cpp @@ -41,7 +41,7 @@ TextureView::TextureView( return; } - this->_pImage = model.images[static_cast(texture.source)].pCesium; + this->_pImage = model.images[static_cast(texture.source)].pAsset; if (this->_pImage->width < 1 || this->_pImage->height < 1) { this->_textureViewStatus = TextureViewStatus::ErrorEmptyImage; return; diff --git a/CesiumGltf/test/TestFeatureIdTextureView.cpp b/CesiumGltf/test/TestFeatureIdTextureView.cpp index 3acd813ac..f94480f86 100644 --- a/CesiumGltf/test/TestFeatureIdTextureView.cpp +++ b/CesiumGltf/test/TestFeatureIdTextureView.cpp @@ -75,9 +75,9 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with empty image") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 0; - image.pCesium->height = 0; + image.pAsset.emplace(); + image.pAsset->width = 0; + image.pAsset->height = 0; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -108,10 +108,10 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with too many bytes " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; - image.pCesium->bytesPerChannel = 2; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; + image.pAsset->bytesPerChannel = 2; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -144,9 +144,9 @@ TEST_CASE( sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -177,9 +177,9 @@ TEST_CASE( sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -210,9 +210,9 @@ TEST_CASE("Test FeatureIdTextureView on feature ID texture with out of range " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -242,9 +242,9 @@ TEST_CASE("Test FeatureIdTextureView on valid feature ID texture") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -275,9 +275,9 @@ TEST_CASE("Test FeatureIdTextureView with applyKhrTextureTransformExtension = " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -325,9 +325,9 @@ TEST_CASE("Test FeatureIdTextureView with applyKhrTextureTransformExtension = " sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -379,14 +379,14 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; - image.pCesium->pixelData.resize(featureIDs.size()); + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; + image.pAsset->pixelData.resize(featureIDs.size()); std::memcpy( - image.pCesium->pixelData.data(), + image.pAsset->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -412,14 +412,14 @@ TEST_CASE("Test FeatureIdTextureView with makeImageCopy = true") { // Clear the original image data. std::vector emptyData; - image.pCesium->pixelData.swap(emptyData); + image.pAsset->pixelData.swap(emptyData); const ImageAsset* pImage = view.getImage(); REQUIRE(pImage); - REQUIRE(pImage->width == image.pCesium->width); - REQUIRE(pImage->height == image.pCesium->height); - REQUIRE(pImage->channels == image.pCesium->channels); - REQUIRE(pImage->bytesPerChannel == image.pCesium->bytesPerChannel); + REQUIRE(pImage->width == image.pAsset->width); + REQUIRE(pImage->height == image.pAsset->height); + REQUIRE(pImage->channels == image.pAsset->channels); + REQUIRE(pImage->bytesPerChannel == image.pAsset->bytesPerChannel); REQUIRE(pImage->pixelData.size() == featureIDs.size()); } @@ -432,9 +432,9 @@ TEST_CASE("Test getFeatureID on invalid feature ID texture view") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 1; - image.pCesium->height = 1; + image.pAsset.emplace(); + image.pAsset->width = 1; + image.pAsset->height = 1; Texture& texture = model.textures.emplace_back(); texture.sampler = 0; @@ -467,14 +467,14 @@ TEST_CASE("Test getFeatureID on valid feature ID texture view") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; - image.pCesium->pixelData.resize(featureIDs.size()); + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; + image.pAsset->pixelData.resize(featureIDs.size()); std::memcpy( - image.pCesium->pixelData.data(), + image.pAsset->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -513,14 +513,14 @@ TEST_CASE("Test getFeatureID on view with applyKhrTextureTransformExtension = " std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; - image.pCesium->pixelData.resize(featureIDs.size()); + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; + image.pAsset->pixelData.resize(featureIDs.size()); std::memcpy( - image.pCesium->pixelData.data(), + image.pAsset->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -565,14 +565,14 @@ TEST_CASE( std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; - image.pCesium->pixelData.resize(featureIDs.size()); + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; + image.pAsset->pixelData.resize(featureIDs.size()); std::memcpy( - image.pCesium->pixelData.data(), + image.pAsset->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -622,14 +622,14 @@ TEST_CASE("Test getFeatureId on view with makeImageCopy = true") { sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; - image.pCesium->pixelData.resize(featureIDs.size()); + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; + image.pAsset->pixelData.resize(featureIDs.size()); std::memcpy( - image.pCesium->pixelData.data(), + image.pAsset->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -655,7 +655,7 @@ TEST_CASE("Test getFeatureId on view with makeImageCopy = true") { // Clear the original image data. std::vector emptyData; - image.pCesium->pixelData.swap(emptyData); + image.pAsset->pixelData.swap(emptyData); REQUIRE(view.getFeatureID(0, 0) == 1); REQUIRE(view.getFeatureID(1, 0) == 2); @@ -674,14 +674,14 @@ TEST_CASE("Test getFeatureID rounds to nearest pixel") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; - image.pCesium->pixelData.resize(featureIDs.size()); + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; + image.pAsset->pixelData.resize(featureIDs.size()); std::memcpy( - image.pCesium->pixelData.data(), + image.pAsset->pixelData.data(), featureIDs.data(), featureIDs.size()); @@ -719,13 +719,13 @@ TEST_CASE("Test getFeatureID clamps values") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; - auto& data = image.pCesium->pixelData; + auto& data = image.pAsset->pixelData; data.resize(featureIDs.size() * sizeof(uint8_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); @@ -763,13 +763,13 @@ TEST_CASE("Test getFeatureID handles multiple channels") { std::vector featureIDs{260, 512, 8, 17}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 2; - image.pCesium->bytesPerChannel = 1; + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 2; + image.pAsset->bytesPerChannel = 1; - auto& data = image.pCesium->pixelData; + auto& data = image.pAsset->pixelData; data.resize(featureIDs.size() * sizeof(uint16_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); @@ -805,13 +805,13 @@ TEST_CASE("Check FeatureIdTextureView sampling with different wrap values") { std::vector featureIDs{1, 2, 0, 7}; Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = 2; - image.pCesium->height = 2; - image.pCesium->channels = 1; - image.pCesium->bytesPerChannel = 1; + image.pAsset.emplace(); + image.pAsset->width = 2; + image.pAsset->height = 2; + image.pAsset->channels = 1; + image.pAsset->bytesPerChannel = 1; - auto& data = image.pCesium->pixelData; + auto& data = image.pAsset->pixelData; data.resize(featureIDs.size() * sizeof(uint8_t)); std::memcpy(data.data(), featureIDs.data(), data.size()); diff --git a/CesiumGltf/test/TestPropertyTextureView.cpp b/CesiumGltf/test/TestPropertyTextureView.cpp index 81325270b..5aa773bf2 100644 --- a/CesiumGltf/test/TestPropertyTextureView.cpp +++ b/CesiumGltf/test/TestPropertyTextureView.cpp @@ -19,13 +19,13 @@ void addTextureToModel( int32_t channels, const std::vector& data) { Image& image = model.images.emplace_back(); - image.pCesium.emplace(); - image.pCesium->width = width; - image.pCesium->height = height; - image.pCesium->channels = channels; - image.pCesium->bytesPerChannel = 1; + image.pAsset.emplace(); + image.pAsset->width = width; + image.pAsset->height = height; + image.pAsset->channels = channels; + image.pAsset->bytesPerChannel = 1; - std::vector& imageData = image.pCesium->pixelData; + std::vector& imageData = image.pAsset->pixelData; imageData.resize(data.size()); std::memcpy(imageData.data(), data.data(), data.size()); @@ -249,7 +249,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pAsset->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -309,7 +309,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].pCesium->channels = 2; + model.images[imageIndex].pAsset->channels = 2; propertyTextureProperty.channels = {0, 1}; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); @@ -337,7 +337,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].pCesium->bytesPerChannel = 2; + model.images[imageIndex].pAsset->bytesPerChannel = 2; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -346,7 +346,7 @@ TEST_CASE("Test scalar PropertyTextureProperty") { } SECTION("Empty image") { - model.images[imageIndex].pCesium->width = 0; + model.images[imageIndex].pAsset->width = 0; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -494,7 +494,7 @@ TEST_CASE("Test scalar PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pAsset->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -557,7 +557,7 @@ TEST_CASE("Test scalar PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].pCesium->channels = 2; + model.images[imageIndex].pAsset->channels = 2; propertyTextureProperty.channels = {0, 1}; PropertyTexturePropertyView uint8Property = view.getPropertyView("TestClassProperty"); @@ -690,7 +690,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pAsset->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -751,7 +751,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].pCesium->channels = 4; + model.images[imageIndex].pAsset->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); @@ -770,7 +770,7 @@ TEST_CASE("Test vecN PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].pCesium->bytesPerChannel = 2; + model.images[imageIndex].pAsset->bytesPerChannel = 2; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); REQUIRE( @@ -904,7 +904,7 @@ TEST_CASE("Test vecN PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pAsset->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -974,7 +974,7 @@ TEST_CASE("Test vecN PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].pCesium->channels = 4; + model.images[imageIndex].pAsset->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView u8vec2Property = view.getPropertyView("TestClassProperty"); @@ -1152,7 +1152,7 @@ TEST_CASE("Test array PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pAsset->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -1221,7 +1221,7 @@ TEST_CASE("Test array PropertyTextureProperty") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].pCesium->channels = 4; + model.images[imageIndex].pAsset->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView> uint8ArrayProperty = view.getPropertyView>("TestClassProperty"); @@ -1240,7 +1240,7 @@ TEST_CASE("Test array PropertyTextureProperty") { } SECTION("Invalid bytes per channel") { - model.images[imageIndex].pCesium->bytesPerChannel = 2; + model.images[imageIndex].pAsset->bytesPerChannel = 2; PropertyTexturePropertyView> uint8ArrayProperty = view.getPropertyView>("TestClassProperty"); REQUIRE( @@ -1423,7 +1423,7 @@ TEST_CASE("Test array PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap(emptyData); + model.images[model.images.size() - 1].pAsset->pixelData.swap(emptyData); std::vector texCoords{ glm::dvec2(0, 0), @@ -1493,7 +1493,7 @@ TEST_CASE("Test array PropertyTextureProperty (normalized)") { } SECTION("Channel and type mismatch") { - model.images[imageIndex].pCesium->channels = 4; + model.images[imageIndex].pAsset->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; PropertyTexturePropertyView, true> uint8ArrayProperty = @@ -2245,7 +2245,7 @@ TEST_CASE("Test callback for scalar PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap( + model.images[model.images.size() - 1].pAsset->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2367,7 +2367,7 @@ TEST_CASE("Test callback for scalar PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap( + model.images[model.images.size() - 1].pAsset->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2498,7 +2498,7 @@ TEST_CASE("Test callback for vecN PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap( + model.images[model.images.size() - 1].pAsset->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2630,7 +2630,7 @@ TEST_CASE("Test callback for vecN PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap( + model.images[model.images.size() - 1].pAsset->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2783,7 +2783,7 @@ TEST_CASE("Test callback for array PropertyTextureProperty") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap( + model.images[model.images.size() - 1].pAsset->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { @@ -2953,7 +2953,7 @@ TEST_CASE("Test callback for array PropertyTextureProperty (normalized)") { // Clear the original image data. std::vector emptyData; - model.images[model.images.size() - 1].pCesium->pixelData.swap( + model.images[model.images.size() - 1].pAsset->pixelData.swap( emptyData); for (size_t i = 0; i < expected.size(); ++i) { diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index f045e7890..47f7e02de 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -265,7 +265,7 @@ void postprocess( } // Image has already been decoded - if (image.pCesium && !image.pCesium->pixelData.empty()) { + if (image.pAsset && !image.pAsset->pixelData.empty()) { continue; } @@ -300,7 +300,7 @@ void postprocess( imageResult.errors.begin(), imageResult.errors.end()); if (imageResult.pImage) { - image.pCesium = imageResult.pImage; + image.pAsset = imageResult.pImage; } else { if (image.mimeType) { readGltf.errors.emplace_back( @@ -574,7 +574,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( pImage->uri = std::nullopt; if (loadedImage.pValue) { - pImage->pCesium = loadedImage.pValue; + pImage->pAsset = loadedImage.pValue; return ExternalBufferLoadResult{ true, imageUri, diff --git a/CesiumGltfReader/src/decodeDataUrls.cpp b/CesiumGltfReader/src/decodeDataUrls.cpp index 10af277b5..bbfdba67f 100644 --- a/CesiumGltfReader/src/decodeDataUrls.cpp +++ b/CesiumGltfReader/src/decodeDataUrls.cpp @@ -141,7 +141,7 @@ void decodeDataUrls( continue; } - image.pCesium = imageResult.pImage; + image.pAsset = imageResult.pImage; if (options.clearDecodedDataUrls) { image.uri.reset(); diff --git a/CesiumGltfReader/test/TestGltfReader.cpp b/CesiumGltfReader/test/TestGltfReader.cpp index 18561464d..84b4336c7 100644 --- a/CesiumGltfReader/test/TestGltfReader.cpp +++ b/CesiumGltfReader/test/TestGltfReader.cpp @@ -689,7 +689,7 @@ TEST_CASE("Decodes images with data uris") { REQUIRE(model.images.size() == 1); - const ImageAsset& image = *model.images.front().pCesium; + const ImageAsset& image = *model.images.front().pAsset; CHECK(image.width == 256); CHECK(image.height == 256); @@ -782,9 +782,9 @@ TEST_CASE("GltfReader::loadGltf") { REQUIRE(result.model->images.size() == 1); const CesiumGltf::Image& image = result.model->images[0]; - CHECK(image.pCesium->width == 2048); - CHECK(image.pCesium->height == 2048); - CHECK(image.pCesium->pixelData.size() == 2048 * 2048 * 4); + CHECK(image.pAsset->width == 2048); + CHECK(image.pAsset->height == 2048); + CHECK(image.pAsset->pixelData.size() == 2048 * 2048 * 4); CHECK(!result.model->buffers.empty()); for (const CesiumGltf::Buffer& buffer : result.model->buffers) { @@ -810,7 +810,7 @@ TEST_CASE("GltfReader::loadGltf") { REQUIRE(result.model->images.size() == 1); const CesiumGltf::Image& image = result.model->images[0]; CHECK(image.uri.has_value()); - CHECK(!image.pCesium); + CHECK(!image.pAsset); } } diff --git a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp index ef05516a9..eb2e420b7 100644 --- a/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp +++ b/CesiumQuantizedMeshTerrain/src/QuantizedMeshLoader.cpp @@ -1073,14 +1073,14 @@ static std::vector generateNormals( const size_t waterMaskImageId = model.images.size(); model.images.emplace_back(); CesiumGltf::Image& waterMaskImage = model.images[waterMaskImageId]; - waterMaskImage.pCesium.emplace(); - waterMaskImage.pCesium->width = 256; - waterMaskImage.pCesium->height = 256; - waterMaskImage.pCesium->channels = 1; - waterMaskImage.pCesium->bytesPerChannel = 1; - waterMaskImage.pCesium->pixelData.resize(65536); + waterMaskImage.pAsset.emplace(); + waterMaskImage.pAsset->width = 256; + waterMaskImage.pAsset->height = 256; + waterMaskImage.pAsset->channels = 1; + waterMaskImage.pAsset->bytesPerChannel = 1; + waterMaskImage.pAsset->pixelData.resize(65536); std::memcpy( - waterMaskImage.pCesium->pixelData.data(), + waterMaskImage.pAsset->pixelData.data(), meshView->waterMaskBuffer.data(), 65536); diff --git a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp index 21e591018..065e2bca6 100644 --- a/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp +++ b/CesiumRasterOverlays/test/TestAddRasterOverlayToGltf.cpp @@ -266,8 +266,8 @@ TEST_CASE("Add raster overlay to glTF") { const Model& gltfBack = *resultBack.model; REQUIRE(gltfBack.images.size() == 1); - REQUIRE(gltfBack.images[0].pCesium != nullptr); - CHECK(!gltfBack.images[0].pCesium->pixelData.empty()); + REQUIRE(gltfBack.images[0].pAsset != nullptr); + CHECK(!gltfBack.images[0].pAsset->pixelData.empty()); REQUIRE(!gltfBack.meshes.empty()); REQUIRE(!gltfBack.meshes[0].primitives.empty()); From 8b60ebdd632eb48bd8cd99fc46043fa835b2149d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 11:06:14 +1100 Subject: [PATCH 74/81] Minor tweaks. --- .../IPrepareRendererResources.h | 1 - .../include/Cesium3DTilesSelection/Tileset.h | 2 +- Cesium3DTilesSelection/src/TilesetContentManager.cpp | 12 ++++++------ Cesium3DTilesSelection/src/TilesetContentManager.h | 2 +- Cesium3DTilesSelection/src/TilesetJsonLoader.cpp | 1 - Cesium3DTilesSelection/src/TilesetJsonLoader.h | 2 +- .../src/TilesetSharedAssetSystem.cpp | 2 -- .../include/CesiumGltfReader/GltfReader.h | 2 +- CesiumGltfReader/src/GltfReader.cpp | 6 +++--- 9 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h index b7977fd62..d6654fee3 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/IPrepareRendererResources.h @@ -20,7 +20,6 @@ struct Rectangle; } namespace CesiumGltf { -struct ImageAsset; struct Model; } // namespace CesiumGltf diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index 840d331a8..8bfd9d2ac 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -184,7 +184,7 @@ class CESIUM3DTILESSELECTION_API Tileset final { const RasterOverlayCollection& getOverlays() const noexcept; /** - * @brief Returns the {@link SharedAssetDepot} of this tileset. + * @brief Returns the {@link TilesetSharedAssetSystem} of this tileset. */ TilesetSharedAssetSystem& getSharedAssetSystem() noexcept; diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 3f953f7f7..266644707 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -566,7 +566,7 @@ postProcessContentInWorkerThread( gltfOptions.applyTextureTransform = tileLoadInfo.contentOptions.applyTextureTransform; if (tileLoadInfo.pSharedAssetSystem) { - gltfOptions.pSharedAssets = tileLoadInfo.pSharedAssetSystem; + gltfOptions.pSharedAssetSystem = tileLoadInfo.pSharedAssetSystem; } auto asyncSystem = tileLoadInfo.asyncSystem; @@ -666,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(TilesetSharedAssetSystem::getDefault()), + _pSharedAssetSystem(TilesetSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(TilesetSharedAssetSystem::getDefault()), + _pSharedAssetSystem(TilesetSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssets(TilesetSharedAssetSystem::getDefault()), + _pSharedAssetSystem(TilesetSharedAssetSystem::getDefault()), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -994,7 +994,7 @@ void TilesetContentManager::loadTileContent( this->_externals.pAssetAccessor, this->_externals.pPrepareRendererResources, this->_externals.pLogger, - this->_pSharedAssets, + this->_pSharedAssetSystem, tilesetOptions.contentOptions, tile}; @@ -1239,7 +1239,7 @@ TilesetContentManager::getTilesetCredits() const noexcept { const CesiumUtility::IntrusivePointer& TilesetContentManager::getSharedAssetSystem() const noexcept { - return this->_pSharedAssets; + return this->_pSharedAssetSystem; } int32_t TilesetContentManager::getNumberOfTilesLoading() const noexcept { diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index e3d32b2da..0a4af3831 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -173,7 +173,7 @@ class TilesetContentManager int64_t _tilesDataUsed; // Stores assets that might be shared between tiles. - CesiumUtility::IntrusivePointer _pSharedAssets; + CesiumUtility::IntrusivePointer _pSharedAssetSystem; CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp index dd6ff117e..c9d733511 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp @@ -1005,5 +1005,4 @@ void TilesetJsonLoader::addChildLoader( std::unique_ptr pLoader) { this->_children.emplace_back(std::move(pLoader)); } - } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.h b/Cesium3DTilesSelection/src/TilesetJsonLoader.h index 55a5e8ae4..f70ce9b7c 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.h +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.h @@ -56,7 +56,7 @@ class TilesetJsonLoader : public TilesetContentLoader { private: std::string _baseUrl; CesiumGeospatial::Ellipsoid _ellipsoid; - CesiumUtility::IntrusivePointer _pSharedAssets; + CesiumUtility::IntrusivePointer _pSharedAssetSystem; /** * @brief The axis that was declared as the "up-axis" for glTF content. diff --git a/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp b/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp index 3f6c0806b..95a8d384d 100644 --- a/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp +++ b/Cesium3DTilesSelection/src/TilesetSharedAssetSystem.cpp @@ -1,8 +1,6 @@ #include using namespace Cesium3DTilesSelection; -using namespace CesiumAsync; -using namespace CesiumGltf; using namespace CesiumGltfReader; using namespace CesiumUtility; diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index 26f39f8b5..39137bd0a 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -114,7 +114,7 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * The shared asset system that will be used to store all of the shared assets * that might appear in this glTF. */ - CesiumUtility::IntrusivePointer pSharedAssets = + CesiumUtility::IntrusivePointer pSharedAssetSystem = nullptr; }; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 47f7e02de..011340119 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -552,13 +552,13 @@ void CesiumGltfReader::GltfReader::postprocessGltf( {uri, headers}, options.ktx2TranscodeTargets}; - if (options.pSharedAssets == nullptr || - options.pSharedAssets->pImage == nullptr) { + if (options.pSharedAssetSystem == nullptr || + options.pSharedAssetSystem->pImage == nullptr) { // We don't have a depot, so fetch this asset directly. return assetKey.load(asyncSystem, pAssetAccessor).share(); } else { // We have a depot, so fetch this asset via that depot. - return options.pSharedAssets->pImage->getOrCreate( + return options.pSharedAssetSystem->pImage->getOrCreate( asyncSystem, pAssetAccessor, assetKey); From e216b54a4aec5107b762d63e2bd13623714dbc7c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 12:14:33 +1100 Subject: [PATCH 75/81] More small tweaks. --- CesiumAsync/include/CesiumAsync/SharedAsset.h | 2 +- .../include/CesiumAsync/SharedAssetDepot.h | 25 ++++++++----------- .../include/CesiumGltf/FeatureIdTextureView.h | 8 ------ CesiumGltf/include/CesiumGltf/ImageAsset.h | 4 +-- CesiumGltf/src/PropertyTextureView.cpp | 2 +- CesiumGltf/src/TextureView.cpp | 2 +- 6 files changed, 16 insertions(+), 27 deletions(-) diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumAsync/include/CesiumAsync/SharedAsset.h index 7c1ed0bd6..929886fc9 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumAsync/include/CesiumAsync/SharedAsset.h @@ -30,7 +30,7 @@ template class SharedAssetDepot; * An independent asset isn't affiliated with an asset depot at all. * Its lifetime is controlled exclusively by IntrusivePointer / reference * counting. When the asset's reference count goes to zero, it deletes itself. - * An independent asset's `_pDepot` is nullptr. + * An independent asset's {@link getDepot} returns nullptr. * * **Active Depot Asset** * This is an asset that is owned by an asset depot and that is in use, meaning diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 2a2579537..532297fae 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -103,18 +103,6 @@ class CESIUMASYNC_API SharedAssetDepot const TAssetKey& assetKey) { // We need to take care here to avoid two assets starting to load before the // first asset has added an entry and set its maybePendingAsset field. - - // Calling the factory function while holding the mutex unnecessarily - // limits parallelism. It can even lead to a bug in the scenario where the - // `thenInWorkerThread` continuation is invoked immediately in the current - // thread, before `thenInWorkerThread` itself returns. That would result - // in an attempt to lock the mutex recursively, which is not allowed. - - // So we jump through some hoops here to publish "this thread is working - // on it", then unlock the mutex, and _then_ actually call the factory - // function. - Promise promise = asyncSystem.createPromise(); - std::unique_lock lock(this->_mutex); auto existingIt = this->_assets.find(assetKey); @@ -131,6 +119,17 @@ class CESIUMASYNC_API SharedAssetDepot } } + // Calling the factory function while holding the mutex unnecessarily + // limits parallelism. It can even lead to a bug in the scenario where the + // `thenInWorkerThread` continuation is invoked immediately in the current + // thread, before `thenInWorkerThread` itself returns. That would result + // in an attempt to lock the mutex recursively, which is not allowed. + + // So we jump through some hoops here to publish "this thread is working + // on it", then unlock the mutex, and _then_ actually call the factory + // function. + Promise promise = asyncSystem.createPromise(); + // We haven't loaded or started to load this asset yet. // Let's do that now. CesiumUtility::IntrusivePointer> @@ -440,8 +439,6 @@ class CESIUMASYNC_API SharedAssetDepot // it are dropped. CesiumUtility::IntrusivePointer> _pKeepAlive; - - friend class SharedAsset; }; } // namespace CesiumAsync diff --git a/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h b/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h index 1cfce2c7d..0200ee192 100644 --- a/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h +++ b/CesiumGltf/include/CesiumGltf/FeatureIdTextureView.h @@ -1,17 +1,9 @@ #pragma once #include "CesiumGltf/FeatureIdTexture.h" -#include "CesiumGltf/Image.h" -#include "CesiumGltf/ImageAsset.h" -#include "CesiumGltf/KhrTextureTransform.h" -#include "CesiumGltf/Texture.h" #include "CesiumGltf/TextureView.h" -#include -#include -#include #include -#include namespace CesiumGltf { diff --git a/CesiumGltf/include/CesiumGltf/ImageAsset.h b/CesiumGltf/include/CesiumGltf/ImageAsset.h index f7614c437..5018015e7 100644 --- a/CesiumGltf/include/CesiumGltf/ImageAsset.h +++ b/CesiumGltf/include/CesiumGltf/ImageAsset.h @@ -27,8 +27,8 @@ struct CESIUMGLTF_API ImageAssetMipPosition { }; /** - * @brief Holds {@link Image} properties that are specific to the glTF loader - * rather than part of the glTF spec. + * @brief A 2D image asset, including its pixel data. The image may have + * mipmaps, and it may be encoded in a GPU compression format. */ struct CESIUMGLTF_API ImageAsset final : public CesiumAsync::SharedAsset { diff --git a/CesiumGltf/src/PropertyTextureView.cpp b/CesiumGltf/src/PropertyTextureView.cpp index de2634b7b..c9968f4b0 100644 --- a/CesiumGltf/src/PropertyTextureView.cpp +++ b/CesiumGltf/src/PropertyTextureView.cpp @@ -83,7 +83,7 @@ PropertyTextureView::checkImage(const int32_t imageIndex) const noexcept { const CesiumUtility::IntrusivePointer& pImage = _pModel->images[static_cast(imageIndex)].pAsset; - if (pImage->width < 1 || pImage->height < 1) { + if (!pImage || pImage->width < 1 || pImage->height < 1) { return PropertyTexturePropertyViewStatus::ErrorEmptyImage; } diff --git a/CesiumGltf/src/TextureView.cpp b/CesiumGltf/src/TextureView.cpp index dee12ce1d..39ad5630e 100644 --- a/CesiumGltf/src/TextureView.cpp +++ b/CesiumGltf/src/TextureView.cpp @@ -42,7 +42,7 @@ TextureView::TextureView( } this->_pImage = model.images[static_cast(texture.source)].pAsset; - if (this->_pImage->width < 1 || this->_pImage->height < 1) { + if (!this->_pImage || this->_pImage->width < 1 || this->_pImage->height < 1) { this->_textureViewStatus = TextureViewStatus::ErrorEmptyImage; return; } From e3c3daf6c3e79e5ded8df7fe6ac65bb830ba71a8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 12:49:07 +1100 Subject: [PATCH 76/81] Move SharedAsset to CesiumUtility. This avoids adding a new dependency on CesiumAsync to CesiumGltf. --- CesiumAsync/include/CesiumAsync/SharedAssetDepot.h | 10 ++++++---- CesiumAsync/test/TestSharedAssetDepot.cpp | 2 +- CesiumGltf/CMakeLists.txt | 1 - CesiumGltf/include/CesiumGltf/ImageAsset.h | 4 ++-- .../include/CesiumUtility}/IDepotOwningAsset.h | 10 +++++----- .../include/CesiumUtility}/SharedAsset.h | 14 ++++++++------ 6 files changed, 22 insertions(+), 19 deletions(-) rename {CesiumAsync/include/CesiumAsync => CesiumUtility/include/CesiumUtility}/IDepotOwningAsset.h (81%) rename {CesiumAsync/include/CesiumAsync => CesiumUtility/include/CesiumUtility}/SharedAsset.h (94%) diff --git a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h index 532297fae..236655d03 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h +++ b/CesiumAsync/include/CesiumAsync/SharedAssetDepot.h @@ -3,8 +3,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -17,9 +17,11 @@ #include #include -namespace CesiumAsync { - +namespace CesiumUtility { template class SharedAsset; +} + +namespace CesiumAsync { /** * @brief A depot for {@link SharedAsset} instances, which are potentially shared between multiple objects. @@ -31,7 +33,7 @@ template class CESIUMASYNC_API SharedAssetDepot : public CesiumUtility::ReferenceCountedThreadSafe< SharedAssetDepot>, - public IDepotOwningAsset { + public CesiumUtility::IDepotOwningAsset { public: /** * @brief The maximum total byte usage of assets that have been loaded but are diff --git a/CesiumAsync/test/TestSharedAssetDepot.cpp b/CesiumAsync/test/TestSharedAssetDepot.cpp index accd6421c..474b834f5 100644 --- a/CesiumAsync/test/TestSharedAssetDepot.cpp +++ b/CesiumAsync/test/TestSharedAssetDepot.cpp @@ -1,7 +1,7 @@ -#include #include #include #include +#include #include diff --git a/CesiumGltf/CMakeLists.txt b/CesiumGltf/CMakeLists.txt index 09643b368..825e1cc90 100644 --- a/CesiumGltf/CMakeLists.txt +++ b/CesiumGltf/CMakeLists.txt @@ -53,7 +53,6 @@ target_include_directories( target_link_libraries(CesiumGltf PUBLIC - CesiumAsync CesiumUtility Microsoft.GSL::GSL ) diff --git a/CesiumGltf/include/CesiumGltf/ImageAsset.h b/CesiumGltf/include/CesiumGltf/ImageAsset.h index 5018015e7..864680af8 100644 --- a/CesiumGltf/include/CesiumGltf/ImageAsset.h +++ b/CesiumGltf/include/CesiumGltf/ImageAsset.h @@ -1,8 +1,8 @@ #pragma once -#include "CesiumAsync/SharedAsset.h" #include "CesiumGltf/Ktx2TranscodeTargets.h" #include "CesiumGltf/Library.h" +#include "CesiumUtility/SharedAsset.h" #include #include @@ -31,7 +31,7 @@ struct CESIUMGLTF_API ImageAssetMipPosition { * mipmaps, and it may be encoded in a GPU compression format. */ struct CESIUMGLTF_API ImageAsset final - : public CesiumAsync::SharedAsset { + : public CesiumUtility::SharedAsset { /** * @brief The width of the image in pixels. */ diff --git a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h b/CesiumUtility/include/CesiumUtility/IDepotOwningAsset.h similarity index 81% rename from CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h rename to CesiumUtility/include/CesiumUtility/IDepotOwningAsset.h index 08962d34d..b5a43c82e 100644 --- a/CesiumAsync/include/CesiumAsync/IDepotOwningAsset.h +++ b/CesiumUtility/include/CesiumUtility/IDepotOwningAsset.h @@ -1,6 +1,6 @@ #pragma once -namespace CesiumAsync { +namespace CesiumUtility { /** * @brief An interface representing the depot that owns a {@link SharedAsset}. @@ -8,9 +8,9 @@ namespace CesiumAsync { * should not be used directly. * * {@link SharedAsset} has a pointer to the asset depot that owns it using this - * interface, rather than a complete {@link SharedAssetDepot}, in order to - * "erase" the type of the asset key. This allows SharedAsset to be templatized - * only on the asset type, not on the asset key type. + * interface, rather than a complete {@link CesiumAsync::SharedAssetDepot}, in + * order to "erase" the type of the asset key. This allows SharedAsset to be + * templatized only on the asset type, not on the asset key type. */ template class IDepotOwningAsset { public: @@ -40,4 +40,4 @@ template class IDepotOwningAsset { bool threadOwnsDepotLock) = 0; }; -} // namespace CesiumAsync +} // namespace CesiumUtility diff --git a/CesiumAsync/include/CesiumAsync/SharedAsset.h b/CesiumUtility/include/CesiumUtility/SharedAsset.h similarity index 94% rename from CesiumAsync/include/CesiumAsync/SharedAsset.h rename to CesiumUtility/include/CesiumUtility/SharedAsset.h index 929886fc9..907e63a29 100644 --- a/CesiumAsync/include/CesiumAsync/SharedAsset.h +++ b/CesiumUtility/include/CesiumUtility/SharedAsset.h @@ -1,15 +1,17 @@ #pragma once -#include -#include #include #include +#include +#include #include namespace CesiumAsync { - template class SharedAssetDepot; +} + +namespace CesiumUtility { /** * @brief An asset that is potentially shared between multiple objects, such as @@ -47,7 +49,7 @@ template class SharedAssetDepot; * @endparblock */ template -class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { +class CESIUMUTILITY_API SharedAsset : public CesiumUtility::ExtensibleObject { public: /** * @brief Adds a counted reference to this object. Use @@ -142,7 +144,7 @@ class CESIUMASYNC_API SharedAsset : public CesiumUtility::ExtensibleObject { // To allow the depot to modify _pDepot. template - friend class SharedAssetDepot; + friend class CesiumAsync::SharedAssetDepot; }; -} // namespace CesiumAsync +} // namespace CesiumUtility From 9dfe894056a7b5e93ad9862db6ec0f3abbb7f6fb Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 13:15:03 +1100 Subject: [PATCH 77/81] ImageDecoder cleanup. --- CesiumGltf/CMakeLists.txt | 24 +++---- .../include/CesiumGltfReader/GltfReader.h | 24 +++---- .../include/CesiumGltfReader/ImageDecoder.h | 2 + CesiumGltfReader/src/GltfReader.cpp | 24 ++++--- CesiumGltfReader/src/decodeDataUrls.cpp | 10 +-- CesiumGltfReader/src/decodeDataUrls.h | 1 - CesiumGltfReader/test/TestGltfReader.cpp | 61 ---------------- CesiumGltfReader/test/TestImageDecoder.cpp | 72 +++++++++++++++++++ .../RasterOverlayTileProvider.h | 2 - .../src/RasterOverlayTileProvider.cpp | 9 +-- 10 files changed, 118 insertions(+), 111 deletions(-) create mode 100644 CesiumGltfReader/test/TestImageDecoder.cpp diff --git a/CesiumGltf/CMakeLists.txt b/CesiumGltf/CMakeLists.txt index 825e1cc90..2beddb9ad 100644 --- a/CesiumGltf/CMakeLists.txt +++ b/CesiumGltf/CMakeLists.txt @@ -22,8 +22,8 @@ cesium_glob_files(CESIUM_GLTF_TEST_HEADERS set_target_properties(CesiumGltf PROPERTIES - TEST_SOURCES "${CESIUM_GLTF_TEST_SOURCES}" - TEST_HEADERS "${CESIUM_GLTF_TEST_HEADERS}" + TEST_SOURCES "${CESIUM_GLTF_TEST_SOURCES}" + TEST_HEADERS "${CESIUM_GLTF_TEST_HEADERS}" ) set_target_properties(CesiumGltf @@ -34,27 +34,27 @@ set_target_properties(CesiumGltf target_sources( CesiumGltf PRIVATE - ${CESIUM_GLTF_SOURCES} - ${CESIUM_GLTF_HEADERS} + ${CESIUM_GLTF_SOURCES} + ${CESIUM_GLTF_HEADERS} PUBLIC - ${CESIUM_GLTF_PUBLIC_HEADERS} + ${CESIUM_GLTF_PUBLIC_HEADERS} ) target_include_directories( CesiumGltf SYSTEM PUBLIC - ${CMAKE_CURRENT_LIST_DIR}/include/ - ${CMAKE_CURRENT_LIST_DIR}/generated/include + ${CMAKE_CURRENT_LIST_DIR}/include/ + ${CMAKE_CURRENT_LIST_DIR}/generated/include PRIVATE - ${CESIUM_NATIVE_RAPIDJSON_INCLUDE_DIR} - ${CMAKE_CURRENT_LIST_DIR}/src - ${CMAKE_CURRENT_LIST_DIR}/generated/src + ${CESIUM_NATIVE_RAPIDJSON_INCLUDE_DIR} + ${CMAKE_CURRENT_LIST_DIR}/src + ${CMAKE_CURRENT_LIST_DIR}/generated/src ) target_link_libraries(CesiumGltf PUBLIC - CesiumUtility - Microsoft.GSL::GSL + CesiumUtility + Microsoft.GSL::GSL ) target_compile_definitions(CesiumGltf PRIVATE GLM_ENABLE_EXPERIMENTAL) diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index 39137bd0a..2f14222ad 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -198,28 +198,22 @@ class CESIUMGLTFREADER_API GltfReader { GltfReaderResult&& result); /** - * Reads an Image from a buffer. + * @brief Reads an Image from a buffer. * @deprecated Use {@link ImageDecoder::readImage} instead. */ - static ImageReaderResult readImage( + [[deprecated( + "Use ImageDecoder::readImage instead.")]] static ImageReaderResult + readImage( const gsl::span& data, - const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets) { - return ImageDecoder::readImage(data, ktx2TranscodeTargets); - } + const CesiumGltf::Ktx2TranscodeTargets& ktx2TranscodeTargets); /** * @brief Generate mipmaps for this image. - * - * Does nothing if mipmaps already exist or the compressedPixelFormat is not - * GpuCompressedPixelFormat::NONE. - * - * @param image The image to generate mipmaps for. * - * @return A string describing the error, if unable to generate mipmaps. + * @deprecated Use {@link ImageDecoder::generateMipMaps} instead. */ - static std::optional - generateMipMaps(CesiumGltf::ImageAsset& image) { - return ImageDecoder::generateMipMaps(image); - } + [[deprecated("Use ImageDecoder::generateMipMaps instead.")]] static std:: + optional + generateMipMaps(CesiumGltf::ImageAsset& image); private: CesiumJsonReader::JsonReaderOptions _context; diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h index b2d60d2df..67cf4e3ec 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -1,3 +1,5 @@ +#pragma once + #include "CesiumGltf/ImageAsset.h" #include "CesiumGltfReader/Library.h" diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 011340119..3809f3ee7 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -234,10 +234,7 @@ GltfReaderResult readBinaryGltf( return result; } -void postprocess( - const GltfReader& reader, - GltfReaderResult& readGltf, - const GltfReaderOptions& options) { +void postprocess(GltfReaderResult& readGltf, const GltfReaderOptions& options) { Model& model = readGltf.model.value(); auto extFeatureMetadataIter = std::find( @@ -253,7 +250,7 @@ void postprocess( } if (options.decodeDataUrls) { - decodeDataUrls(reader, readGltf, options); + decodeDataUrls(readGltf, options); } if (options.decodeEmbeddedImages) { @@ -381,7 +378,7 @@ GltfReaderResult GltfReader::readGltf( : readJsonGltf(context, data); if (result.model) { - postprocess(*this, result, options); + postprocess(result, options); } return result; @@ -437,7 +434,7 @@ CesiumAsync::Future GltfReader::loadGltf( std::move(result)); }) .thenInWorkerThread([options, this](GltfReaderResult&& result) { - postprocess(*this, result, options); + postprocess(result, options); return std::move(result); }); } @@ -446,7 +443,7 @@ void CesiumGltfReader::GltfReader::postprocessGltf( GltfReaderResult& readGltf, const GltfReaderOptions& options) { if (readGltf.model) { - postprocess(*this, readGltf, options); + postprocess(readGltf, options); } } @@ -623,3 +620,14 @@ void CesiumGltfReader::GltfReader::postprocessGltf( return std::move(*pResult.release()); }); } + +/*static*/ ImageReaderResult GltfReader::readImage( + const gsl::span& data, + const Ktx2TranscodeTargets& ktx2TranscodeTargets) { + return ImageDecoder::readImage(data, ktx2TranscodeTargets); +} + +/*static*/ std::optional +GltfReader::generateMipMaps(CesiumGltf::ImageAsset& image) { + return ImageDecoder::generateMipMaps(image); +} diff --git a/CesiumGltfReader/src/decodeDataUrls.cpp b/CesiumGltfReader/src/decodeDataUrls.cpp index bbfdba67f..e76d03c43 100644 --- a/CesiumGltfReader/src/decodeDataUrls.cpp +++ b/CesiumGltfReader/src/decodeDataUrls.cpp @@ -1,8 +1,8 @@ #include "decodeDataUrls.h" -#include "CesiumGltfReader/GltfReader.h" - #include +#include +#include #include #include @@ -87,7 +87,6 @@ std::optional tryDecode(const std::string& uri) { } // namespace void decodeDataUrls( - const GltfReader& reader, GltfReaderResult& readGltf, const GltfReaderOptions& options) { CESIUM_TRACE("CesiumGltfReader::decodeDataUrls"); @@ -134,8 +133,9 @@ void decodeDataUrls( continue; } - ImageReaderResult imageResult = - reader.readImage(decoded.value().data, options.ktx2TranscodeTargets); + ImageReaderResult imageResult = ImageDecoder::readImage( + decoded.value().data, + options.ktx2TranscodeTargets); if (!imageResult.pImage) { continue; diff --git a/CesiumGltfReader/src/decodeDataUrls.h b/CesiumGltfReader/src/decodeDataUrls.h index 98c217110..5b9d2d01e 100644 --- a/CesiumGltfReader/src/decodeDataUrls.h +++ b/CesiumGltfReader/src/decodeDataUrls.h @@ -7,7 +7,6 @@ struct GltfReaderOptions; class GltfReader; void decodeDataUrls( - const GltfReader& reader, GltfReaderResult& readGltf, const GltfReaderOptions& clearDecodedDataUrls); } // namespace CesiumGltfReader diff --git a/CesiumGltfReader/test/TestGltfReader.cpp b/CesiumGltfReader/test/TestGltfReader.cpp index 84b4336c7..10b62fd78 100644 --- a/CesiumGltfReader/test/TestGltfReader.cpp +++ b/CesiumGltfReader/test/TestGltfReader.cpp @@ -563,67 +563,6 @@ TEST_CASE("Can apply RTC CENTER if model uses Cesium RTC extension") { CHECK(cesiumRTC->center == rtcCenter); } -TEST_CASE("Can correctly interpret mipmaps in KTX2 files") { - { - // This KTX2 file has a single mip level and no further mip levels should be - // generated. `mipPositions` should reflect this single mip level. - std::filesystem::path ktx2File = CesiumGltfReader_TEST_DATA_DIR; - ktx2File /= "ktx2/kota-onelevel.ktx2"; - std::vector data = readFile(ktx2File.string()); - ImageReaderResult imageResult = - GltfReader::readImage(data, Ktx2TranscodeTargets{}); - REQUIRE(imageResult.pImage); - - const ImageAsset& image = *imageResult.pImage; - REQUIRE(image.mipPositions.size() == 1); - CHECK(image.mipPositions[0].byteOffset == 0); - CHECK(image.mipPositions[0].byteSize > 0); - CHECK( - image.mipPositions[0].byteSize == - size_t(image.width * image.height * image.channels)); - CHECK(image.mipPositions[0].byteSize == image.pixelData.size()); - } - - { - // This KTX2 file has only a base image but further mip levels can be - // generated. This image effectively has no mip levels. - std::filesystem::path ktx2File = CesiumGltfReader_TEST_DATA_DIR; - ktx2File /= "ktx2/kota-automipmap.ktx2"; - std::vector data = readFile(ktx2File.string()); - ImageReaderResult imageResult = - GltfReader::readImage(data, Ktx2TranscodeTargets{}); - REQUIRE(imageResult.pImage); - - const ImageAsset& image = *imageResult.pImage; - REQUIRE(image.mipPositions.size() == 0); - CHECK(image.pixelData.size() > 0); - } - - { - // This KTX2 file has a complete mip chain. - std::filesystem::path ktx2File = CesiumGltfReader_TEST_DATA_DIR; - ktx2File /= "ktx2/kota-mipmaps.ktx2"; - std::vector data = readFile(ktx2File.string()); - ImageReaderResult imageResult = - GltfReader::readImage(data, Ktx2TranscodeTargets{}); - REQUIRE(imageResult.pImage); - - const ImageAsset& image = *imageResult.pImage; - REQUIRE(image.mipPositions.size() == 9); - CHECK(image.mipPositions[0].byteSize > 0); - CHECK( - image.mipPositions[0].byteSize == - size_t(image.width * image.height * image.channels)); - CHECK(image.mipPositions[0].byteSize < image.pixelData.size()); - - size_t smallerThan = image.mipPositions[0].byteSize; - for (size_t i = 1; i < image.mipPositions.size(); ++i) { - CHECK(image.mipPositions[i].byteSize < smallerThan); - smallerThan = image.mipPositions[i].byteSize; - } - } -} - TEST_CASE("Can read unknown properties from a glTF") { const std::string s = R"( { diff --git a/CesiumGltfReader/test/TestImageDecoder.cpp b/CesiumGltfReader/test/TestImageDecoder.cpp new file mode 100644 index 000000000..6cebfe41c --- /dev/null +++ b/CesiumGltfReader/test/TestImageDecoder.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include + +#include + +using namespace CesiumGltf; +using namespace CesiumGltfReader; + +TEST_CASE("CesiumGltfReader::ImageDecoder") { + SECTION("Can correctly interpret mipmaps in KTX2 files") { + { + // This KTX2 file has a single mip level and no further mip levels should + // be generated. `mipPositions` should reflect this single mip level. + std::filesystem::path ktx2File = CesiumGltfReader_TEST_DATA_DIR; + ktx2File /= "ktx2/kota-onelevel.ktx2"; + std::vector data = readFile(ktx2File.string()); + ImageReaderResult imageResult = + ImageDecoder::readImage(data, Ktx2TranscodeTargets{}); + REQUIRE(imageResult.pImage); + + const ImageAsset& image = *imageResult.pImage; + REQUIRE(image.mipPositions.size() == 1); + CHECK(image.mipPositions[0].byteOffset == 0); + CHECK(image.mipPositions[0].byteSize > 0); + CHECK( + image.mipPositions[0].byteSize == + size_t(image.width * image.height * image.channels)); + CHECK(image.mipPositions[0].byteSize == image.pixelData.size()); + } + + { + // This KTX2 file has only a base image but further mip levels can be + // generated. This image effectively has no mip levels. + std::filesystem::path ktx2File = CesiumGltfReader_TEST_DATA_DIR; + ktx2File /= "ktx2/kota-automipmap.ktx2"; + std::vector data = readFile(ktx2File.string()); + ImageReaderResult imageResult = + ImageDecoder::readImage(data, Ktx2TranscodeTargets{}); + REQUIRE(imageResult.pImage); + + const ImageAsset& image = *imageResult.pImage; + REQUIRE(image.mipPositions.size() == 0); + CHECK(image.pixelData.size() > 0); + } + + { + // This KTX2 file has a complete mip chain. + std::filesystem::path ktx2File = CesiumGltfReader_TEST_DATA_DIR; + ktx2File /= "ktx2/kota-mipmaps.ktx2"; + std::vector data = readFile(ktx2File.string()); + ImageReaderResult imageResult = + ImageDecoder::readImage(data, Ktx2TranscodeTargets{}); + REQUIRE(imageResult.pImage); + + const ImageAsset& image = *imageResult.pImage; + REQUIRE(image.mipPositions.size() == 9); + CHECK(image.mipPositions[0].byteSize > 0); + CHECK( + image.mipPositions[0].byteSize == + size_t(image.width * image.height * image.channels)); + CHECK(image.mipPositions[0].byteSize < image.pixelData.size()); + + size_t smallerThan = image.mipPositions[0].byteSize; + for (size_t i = 1; i < image.mipPositions.size(); ++i) { + CHECK(image.mipPositions[i].byteSize < smallerThan); + smallerThan = image.mipPositions[i].byteSize; + } + } + } +} diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h index db1113b5d..b104e1b1d 100644 --- a/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/RasterOverlayTileProvider.h @@ -414,7 +414,5 @@ class CESIUMRASTEROVERLAYS_API RasterOverlayTileProvider CESIUM_TRACE_DECLARE_TRACK_SET( _loadingSlots, "Raster Overlay Tile Loading Slot"); - - static CesiumGltfReader::GltfReader _gltfReader; }; } // namespace CesiumRasterOverlays diff --git a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp index 381497083..33feb4e15 100644 --- a/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/RasterOverlayTileProvider.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -18,9 +18,6 @@ using namespace CesiumUtility; namespace CesiumRasterOverlays { -/*static*/ CesiumGltfReader::GltfReader - RasterOverlayTileProvider::_gltfReader{}; - RasterOverlayTileProvider::RasterOverlayTileProvider( const CesiumUtility::IntrusivePointer& pOwner, const CesiumAsync::AsyncSystem& asyncSystem, @@ -189,9 +186,7 @@ RasterOverlayTileProvider::loadTileImageFromUrl( const gsl::span data = pResponse->data(); CesiumGltfReader::ImageReaderResult loadedImage = - RasterOverlayTileProvider::_gltfReader.readImage( - data, - Ktx2TranscodeTargets); + ImageDecoder::readImage(data, Ktx2TranscodeTargets); if (!loadedImage.errors.empty()) { loadedImage.errors.push_back("Image url: " + pRequest->url()); From e8fbd40e4b647370023c1bd085fc6d3a6140b6eb Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 13:32:30 +1100 Subject: [PATCH 78/81] Doc and cleanup. --- .../include/CesiumGltfReader/ImageDecoder.h | 6 ++++-- .../CesiumGltfReader/NetworkImageAssetDescriptor.h | 3 +-- CesiumGltfReader/src/GltfReader.cpp | 4 ---- .../CesiumJsonReader/IgnoreValueJsonHandler.h | 1 - CesiumUtility/include/CesiumUtility/Hash.h | 12 ++++++++++++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h index 67cf4e3ec..c3bb0b300 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h +++ b/CesiumGltfReader/include/CesiumGltfReader/ImageDecoder.h @@ -14,8 +14,7 @@ namespace CesiumGltfReader { /** - * @brief The result of reading an image with - * {@link ImageDecoder::readImage}. + * @brief The result of reading an image with {@link ImageDecoder::readImage}. */ struct CESIUMGLTFREADER_API ImageReaderResult { @@ -37,6 +36,9 @@ struct CESIUMGLTFREADER_API ImageReaderResult { std::vector warnings; }; +/** + * @brief Contains methods for reading and manipulating images. + */ class ImageDecoder { public: /** diff --git a/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h b/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h index 1f5129e4a..2c2c30f03 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h +++ b/CesiumGltfReader/include/CesiumGltfReader/NetworkImageAssetDescriptor.h @@ -42,8 +42,7 @@ struct NetworkImageAssetDescriptor * @return A future that resolves to the image asset once the request is * complete. */ - CesiumAsync::Future>> + CesiumAsync::Future> load( const CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor) const; diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 3809f3ee7..4d68086f7 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -8,13 +8,10 @@ #include "dequantizeMeshData.h" #include "registerReaderExtensions.h" -#include #include #include #include #include -#include -#include #include #include #include @@ -29,7 +26,6 @@ #include #include #include -#include #include #include diff --git a/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h b/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h index 2dd50d570..2adc9f917 100644 --- a/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h +++ b/CesiumJsonReader/include/CesiumJsonReader/IgnoreValueJsonHandler.h @@ -4,7 +4,6 @@ #include "Library.h" #include -#include namespace CesiumJsonReader { class CESIUMJSONREADER_API IgnoreValueJsonHandler : public IJsonHandler { diff --git a/CesiumUtility/include/CesiumUtility/Hash.h b/CesiumUtility/include/CesiumUtility/Hash.h index 74ca7abfc..950d2cacf 100644 --- a/CesiumUtility/include/CesiumUtility/Hash.h +++ b/CesiumUtility/include/CesiumUtility/Hash.h @@ -4,7 +4,19 @@ namespace CesiumUtility { +/** + * @brief Contains functions for working with hashes. + * + */ struct Hash { + /** + * @brief Combines two hash values, usually generated using `std::hash`, to + * form a single hash value. + * + * @param first The first hash value. + * @param second The second hash value. + * @return A new hash value which is a combination of the two. + */ static std::size_t combine(std::size_t first, std::size_t second); }; From 3435ad6a3c16281ca3d1dbaa7120e4deddf652fc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 14:11:22 +1100 Subject: [PATCH 79/81] Pass a TilesetSharedAssetSystem into TilesetExternals. --- .../include/Cesium3DTilesSelection/TilesetExternals.h | 8 ++++++++ Cesium3DTilesSelection/src/TilesetContentManager.cpp | 6 +++--- CesiumGltfReader/include/CesiumGltfReader/GltfReader.h | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetExternals.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetExternals.h index 53c290373..17fec2f66 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetExternals.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetExternals.h @@ -2,6 +2,7 @@ #include "Library.h" #include "TileOcclusionRendererProxy.h" +#include "TilesetSharedAssetSystem.h" #include "spdlog-cesium.h" #include @@ -69,6 +70,13 @@ class CESIUM3DTILESSELECTION_API TilesetExternals final { */ std::shared_ptr pTileOcclusionProxyPool = nullptr; + + /** + * @brief The shared asset system used to facilitate sharing of common assets, + * such as images, between and within tilesets. + */ + CesiumUtility::IntrusivePointer pSharedAssetSystem = + TilesetSharedAssetSystem::getDefault(); }; } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 266644707..805d4d6b9 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -666,7 +666,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssetSystem(TilesetSharedAssetSystem::getDefault()), + _pSharedAssetSystem(externals.pSharedAssetSystem), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -696,7 +696,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssetSystem(TilesetSharedAssetSystem::getDefault()), + _pSharedAssetSystem(externals.pSharedAssetSystem), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, @@ -848,7 +848,7 @@ TilesetContentManager::TilesetContentManager( _tileLoadsInProgress{0}, _loadedTilesCount{0}, _tilesDataUsed{0}, - _pSharedAssetSystem(TilesetSharedAssetSystem::getDefault()), + _pSharedAssetSystem(externals.pSharedAssetSystem), _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ this->_destructionCompletePromise.getFuture().share()}, diff --git a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h index 2f14222ad..62a496dcf 100644 --- a/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h +++ b/CesiumGltfReader/include/CesiumGltfReader/GltfReader.h @@ -115,7 +115,7 @@ struct CESIUMGLTFREADER_API GltfReaderOptions { * that might appear in this glTF. */ CesiumUtility::IntrusivePointer pSharedAssetSystem = - nullptr; + GltfSharedAssetSystem::getDefault(); }; /** From ba4f6f54befce575d4da63f4d0c279ce6aafcb1d Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 14:13:58 +1100 Subject: [PATCH 80/81] Update CHANGES.md. --- CHANGES.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b06ef026c..87c146ffb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,11 +5,34 @@ ##### Breaking Changes :mega: - Renamed `CesiumUtility/Gunzip.h` to `CesiumUtility/Gzip.h`. +- Renamed `ImageCesium` to `ImageAsset`. +- The `cesium` field in `CesiumGltf::Image` is now named `pAsset` and is an `IntrusivePointer` to an `ImageAsset`. +- The `image` field in `LoadedRasterOverlayImage` is now named `pImage` and is an `IntrusivePointer` to an `ImageAsset`. +- Deprecated the `readImage` and `generateMipMaps` methods on `GltfReader`. These methods are now found on `ImageDecoder`. ##### Additions :tada: - Added `CesiumUtility::gzip`. - Added `CesiumGeometry::Transforms::getUpAxisTransform` to get the transform that converts from one up axis to another. +- Added `TilesetSharedAssetSystem` to `Cesium3DTilesSelection` and `GltfSharedAssetSystem` to `CesiumGltfReader`. +- Added `SharedAsset` to `CesiumUtility` to serve as the base class for assets such as `ImageAsset`. +- Added `SharedAssetDepot` to `CesiumAsync` for managing assets, such as images, that can be shared among multiple models or other objects. +- Added `NetworkAssetDescriptor` and `NetworkImageAssetDescriptor`. +- `ImageAsset` (formerly `ImageCesium`) is now an `ExtensibleObject`. +- Added `VertexAttributeSemantics` to `CesiumGltf`. +- Added `ImageDecoder` to `CesiumGltfReader`. +- Added `DoublyLinkedListAdvanced` to `CesiumUtility`. It is equivalent to `DoublyLinkedList` except it allows the next and previous pointers to be in a base class of the node class. +- Added `contains` method to `DoublyLinkedList` (and `DoublyLinkedListAdvanced`). +- Added static `error` and `warning` methods to `ErrorList`, making it easy to create an instance with a single error or warning. +- `ExtensibleObject::addExtension` now takes arguments that are passed through to the extension's constructor. +- Added `Hash` to `CesiumUtility`. +- Added `emplace` and `reset` methods to `IntrusivePointer`. +- Added `Result` and `ResultPointer` classes to represent the result of an operation that might complete with warnings and errors. + +##### Fixes :wrench: + +- Fixed a bug in `AsyncSystem::all` where the resolved values of individual futures were copied instead of moved into the output array. +- Improved the hash function for `QuadtreeTileID`. ### v0.40.1 - 2024-10-01 From 750d5f91d18f7d8e203d89e6906edc67c1a7101c Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 30 Oct 2024 14:20:21 +1100 Subject: [PATCH 81/81] Fix clang warning. --- CesiumGltfReader/src/GltfReader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CesiumGltfReader/src/GltfReader.cpp b/CesiumGltfReader/src/GltfReader.cpp index 4d68086f7..28a84b941 100644 --- a/CesiumGltfReader/src/GltfReader.cpp +++ b/CesiumGltfReader/src/GltfReader.cpp @@ -429,7 +429,7 @@ CesiumAsync::Future GltfReader::loadGltf( options, std::move(result)); }) - .thenInWorkerThread([options, this](GltfReaderResult&& result) { + .thenInWorkerThread([options](GltfReaderResult&& result) { postprocess(result, options); return std::move(result); });