diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 39b58fa61..af8a85824 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -948,7 +948,7 @@ class UnrealResourcePreparer const Cesium3DTilesSelection::TileContent& content = tile.getContent(); const Cesium3DTilesSelection::TileRenderContent* pRenderContent = content.getRenderContent(); - if (pRenderContent) { + if (pMainThreadRendererResources != nullptr && pRenderContent != nullptr) { UCesiumGltfComponent* pGltfContent = reinterpret_cast( pRenderContent->getRenderResources()); @@ -978,7 +978,7 @@ class UnrealResourcePreparer UCesiumGltfComponent* pGltfContent = reinterpret_cast( pRenderContent->getRenderResources()); - if (pGltfContent) { + if (pMainThreadRendererResources != nullptr && pGltfContent != nullptr) { pGltfContent->DetachRasterTile( tile, rasterTile, diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 2f07cd875..e8d2c5620 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -8,6 +8,7 @@ #include "CesiumFeatureIdSet.h" #include "CesiumGltfPointsComponent.h" #include "CesiumGltfPrimitiveComponent.h" +#include "CesiumGltfTextures.h" #include "CesiumMaterialUserData.h" #include "CesiumRasterOverlays.h" #include "CesiumRuntime.h" @@ -400,22 +401,22 @@ struct ColorVisitor { template static TUniquePtr loadTexture( CesiumGltf::Model& model, - const std::optional& gltfTexture, + const std::optional& gltfTextureInfo, bool sRGB) { - if (!gltfTexture || gltfTexture.value().index < 0 || - gltfTexture.value().index >= model.textures.size()) { - if (gltfTexture && gltfTexture.value().index >= 0) { + if (!gltfTextureInfo || gltfTextureInfo.value().index < 0 || + gltfTextureInfo.value().index >= model.textures.size()) { + if (gltfTextureInfo && gltfTextureInfo.value().index >= 0) { UE_LOG( LogCesium, Warning, TEXT("Texture index must be less than %d, but is %d"), model.textures.size(), - gltfTexture.value().index); + gltfTextureInfo.value().index); } return nullptr; } - int32_t textureIndex = gltfTexture.value().index; + int32_t textureIndex = gltfTextureInfo.value().index; CesiumGltf::Texture& texture = model.textures[textureIndex]; return loadTextureFromModelAnyThreadPart(model, texture, sRGB); } @@ -1061,7 +1062,7 @@ std::string constrainLength(const std::string& s, const size_t maxLength) { } /** - * @brief Create an FName from the given strings. + * @brief CreateNew an FName from the given strings. * * This will combine the prefix and the suffix and create an FName. * If the string would be longer than the given length, then @@ -2238,7 +2239,7 @@ loadModelAnyThreadPart( const CesiumGeospatial::Ellipsoid& ellipsoid) { TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::loadModelAnyThreadPart) - return createMipMapsForAllTextures(asyncSystem, *options.pModel) + return CesiumGltfTextures::createInWorkerThread(asyncSystem, *options.pModel) .thenInWorkerThread( [transform, ellipsoid, options = std::move(options)]() -> UCesiumGltfComponent::CreateOffGameThreadResult { diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index a6fff0bbb..95cfe003c 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -4,6 +4,7 @@ #include "CesiumRuntime.h" #include "CesiumTextureResource.h" #include "CesiumTextureUtility.h" +#include "ExtensionImageCesiumUnreal.h" #include #include #include @@ -36,7 +37,7 @@ SharedFuture createTextureInLoadThread( } // namespace -/*static*/ CesiumAsync::Future CesiumGltfTextures::createInLoadThread( +/*static*/ CesiumAsync::Future CesiumGltfTextures::createInWorkerThread( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::Model& model) { // This array is parallel to model.images and indicates whether each image @@ -183,39 +184,6 @@ bool doesTextureUseMipmaps(const Model& gltf, const Texture& texture) { } } -struct ExtensionUnrealTextureResource { - static inline constexpr const char* TypeName = - "ExtensionUnrealTextureResource"; - static inline constexpr const char* ExtensionName = - "PRIVATE_unreal_texture_resource"; - - ExtensionUnrealTextureResource() {} - - TSharedPtr pTextureResource = nullptr; - std::optional> createFuture = std::nullopt; -}; - -std::mutex textureResourceMutex; - -// Returns a Future that will resolve when the image is loaded. It _may_ also -// return a Promise, in which case the calling thread is responsible for doing -// the loading and should resolve the Promise when it's done. -std::pair, std::optional>> -getOrCreateImageFuture(const AsyncSystem& asyncSystem, Image& image) { - std::scoped_lock lock(textureResourceMutex); - - ExtensionUnrealTextureResource& extension = - image.cesium->addExtension(); - if (extension.createFuture) { - // Another thread is already working on this image. - return {*extension.createFuture, std::nullopt}; - } else { - // This thread will work on this image. - Promise promise = asyncSystem.createPromise(); - return {promise.getFuture().share(), promise}; - } -} - SharedFuture createTextureInLoadThread( const AsyncSystem& asyncSystem, Model& gltf, @@ -233,35 +201,13 @@ SharedFuture createTextureInLoadThread( check(pTexture->source >= 0 && pTexture->source < imageNeedsMipmaps.size()); bool needsMips = imageNeedsMipmaps[pTexture->source]; - auto [future, maybePromise] = getOrCreateImageFuture(asyncSystem, *pImage); - if (!maybePromise) { - // Another thread is already loading this image. - return future; - } - - // Proceed to load the image in this thread. - ImageCesium& cesium = *pImage->cesium; - - if (needsMips && !cesium.pixelData.empty()) { - std::optional errorMessage = - CesiumGltfReader::GltfReader::generateMipMaps(cesium); - if (errorMessage) { - UE_LOG( - LogCesium, - Warning, - TEXT("%s"), - UTF8_TO_TCHAR(errorMessage->c_str())); - } - } - - CesiumTextureUtility::loadTextureFromModelAnyThreadPart( - gltf, - *pTexture, - sRGB); - - maybePromise->resolve(); + const ExtensionImageCesiumUnreal& extension = ExtensionImageCesiumUnreal::GetOrCreate( + asyncSystem, + *pImage->cesium, + needsMips, + std::nullopt); - return future; + return extension.getFuture(); } } // namespace diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.h b/Source/CesiumRuntime/Private/CesiumGltfTextures.h index 339481b1b..7f0326ad1 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.h +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.h @@ -18,7 +18,7 @@ class CesiumGltfTextures { * Creates all of the textures that are required by the given glTF, and adds * `ExtensionUnrealTexture` to each. */ - static CesiumAsync::Future createInLoadThread( + static CesiumAsync::Future createInWorkerThread( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::Model& model); }; diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index 1bbeb5a59..b3ca34a8b 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp @@ -1,11 +1,87 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "CesiumTextureResource.h" +#include "CesiumRuntime.h" +#include "CesiumTextureUtility.h" #include "Misc/CoreStats.h" #include "RenderUtils.h" +#include namespace { +/** + * A Cesium texture resource that uses an already-created `FRHITexture`. This is + * used when `GRHISupportsAsyncTextureCreation` is true and so we were already + * able to create the FRHITexture in a worker thread. It is also used when a + * single glTF `Image` is referenced by multiple glTF `Texture` instances. We + * only need one `FRHITexture` is this case, but we need multiple + * `FTextureResource` instances to support the different sampler settings that + * are likely used in the different textures. + */ +class FCesiumUseExistingTextureResource : public FCesiumTextureResource { +public: + FCesiumUseExistingTextureResource( + FTextureRHIRef existingTexture, + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipsIfAvailable, + uint32 extData); + + FCesiumUseExistingTextureResource( + const TSharedPtr& pExistingTexture, + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipsIfAvailable, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; + +private: + TSharedPtr _pExistingTexture; +}; + +/** + * A Cesium texture resource that creates an `FRHITexture` from a glTF + * `ImageCesium` when `InitRHI` is called from the render thread. When + * `GRHISupportsAsyncTextureCreation` is false (everywhere but Direct3D), we can + * only create a `FRHITexture` on the render thread, so this is the code that + * does it. + */ +class FCesiumCreateNewTextureResource : public FCesiumTextureResource { +public: + FCesiumCreateNewTextureResource( + CesiumGltf::ImageCesium&& image, + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipsIfAvailable, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; + +private: + CesiumGltf::ImageCesium _image; +}; + ESamplerFilter convertFilter(TextureFilter filter) { switch (filter) { case TF_Nearest: @@ -98,9 +174,268 @@ void CopyMip( } } +FTexture2DRHIRef createAsyncTextureAndWait( + uint32 SizeX, + uint32 SizeY, + uint8 Format, + uint32 NumMips, + ETextureCreateFlags Flags, + void** InitialMipData, + uint32 NumInitialMips) { +#if ENGINE_VERSION_5_4_OR_HIGHER + FGraphEventRef CompletionEvent; + + FTexture2DRHIRef result = RHIAsyncCreateTexture2D( + SizeX, + SizeY, + Format, + NumMips, + Flags, + ERHIAccess::Unknown, + InitialMipData, + NumInitialMips, + TEXT("CesiumTexture"), + CompletionEvent); + + if (CompletionEvent) { + CompletionEvent->Wait(); + } + + return result; +#elif ENGINE_VERSION_5_3_OR_HIGHER + FGraphEventRef CompletionEvent; + + FTexture2DRHIRef result = RHIAsyncCreateTexture2D( + SizeX, + SizeY, + Format, + NumMips, + Flags, + InitialMipData, + NumInitialMips, + CompletionEvent); + + if (CompletionEvent) { + CompletionEvent->Wait(); + } + + return result; +#else + return RHIAsyncCreateTexture2D( + SizeX, + SizeY, + Format, + NumMips, + Flags, + InitialMipData, + NumInitialMips); +#endif +} + +/** + * @brief Create an RHI texture on this thread. This requires + * GRHISupportsAsyncTextureCreation to be true. + * + * @param image The CPU image to create on the GPU. + * @param format The pixel format of the image. + * @param Whether to use a sRGB color-space. + * @return The RHI texture reference. + */ +FTexture2DRHIRef CreateRHITexture2D_Async( + const CesiumGltf::ImageCesium& image, + EPixelFormat format, + bool sRGB) { + check(GRHISupportsAsyncTextureCreation); + + ETextureCreateFlags textureFlags = TexCreate_ShaderResource; + + // Just like in FCesiumCreateNewTextureResource, we're assuming here that we + // can create an FRHITexture as sRGB, and later create another + // UTexture2D / FTextureResource pointing to the same FRHITexture that is not + // sRGB (or vice-versa), and that Unreal will effectively ignore the flag on + // FRHITexture. + if (sRGB) { + textureFlags |= TexCreate_SRGB; + } + + if (!image.mipPositions.empty()) { + // Here 16 is a generously large (but arbitrary) hard limit for number of + // mips. + uint32 mipCount = static_cast(image.mipPositions.size()); + if (mipCount > 16) { + mipCount = 16; + } + + void* mipsData[16]; + for (size_t i = 0; i < mipCount; ++i) { + const CesiumGltf::ImageCesiumMipPosition& mipPos = image.mipPositions[i]; + mipsData[i] = (void*)(&image.pixelData[mipPos.byteOffset]); + } + + return createAsyncTextureAndWait( + static_cast(image.width), + static_cast(image.height), + format, + mipCount, + textureFlags, + mipsData, + mipCount); + } else { + void* pTextureData = (void*)(image.pixelData.data()); + return createAsyncTextureAndWait( + static_cast(image.width), + static_cast(image.height), + format, + 1, + textureFlags, + &pTextureData, + 1); + } +} + } // namespace -FCesiumTextureResourceBase::FCesiumTextureResourceBase( +void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { + FCesiumTextureResource::Destroy(p); +} + +/*static*/ FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateNew( + CesiumGltf::ImageCesium& imageCesium, + TextureGroup textureGroup, + const std::optional& overridePixelFormat, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool needsMipMaps) { + if (imageCesium.pixelData.empty()) { + return nullptr; + } + + if (needsMipMaps) { + std::optional errorMessage = + CesiumGltfReader::GltfReader::generateMipMaps(imageCesium); + if (errorMessage) { + UE_LOG( + LogCesium, + Warning, + TEXT("%s"), + UTF8_TO_TCHAR(errorMessage->c_str())); + } + } + + std::optional maybePixelFormat = + CesiumTextureUtility::getPixelFormatForImageCesium( + imageCesium, + overridePixelFormat); + if (!maybePixelFormat) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Image cannot be created because it has an unsupported compressed pixel format (%d)."), + imageCesium.compressedPixelFormat); + return nullptr; + } + + // Store the current size of the pixel data, because + // we're about to clear it but we still want to have + // an accurate estimation of the size of the image for + // caching purposes. + imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); + + if (GRHISupportsAsyncTextureCreation) { + // Create RHI texture resource on this worker + // thread, and then hand it off to the renderer + // thread. + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::CreateRHITexture2D) + + FTexture2DRHIRef textureReference = + CreateRHITexture2D_Async(imageCesium, *maybePixelFormat, sRGB); + auto pResult = TUniquePtr< + FCesiumUseExistingTextureResource, + FCesiumTextureResourceDeleter>(new FCesiumUseExistingTextureResource( + textureReference, + textureGroup, + imageCesium.width, + imageCesium.height, + *maybePixelFormat, + filter, + addressX, + addressY, + sRGB, + needsMipMaps, + 0)); + + // Clear the now-unnecessary copy of the pixel data. + // Calling clear() isn't good enough because it + // won't actually release the memory. + std::vector pixelData; + imageCesium.pixelData.swap(pixelData); + + std::vector mipPositions; + imageCesium.mipPositions.swap(mipPositions); + + return pResult; + } else { + // The RHI texture will be created later on the + // render thread, directly from this texture source. + // We need valid pixelData here, though. + auto pResult = TUniquePtr< + FCesiumCreateNewTextureResource, + FCesiumTextureResourceDeleter>(new FCesiumCreateNewTextureResource( + std::move(imageCesium), + textureGroup, + imageCesium.width, + imageCesium.height, + *maybePixelFormat, + filter, + addressX, + addressY, + sRGB, + needsMipMaps, + 0)); + return pResult; + } +} + +FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateWrapped( + const TSharedPtr& pExistingResource, + TextureGroup textureGroup, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipMapsIfAvailable) { + if (pExistingResource == nullptr) + return nullptr; + + return FCesiumTextureResourceUniquePtr(new FCesiumUseExistingTextureResource( + pExistingResource, + textureGroup, + pExistingResource->_width, + pExistingResource->_height, + pExistingResource->_format, + filter, + addressX, + addressY, + sRGB, + useMipMapsIfAvailable, + 0)); +} + +/*static*/ void FCesiumTextureResource::Destroy(FCesiumTextureResource* p) { + if (p == nullptr) + return; + + ENQUEUE_RENDER_COMMAND(DeleteResource) + ([p](FRHICommandListImmediate& RHICmdList) { + p->ReleaseResource(); + delete p; + }); +} + +FCesiumTextureResource::FCesiumTextureResource( TextureGroup textureGroup, uint32 width, uint32 height, @@ -128,7 +463,7 @@ FCesiumTextureResourceBase::FCesiumTextureResourceBase( #if ENGINE_VERSION_5_3_OR_HIGHER void FCesiumTextureResourceBase::InitRHI(FRHICommandListBase& RHICmdList) { #else -void FCesiumTextureResourceBase::InitRHI() { +void FCesiumTextureResource::InitRHI() { #endif FSamplerStateInitializerRHI samplerStateInitializer( this->_filter, @@ -188,7 +523,7 @@ void FCesiumTextureResourceBase::InitRHI() { #endif } -void FCesiumTextureResourceBase::ReleaseRHI() { +void FCesiumTextureResource::ReleaseRHI() { DEC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); DEC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); @@ -216,7 +551,7 @@ FOREACH_ENUM_TEXTUREGROUP(DECLARETEXTUREGROUPSTAT) #undef DECLARETEXTUREGROUPSTAT } // namespace -FName FCesiumTextureResourceBase::TextureGroupStatFNames[TEXTUREGROUP_MAX] = { +FName FCesiumTextureResource::TextureGroupStatFNames[TEXTUREGROUP_MAX] = { #define ASSIGNTEXTUREGROUPSTATNAME(Group) GET_STATFNAME(STAT_##Group), FOREACH_ENUM_TEXTUREGROUP(ASSIGNTEXTUREGROUPSTATNAME) #undef ASSIGNTEXTUREGROUPSTATNAME @@ -236,7 +571,7 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( bool sRGB, bool useMipsIfAvailable, uint32 extData) - : FCesiumTextureResourceBase( + : FCesiumTextureResource( textureGroup, width, height, @@ -252,7 +587,7 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( } FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( - FTextureResource* pExistingTexture, + const TSharedPtr& pExistingTexture, TextureGroup textureGroup, uint32 width, uint32 height, @@ -263,7 +598,7 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( bool sRGB, bool useMipsIfAvailable, uint32 extData) - : FCesiumTextureResourceBase( + : FCesiumTextureResource( textureGroup, width, height, @@ -296,7 +631,7 @@ FCesiumCreateNewTextureResource::FCesiumCreateNewTextureResource( bool sRGB, bool useMipsIfAvailable, uint32 extData) - : FCesiumTextureResourceBase( + : FCesiumTextureResource( textureGroup, width, height, diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.h b/Source/CesiumRuntime/Private/CesiumTextureResource.h index 83b75d503..cba96bc6b 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.h +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.h @@ -8,14 +8,79 @@ #include #include +class FCesiumTextureResource; + +struct FCesiumTextureResourceDeleter { + void operator()(FCesiumTextureResource* p); +}; + +using FCesiumTextureResourceUniquePtr = + TUniquePtr; + /** * The base class for Cesium texture resources, making Cesium's texture data * available to Unreal's RHI. The actual creation of the RHI texture is deferred * to a pure virtual method, `InitializeTextureRHI`. */ -class FCesiumTextureResourceBase : public FTextureResource { +class FCesiumTextureResource : public FTextureResource { public: - FCesiumTextureResourceBase( + /** + * Create a new FCesiumTextureResource from an ImageCesium and the given + * sampling parameters. This method is intended to be called from a worker + * thread, not from the game or render thread. + * + * @param imageCesium The image data from which to create the texture + * resource. After this method returns, the `pixelData` will be empty, and + * `sizeBytes` will be set to its previous size. + * @param textureGroup The texture group in which to create this texture. + * @param overridePixelFormat Overrides the pixel format. If std::nullopt, the + * format is inferred from the `ImageCesium`. + * @param filter The texture filtering to use when sampling this texture. + * @param addressX The X texture addressing mode to use when sampling this + * texture. + * @param addressY The Y texture addressing mode to use when sampling this + * texture. + * @param sRGB True if the image data stored in this texture should be treated + * as sRGB. + * @param needsMipMaps True if this texture requires mipmaps. They will be + * generated if they don't already exist. + * @return The created texture resource, or nullptr if a texture could not be + * created. + */ + static FCesiumTextureResourceUniquePtr CreateNew( + CesiumGltf::ImageCesium& imageCesium, + TextureGroup textureGroup, + const std::optional& overridePixelFormat, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool needsMipMaps); + + /** + * Create a new FCesiumTextureResource wrapping an existing one and providing + * new sampling parameters. This method is intended to be called from a worker + * thread, not from the game or render thread. + */ + static FCesiumTextureResourceUniquePtr CreateWrapped( + const TSharedPtr& pExistingResource, + TextureGroup textureGroup, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipMapsIfAvailable); + + /** + * Destroys an FCesiumTextureResource. Unreal TextureResources must be + * destroyed on the render thread, so it is important not to call `delete` + * directly. + * + * \param p + */ + static void Destroy(FCesiumTextureResource* p); + + FCesiumTextureResource( TextureGroup textureGroup, uint32 width, uint32 height, @@ -56,76 +121,3 @@ class FCesiumTextureResourceBase : public FTextureResource { FName _lodGroupStatName; uint64 _textureSize; }; - -/** - * A Cesium texture resource that uses an already-created `FRHITexture`. This is - * used when `GRHISupportsAsyncTextureCreation` is true and so we were already - * able to create the FRHITexture in a worker thread. It is also used when a - * single glTF `Image` is referenced by multiple glTF `Texture` instances. We - * only need one `FRHITexture` is this case, but we need multiple - * `FTextureResource` instances to support the different sampler settings that - * are likely used in the different textures. - */ -class FCesiumUseExistingTextureResource : public FCesiumTextureResourceBase { -public: - FCesiumUseExistingTextureResource( - FTextureRHIRef existingTexture, - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - bool useMipsIfAvailable, - uint32 extData); - - FCesiumUseExistingTextureResource( - FTextureResource* pExistingTexture, - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - bool useMipsIfAvailable, - uint32 extData); - -protected: - virtual FTextureRHIRef InitializeTextureRHI() override; - -private: - FTextureResource* _pExistingTexture; -}; - -/** - * A Cesium texture resource that creates an `FRHITexture` from a glTF - * `ImageCesium` when `InitRHI` is called from the render thread. When - * `GRHISupportsAsyncTextureCreation` is false (everywhere but Direct3D), we can - * only create a `FRHITexture` on the render thread, so this is the code that - * does it. - */ -class FCesiumCreateNewTextureResource : public FCesiumTextureResourceBase { -public: - FCesiumCreateNewTextureResource( - CesiumGltf::ImageCesium&& image, - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - bool useMipsIfAvailable, - uint32 extData); - -protected: - virtual FTextureRHIRef InitializeTextureRHI() override; - -private: - CesiumGltf::ImageCesium _image; -}; diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index 19c3ea3f6..edad7fd09 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -10,6 +10,7 @@ #include "CesiumTextureResource.h" #include "Containers/ResourceArray.h" #include "DynamicRHI.h" +#include "ExtensionImageCesiumUnreal.h" #include "GenericPlatform/GenericPlatformProcess.h" #include "PixelFormat.h" #include "RHICommandList.h" @@ -40,69 +41,7 @@ struct ExtensionUnrealTexture { pTexture = nullptr; }; -std::optional getPixelFormatForImageCesium( - const ImageCesium& imageCesium, - const std::optional overridePixelFormat) { - if (imageCesium.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { - switch (imageCesium.compressedPixelFormat) { - case GpuCompressedPixelFormat::ETC1_RGB: - return EPixelFormat::PF_ETC1; - break; - case GpuCompressedPixelFormat::ETC2_RGBA: - return EPixelFormat::PF_ETC2_RGBA; - break; - case GpuCompressedPixelFormat::BC1_RGB: - return EPixelFormat::PF_DXT1; - break; - case GpuCompressedPixelFormat::BC3_RGBA: - return EPixelFormat::PF_DXT5; - break; - case GpuCompressedPixelFormat::BC4_R: - return EPixelFormat::PF_BC4; - break; - case GpuCompressedPixelFormat::BC5_RG: - return EPixelFormat::PF_BC5; - break; - case GpuCompressedPixelFormat::BC7_RGBA: - return EPixelFormat::PF_BC7; - break; - case GpuCompressedPixelFormat::ASTC_4x4_RGBA: - return EPixelFormat::PF_ASTC_4x4; - break; - case GpuCompressedPixelFormat::PVRTC2_4_RGBA: - return EPixelFormat::PF_PVRTC2; - break; - case GpuCompressedPixelFormat::ETC2_EAC_R11: - return EPixelFormat::PF_ETC2_R11_EAC; - break; - case GpuCompressedPixelFormat::ETC2_EAC_RG11: - return EPixelFormat::PF_ETC2_RG11_EAC; - break; - default: - // Unsupported compressed texture format. - return std::nullopt; - }; - } else if (overridePixelFormat) { - return *overridePixelFormat; - } else { - switch (imageCesium.channels) { - case 1: - return PF_R8; - break; - case 2: - return PF_R8G8; - break; - case 3: - case 4: - default: - return PF_R8G8B8A8; - }; - } - - return std::nullopt; -} - -FTexture2DRHIRef createAsyncTextureAndWait( +FTexture2DRHIRef createAsyncTextureAndWaitOld( uint32 SizeX, uint32 SizeY, uint8 Format, @@ -169,7 +108,7 @@ FTexture2DRHIRef createAsyncTextureAndWait( * @param Whether to use a sRGB color-space. * @return The RHI texture reference. */ -FTexture2DRHIRef CreateRHITexture2D_Async( +FTexture2DRHIRef CreateRHITexture2D_AsyncOld( const CesiumGltf::ImageCesium& image, EPixelFormat format, bool sRGB) { @@ -200,7 +139,7 @@ FTexture2DRHIRef CreateRHITexture2D_Async( mipsData[i] = (void*)(&image.pixelData[mipPos.byteOffset]); } - return createAsyncTextureAndWait( + return createAsyncTextureAndWaitOld( static_cast(image.width), static_cast(image.height), format, @@ -210,7 +149,7 @@ FTexture2DRHIRef CreateRHITexture2D_Async( mipCount); } else { void* pTextureData = (void*)(image.pixelData.data()); - return createAsyncTextureAndWait( + return createAsyncTextureAndWaitOld( static_cast(image.width), static_cast(image.height), format, @@ -265,612 +204,399 @@ void ReferenceCountedUnrealTexture::setUnrealTexture( this->_pUnrealTexture = p; } -const TSharedPtr& +const FCesiumTextureResourceUniquePtr& ReferenceCountedUnrealTexture::getTextureResource() const { return this->_pTextureResource; } -TSharedPtr& +FCesiumTextureResourceUniquePtr& ReferenceCountedUnrealTexture::getTextureResource() { return this->_pTextureResource; } void ReferenceCountedUnrealTexture::setTextureResource( - TSharedPtr&& p) { + FCesiumTextureResourceUniquePtr&& p) { this->_pTextureResource = std::move(p); } -CesiumGltf::SharedAsset -ReferenceCountedUnrealTexture::getSharedImage() const { - return this->_pImageCesium; -} - -void ReferenceCountedUnrealTexture::setSharedImage( - CesiumGltf::SharedAsset& image) { - this->_pImageCesium = image; -} - -TSharedPtr createTextureResourceFromImageCesium( - CesiumGltf::ImageCesium& imageCesium, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - EPixelFormat pixelFormat) { - if (GRHISupportsAsyncTextureCreation && !imageCesium.pixelData.empty()) { - // Create RHI texture resource on this worker - // thread, and then hand it off to the renderer - // thread. - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::CreateRHITexture2D) - - FTexture2DRHIRef textureReference = - CreateRHITexture2D_Async(imageCesium, pixelFormat, sRGB); - TSharedPtr textureResource = - MakeShared( - textureReference, - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); - - // Clear the now-unnecessary copy of the pixel data. - // Calling clear() isn't good enough because it - // won't actually release the memory. - std::vector pixelData; - imageCesium.pixelData.swap(pixelData); - - std::vector mipPositions; - imageCesium.mipPositions.swap(mipPositions); - - return textureResource; +std::optional getSourceIndexFromModelAndTexture( + const CesiumGltf::Model& model, + const CesiumGltf::Texture& texture) { + const CesiumGltf::ExtensionKhrTextureBasisu* pKtxExtension = + texture.getExtension(); + const CesiumGltf::ExtensionTextureWebp* pWebpExtension = + texture.getExtension(); + + int32_t source = -1; + if (pKtxExtension) { + if (pKtxExtension->source < 0 || + pKtxExtension->source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "KTX texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + pKtxExtension->source); + return std::nullopt; + } + return std::optional(pKtxExtension->source); + } else if (pWebpExtension) { + if (pWebpExtension->source < 0 || + pWebpExtension->source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "WebP texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + pWebpExtension->source); + return std::nullopt; + } + return std::optional(pWebpExtension->source); } else { - // The RHI texture will be created later on the - // render thread, directly from this texture source. - // We need valid pixelData here, though. - if (imageCesium.pixelData.empty()) { - return nullptr; + if (texture.source < 0 || texture.source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + texture.source); + return std::nullopt; } - - return MakeShared( - std::move(imageCesium), - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); + return std::optional(texture.source); } } -static std::mutex textureResourceMutex; - -struct ExtensionUnrealTextureResource { - static inline constexpr const char* TypeName = - "ExtensionUnrealTextureResource"; - static inline constexpr const char* ExtensionName = - "PRIVATE_unreal_texture_resource"; - - ExtensionUnrealTextureResource() {} - - TSharedPtr pTextureResource = nullptr; - - // If a preprocessing step is required (such as generating mipmaps), this - // future returns the preprocessed image. If no preprocessing is required, - // this just passes the image through. - std::optional> - preprocessFuture = std::nullopt; - - static TSharedPtr loadTextureResource( - CesiumGltf::ImageCesium& imageCesium, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - std::optional overridePixelFormat) { - std::optional optionalPixelFormat = - getPixelFormatForImageCesium(imageCesium, overridePixelFormat); - if (!optionalPixelFormat.has_value()) { - return nullptr; - } - - EPixelFormat pixelFormat = optionalPixelFormat.value(); - - std::lock_guard lock(textureResourceMutex); - - ExtensionUnrealTextureResource& extension = - imageCesium.addExtension(); - - // Already have a texture resource, just use that. - if (extension.pTextureResource != nullptr) { - return MakeShared( - extension.pTextureResource.Get(), - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); - } - - // Store the current size of the pixel data, because - // we're about to clear it but we still want to have - // an accurate estimation of the size of the image for - // caching purposes. - imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); - - TSharedPtr textureResource = - createTextureResourceFromImageCesium( - imageCesium, - addressX, - addressY, - filter, - useMipMapsIfAvailable, - group, - sRGB, - pixelFormat); - - check(textureResource != nullptr); - - extension.pTextureResource = textureResource; - - return textureResource; +TUniquePtr loadTextureFromModelAnyThreadPart( + CesiumGltf::Model& model, + CesiumGltf::Texture& texture, + bool sRGB) { + int64_t textureIndex = + model.textures.empty() ? -1 : &texture - &model.textures[0]; + if (textureIndex < 0 || size_t(textureIndex) >= model.textures.size()) { + textureIndex = -1; } - std::optional getSourceIndexFromModelAndTexture( - const CesiumGltf::Model& model, - const CesiumGltf::Texture& texture) { - const CesiumGltf::ExtensionKhrTextureBasisu* pKtxExtension = - texture.getExtension(); - const CesiumGltf::ExtensionTextureWebp* pWebpExtension = - texture.getExtension(); - - int32_t source = -1; - if (pKtxExtension) { - if (pKtxExtension->source < 0 || - pKtxExtension->source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "KTX texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - pKtxExtension->source); - return std::nullopt; - } - return std::optional(pKtxExtension->source); - } else if (pWebpExtension) { - if (pWebpExtension->source < 0 || - pWebpExtension->source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "WebP texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - pWebpExtension->source); - return std::nullopt; - } - return std::optional(pWebpExtension->source); - } else { - if (texture.source < 0 || texture.source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "Texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - texture.source); - return std::nullopt; - } - return std::optional(texture.source); - } + ExtensionUnrealTexture& extension = + texture.addExtension(); + if (extension.pTexture && (extension.pTexture->getUnrealTexture() || + extension.pTexture->getTextureResource())) { + // There's an existing Unreal texture for this glTF texture. This will + // happen if this texture is used by multiple primitives on the same + // model. It will also be the case when this model was upsampled from a + // parent tile. + TUniquePtr pResult = MakeUnique(); + pResult->pTexture = extension.pTexture; + pResult->textureIndex = textureIndex; + return pResult; } - TUniquePtr loadTextureFromModelAnyThreadPart( - CesiumGltf::Model& model, - CesiumGltf::Texture& texture, - bool sRGB) { - int64_t textureIndex = - model.textures.empty() ? -1 : &texture - &model.textures[0]; - if (textureIndex < 0 || size_t(textureIndex) >= model.textures.size()) { - textureIndex = -1; - } - - ExtensionUnrealTexture& extension = - texture.addExtension(); - if (extension.pTexture && (extension.pTexture->getUnrealTexture() || - extension.pTexture->getTextureResource())) { - // There's an existing Unreal texture for this glTF texture. This will - // happen if this texture is used by multiple primitives on the same - // model. It will also be the case when this model was upsampled from a - // parent tile. - TUniquePtr pResult = - MakeUnique(); - pResult->pTexture = extension.pTexture; - pResult->textureIndex = textureIndex; - return pResult; - } - - std::optional optionalSourceIndex = - getSourceIndexFromModelAndTexture(model, texture); - if (!optionalSourceIndex.has_value()) { - return nullptr; - }; - - CesiumGltf::Image& image = model.images[*optionalSourceIndex]; - const CesiumGltf::Sampler& sampler = - model.getSafe(model.samplers, texture.sampler); + std::optional optionalSourceIndex = + getSourceIndexFromModelAndTexture(model, texture); + if (!optionalSourceIndex.has_value()) { + return nullptr; + }; - TUniquePtr result = - loadTextureFromImageAndSamplerAnyThreadPart( - image.cesium, - sampler, - sRGB); + CesiumGltf::Image& image = model.images[*optionalSourceIndex]; + const CesiumGltf::Sampler& sampler = + model.getSafe(model.samplers, texture.sampler); - if (result) { - extension.pTexture = result->pTexture; - result->textureIndex = textureIndex; - } + TUniquePtr result = + loadTextureFromImageAndSamplerAnyThreadPart(image.cesium, sampler, sRGB); - return result; + if (result) { + extension.pTexture = result->pTexture; + result->textureIndex = textureIndex; } - TextureFilter - getTextureFilterFromSampler(const CesiumGltf::Sampler& sampler) { - // Unreal Engine's available filtering modes are only nearest, bilinear, - // trilinear, and "default". Default means "use the texture group settings", - // and the texture group settings are defined in a config file and can - // vary per platform. All filter modes can use mipmaps if they're available, - // but only TF_Default will ever use anisotropic texture filtering. - // - // Unreal also doesn't separate the minification filter from the - // magnification filter. So we'll just ignore the magFilter unless it's the - // only filter specified. - // - // Generally our bias is toward TF_Default, because that gives the user more - // control via texture groups. - - if (sampler.magFilter && !sampler.minFilter) { - // Only a magnification filter is specified, so use it. - return sampler.magFilter.value() == - CesiumGltf::Sampler::MagFilter::NEAREST - ? TextureFilter::TF_Nearest - : TextureFilter::TF_Default; - } else if (sampler.minFilter) { - // Use specified minFilter. - switch (sampler.minFilter.value()) { - case CesiumGltf::Sampler::MinFilter::NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - return TextureFilter::TF_Nearest; - case CesiumGltf::Sampler::MinFilter::LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - return TextureFilter::TF_Bilinear; - default: - return TextureFilter::TF_Default; - } - } else { - // No filtering specified at all, let the texture group decide. + return result; +} + +TextureFilter getTextureFilterFromSampler(const CesiumGltf::Sampler& sampler) { + // Unreal Engine's available filtering modes are only nearest, bilinear, + // trilinear, and "default". Default means "use the texture group settings", + // and the texture group settings are defined in a config file and can + // vary per platform. All filter modes can use mipmaps if they're available, + // but only TF_Default will ever use anisotropic texture filtering. + // + // Unreal also doesn't separate the minification filter from the + // magnification filter. So we'll just ignore the magFilter unless it's the + // only filter specified. + // + // Generally our bias is toward TF_Default, because that gives the user more + // control via texture groups. + + if (sampler.magFilter && !sampler.minFilter) { + // Only a magnification filter is specified, so use it. + return sampler.magFilter.value() == CesiumGltf::Sampler::MagFilter::NEAREST + ? TextureFilter::TF_Nearest + : TextureFilter::TF_Default; + } else if (sampler.minFilter) { + // Use specified minFilter. + switch (sampler.minFilter.value()) { + case CesiumGltf::Sampler::MinFilter::NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return TextureFilter::TF_Nearest; + case CesiumGltf::Sampler::MinFilter::LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + return TextureFilter::TF_Bilinear; + default: return TextureFilter::TF_Default; } + } else { + // No filtering specified at all, let the texture group decide. + return TextureFilter::TF_Default; } +} - bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { - switch (sampler.minFilter.value_or( - CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - return true; - break; - default: // LINEAR and NEAREST - return false; - } +bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { + switch (sampler.minFilter.value_or( + CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return true; + break; + default: // LINEAR and NEAREST + return false; } +} + +TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( + CesiumGltf::SharedAsset& image, + const CesiumGltf::Sampler& sampler, + bool sRGB) { + return loadTextureAnyThreadPart( + image, + convertGltfWrapSToUnreal(sampler.wrapS), + convertGltfWrapTToUnreal(sampler.wrapT), + getTextureFilterFromSampler(sampler), + getUseMipmapsIfAvailableFromSampler(sampler), + // TODO: allow texture group to be configured on Cesium3DTileset. + TEXTUREGROUP_World, + sRGB, + std::nullopt); +} - TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::SharedAsset& image, - const CesiumGltf::Sampler& sampler, - bool sRGB) { - return loadTextureAnyThreadPart( - image, - convertGltfWrapSToUnreal(sampler.wrapS), - convertGltfWrapTToUnreal(sampler.wrapT), - getTextureFilterFromSampler(sampler), - getUseMipmapsIfAvailableFromSampler(sampler), - // TODO: allow texture group to be configured on Cesium3DTileset. - TEXTUREGROUP_World, - sRGB, - std::nullopt); +static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { + if (!pHalfLoadedTexture || !pHalfLoadedTexture->pTexture) { + return nullptr; } - static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { - if (!pHalfLoadedTexture || !pHalfLoadedTexture->pTexture) { - return nullptr; - } + UTexture2D* pTexture = pHalfLoadedTexture->pTexture->getUnrealTexture(); + if (!pTexture) { + pTexture = NewObject( + GetTransientPackage(), + MakeUniqueObjectName( + GetTransientPackage(), + UTexture2D::StaticClass(), + "CesiumRuntimeTexture"), + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + pTexture->AddressX = pHalfLoadedTexture->addressX; + pTexture->AddressY = pHalfLoadedTexture->addressY; + pTexture->Filter = pHalfLoadedTexture->filter; + pTexture->LODGroup = pHalfLoadedTexture->group; + pTexture->SRGB = pHalfLoadedTexture->sRGB; + + pTexture->NeverStream = true; + + pHalfLoadedTexture->pTexture->setUnrealTexture(pTexture); + } - UTexture2D* pTexture = pHalfLoadedTexture->pTexture->getUnrealTexture(); - if (!pTexture) { - pTexture = NewObject( - GetTransientPackage(), - MakeUniqueObjectName( - GetTransientPackage(), - UTexture2D::StaticClass(), - "CesiumRuntimeTexture"), - RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); - - pTexture->AddressX = pHalfLoadedTexture->addressX; - pTexture->AddressY = pHalfLoadedTexture->addressY; - pTexture->Filter = pHalfLoadedTexture->filter; - pTexture->LODGroup = pHalfLoadedTexture->group; - pTexture->SRGB = pHalfLoadedTexture->sRGB; - - pTexture->NeverStream = true; - - pHalfLoadedTexture->pTexture->setUnrealTexture(pTexture); - } + return pTexture; +} - return pTexture; +TUniquePtr loadTextureAnyThreadPart( + CesiumGltf::SharedAsset& image, + TextureAddress addressX, + TextureAddress addressY, + TextureFilter filter, + bool useMipMapsIfAvailable, + TextureGroup group, + bool sRGB, + std::optional overridePixelFormat) { + // The FCesiumTextureResource for the ImageCesium should already be created at + // this point, if it can be. + ExtensionImageCesiumUnreal* pExtension = + image->getExtension(); + if (pExtension == nullptr || pExtension->getTextureResource() == nullptr) { + return nullptr; } - TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::SharedAsset& image, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - std::optional overridePixelFormat) { - TSharedPtr textureResource = - ExtensionUnrealTextureResource::loadTextureResource( - *image, - addressX, - addressY, - filter, - useMipMapsIfAvailable, - group, - sRGB, - overridePixelFormat); + auto pResource = FCesiumTextureResource::CreateWrapped( + pExtension->getTextureResource(), + group, + filter, + addressX, + addressY, + sRGB, + useMipMapsIfAvailable); + + TUniquePtr pResult = MakeUnique(); + pResult->pTexture = new ReferenceCountedUnrealTexture(); + + pResult->addressX = addressX; + pResult->addressY = addressY; + pResult->filter = filter; + pResult->group = group; + pResult->sRGB = sRGB; + pResult->pTexture->setTextureResource(MoveTemp(pResource)); + + return pResult; +} - TUniquePtr pResult = MakeUnique(); - pResult->pTexture = new ReferenceCountedUnrealTexture(); +CesiumUtility::IntrusivePointer +loadTextureGameThreadPart( + CesiumGltf::Model& model, + LoadedTextureResult* pHalfLoadedTexture) { + if (pHalfLoadedTexture == nullptr) + return nullptr; - pResult->addressX = addressX; - pResult->addressY = addressY; - pResult->filter = filter; - pResult->group = group; - pResult->sRGB = sRGB; + CesiumUtility::IntrusivePointer pResult = + loadTextureGameThreadPart(pHalfLoadedTexture); - pResult->pTexture->setTextureResource(MoveTemp(textureResource)); - pResult->pTexture->setSharedImage(image); - return pResult; + if (pResult && pHalfLoadedTexture && pHalfLoadedTexture->textureIndex >= 0 && + size_t(pHalfLoadedTexture->textureIndex) < model.textures.size()) { + CesiumGltf::Texture& texture = + model.textures[pHalfLoadedTexture->textureIndex]; + ExtensionUnrealTexture& extension = + texture.addExtension(); + extension.pTexture = pHalfLoadedTexture->pTexture; } - CesiumUtility::IntrusivePointer - loadTextureGameThreadPart( - CesiumGltf::Model& model, - LoadedTextureResult* pHalfLoadedTexture) { - if (pHalfLoadedTexture == nullptr) - return nullptr; - - CesiumUtility::IntrusivePointer pResult = - loadTextureGameThreadPart(pHalfLoadedTexture); - - if (pResult && pHalfLoadedTexture && - pHalfLoadedTexture->textureIndex >= 0 && - size_t(pHalfLoadedTexture->textureIndex) < model.textures.size()) { - CesiumGltf::Texture& texture = - model.textures[pHalfLoadedTexture->textureIndex]; - ExtensionUnrealTexture& extension = - texture.addExtension(); - extension.pTexture = pHalfLoadedTexture->pTexture; - } + return pHalfLoadedTexture->pTexture; +} +CesiumUtility::IntrusivePointer +loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTexture) + + FCesiumTextureResourceUniquePtr& pTextureResource = + pHalfLoadedTexture->pTexture->getTextureResource(); + if (pTextureResource == nullptr) { + // Texture is already loaded (or unloadable). return pHalfLoadedTexture->pTexture; } - CesiumUtility::IntrusivePointer - loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTexture) - - TSharedPtr& pTextureResource = - pHalfLoadedTexture->pTexture->getTextureResource(); - if (pTextureResource == nullptr) { - // Texture is already loaded (or unloadable). - return pHalfLoadedTexture->pTexture; - } - - UTexture2D* pTexture = CreateTexture2D(pHalfLoadedTexture); - if (pTexture == nullptr) { - return nullptr; - } + UTexture2D* pTexture = CreateTexture2D(pHalfLoadedTexture); + if (pTexture == nullptr) { + return nullptr; + } - FCesiumTextureResourceBase* pCesiumTextureResource = - pTextureResource.Release(); - if (pCesiumTextureResource) { - pTexture->SetResource(pCesiumTextureResource); + if (pTextureResource) { + // Give the UTexture2D exclusive ownership of this FCesiumTextureResource. + pTexture->SetResource(pTextureResource.Release()); - ENQUEUE_RENDER_COMMAND(Cesium_InitResource) - ([pTexture, - pCesiumTextureResource](FRHICommandListImmediate& RHICmdList) { - pCesiumTextureResource->SetTextureReference( - pTexture->TextureReference.TextureReferenceRHI); + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture, pTextureResource = pTexture->GetResource()]( + FRHICommandListImmediate& RHICmdList) { + pTextureResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); #if ENGINE_VERSION_5_3_OR_HIGHER - pCesiumTextureResource->InitResource( - FRHICommandListImmediate::Get()); // Init Resource now requires a - // command list. + pCesiumTextureResource->InitResource( + FRHICommandListImmediate::Get()); // Init Resource now requires a + // command list. #else - pCesiumTextureResource->InitResource(); + pTextureResource->InitResource(); #endif - }); - } - - return pHalfLoadedTexture->pTexture; + }); } - TSharedPtr createTextureResource( - CesiumGltf::ImageCesium& imageCesium, - bool sRGB, - std::optional overridePixelFormat) { - std::optional optionalPixelFormat = - getPixelFormatForImageCesium(imageCesium, overridePixelFormat); - if (!optionalPixelFormat.has_value()) { - return nullptr; - } + return pHalfLoadedTexture->pTexture; +} - EPixelFormat pixelFormat = optionalPixelFormat.value(); - - // Store the current size of the pixel data, because - // we're about to clear it but we still want to have - // an accurate estimation of the size of the image for - // caching purposes. - imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); - - return createTextureResourceFromImageCesium( - imageCesium, - TextureAddress::TA_Wrap, - TextureAddress::TA_Wrap, - TextureFilter::TF_Default, - true, - TextureGroup::TEXTUREGROUP_World, - sRGB, - pixelFormat); - } - - TextureAddress convertGltfWrapSToUnreal(int32_t wrapS) { - // glTF spec: "When undefined, a sampler with repeat wrapping and auto - // filtering should be used." - switch (wrapS) { - case CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE: - return TextureAddress::TA_Clamp; - case CesiumGltf::Sampler::WrapS::MIRRORED_REPEAT: - return TextureAddress::TA_Mirror; - case CesiumGltf::Sampler::WrapS::REPEAT: - default: - return TextureAddress::TA_Wrap; - } +TextureAddress convertGltfWrapSToUnreal(int32_t wrapS) { + // glTF spec: "When undefined, a sampler with repeat wrapping and auto + // filtering should be used." + switch (wrapS) { + case CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE: + return TextureAddress::TA_Clamp; + case CesiumGltf::Sampler::WrapS::MIRRORED_REPEAT: + return TextureAddress::TA_Mirror; + case CesiumGltf::Sampler::WrapS::REPEAT: + default: + return TextureAddress::TA_Wrap; } +} - TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { - // glTF spec: "When undefined, a sampler with repeat wrapping and auto - // filtering should be used." - switch (wrapT) { - case CesiumGltf::Sampler::WrapT::CLAMP_TO_EDGE: - return TextureAddress::TA_Clamp; - case CesiumGltf::Sampler::WrapT::MIRRORED_REPEAT: - return TextureAddress::TA_Mirror; - case CesiumGltf::Sampler::WrapT::REPEAT: - default: - return TextureAddress::TA_Wrap; - } +TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { + // glTF spec: "When undefined, a sampler with repeat wrapping and auto + // filtering should be used." + switch (wrapT) { + case CesiumGltf::Sampler::WrapT::CLAMP_TO_EDGE: + return TextureAddress::TA_Clamp; + case CesiumGltf::Sampler::WrapT::MIRRORED_REPEAT: + return TextureAddress::TA_Mirror; + case CesiumGltf::Sampler::WrapT::REPEAT: + default: + return TextureAddress::TA_Wrap; } +} - std::optional> - createMipMapsForSampler( - const CesiumAsync::AsyncSystem& asyncSystem, - const CesiumGltf::Sampler& sampler, - CesiumGltf::ImageCesium& image) { - std::unique_lock lock(textureResourceMutex); - - ExtensionUnrealTextureResource& extension = - image.addExtension(); - - // Future already exists, we don't need to do anything else. - if (extension.preprocessFuture.has_value()) { - return extension.preprocessFuture.value(); - } - - // Generate mipmaps if needed. - // An image needs mipmaps generated for it if: - // 1. It is used by a Texture that has a Sampler with a mipmap filtering - // mode, and - // 2. It does not already have mipmaps. - // It's ok if an image has mipmaps even if not all textures will use them. - // There's no reason to have two RHI textures, one with and one without - // mips. - bool needsMipmaps; - switch (sampler.minFilter.value_or( - CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - needsMipmaps = true; +std::optional getPixelFormatForImageCesium( + const ImageCesium& imageCesium, + const std::optional overridePixelFormat) { + if (imageCesium.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { + switch (imageCesium.compressedPixelFormat) { + case GpuCompressedPixelFormat::ETC1_RGB: + return EPixelFormat::PF_ETC1; break; - default: // LINEAR and NEAREST - needsMipmaps = false; + case GpuCompressedPixelFormat::ETC2_RGBA: + return EPixelFormat::PF_ETC2_RGBA; break; - } - - if (!needsMipmaps || image.pixelData.empty()) { - // If we don't need mipmaps, we don't want to create a future for them. - // This allows a future sampler using this image that does need mipmaps to - // generate them. + case GpuCompressedPixelFormat::BC1_RGB: + return EPixelFormat::PF_DXT1; + break; + case GpuCompressedPixelFormat::BC3_RGBA: + return EPixelFormat::PF_DXT5; + break; + case GpuCompressedPixelFormat::BC4_R: + return EPixelFormat::PF_BC4; + break; + case GpuCompressedPixelFormat::BC5_RG: + return EPixelFormat::PF_BC5; + break; + case GpuCompressedPixelFormat::BC7_RGBA: + return EPixelFormat::PF_BC7; + break; + case GpuCompressedPixelFormat::ASTC_4x4_RGBA: + return EPixelFormat::PF_ASTC_4x4; + break; + case GpuCompressedPixelFormat::PVRTC2_4_RGBA: + return EPixelFormat::PF_PVRTC2; + break; + case GpuCompressedPixelFormat::ETC2_EAC_R11: + return EPixelFormat::PF_ETC2_R11_EAC; + break; + case GpuCompressedPixelFormat::ETC2_EAC_RG11: + return EPixelFormat::PF_ETC2_RG11_EAC; + break; + default: + // Unsupported compressed texture format. return std::nullopt; - } - - CesiumAsync::Promise promise = - asyncSystem.createPromise(); - - extension.preprocessFuture = promise.getFuture().share(); - - lock.unlock(); - - // We need mipmaps generated. - std::optional errorMessage = - CesiumGltfReader::GltfReader::generateMipMaps(image); - if (errorMessage) { - UE_LOG( - LogCesium, - Warning, - TEXT("%s"), - UTF8_TO_TCHAR(errorMessage->c_str())); - } - promise.resolve(&image); - return *extension.preprocessFuture; + }; + } else if (overridePixelFormat) { + return *overridePixelFormat; + } else { + switch (imageCesium.channels) { + case 1: + return PF_R8; + break; + case 2: + return PF_R8G8; + break; + case 3: + case 4: + default: + return PF_R8G8B8A8; + }; } - CesiumAsync::SharedFuture createMipMapsForAllTextures( - const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::Model& model) { - std::vector> futures; - for (const Texture& texture : model.textures) { - const CesiumGltf::Sampler& sampler = - model.getSafe(model.samplers, texture.sampler); - std::optional> - optionalFuture = createMipMapsForSampler( - asyncSystem, - sampler, - *model.images[texture.source].cesium); - if (optionalFuture.has_value()) { - futures.push_back(optionalFuture.value()); - } - } - - return asyncSystem.all(std::move(futures)) - .thenImmediately( - []([[maybe_unused]] std::vector< - CesiumGltf::ImageCesium*>&& /*results*/) -> void {}) - .share(); - } + return std::nullopt; +} } // namespace CesiumTextureUtility diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 767b6da4e..e037c4e95 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -47,18 +47,13 @@ struct ReferenceCountedUnrealTexture void setUnrealTexture(const TObjectPtr& p); // The renderer / RHI FTextureResource holding the pixel data. - const TSharedPtr& getTextureResource() const; - TSharedPtr& getTextureResource(); - void setTextureResource(TSharedPtr&& p); - - /// The SharedAsset that this texture was created from. - CesiumGltf::SharedAsset getSharedImage() const; - void setSharedImage(CesiumGltf::SharedAsset& image); + const FCesiumTextureResourceUniquePtr& getTextureResource() const; + FCesiumTextureResourceUniquePtr& getTextureResource(); + void setTextureResource(FCesiumTextureResourceUniquePtr&& p); private: TObjectPtr _pUnrealTexture; - TSharedPtr _pTextureResource; - CesiumGltf::SharedAsset _pImageCesium; + FCesiumTextureResourceUniquePtr _pTextureResource; }; /** @@ -191,7 +186,7 @@ loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture); * intended to be later used with `FCesiumUseExistingTextureResource`, which * will supply sampler, texture group, and other settings. */ -TSharedPtr createTextureResource( +TSharedPtr createTextureResource( CesiumGltf::ImageCesium& imageCesium, bool sRGB, std::optional overridePixelFormat); @@ -216,8 +211,8 @@ TextureAddress convertGltfWrapSToUnreal(int32_t wrapS); */ TextureAddress convertGltfWrapTToUnreal(int32_t wrapT); -CesiumAsync::SharedFuture createMipMapsForAllTextures( - const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::Model& model); +std::optional getPixelFormatForImageCesium( + const CesiumGltf::ImageCesium& imageCesium, + const std::optional overridePixelFormat); } // namespace CesiumTextureUtility diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp new file mode 100644 index 000000000..e77409a24 --- /dev/null +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp @@ -0,0 +1,101 @@ +#include "ExtensionImageCesiumUnreal.h" +#include "CesiumRuntime.h" +#include "CesiumTextureUtility.h" +#include +#include + +using namespace CesiumAsync; +using namespace CesiumGltf; +using namespace CesiumGltfReader; + +namespace { + +std::mutex createExtensionMutex; + +std::pair>> +getOrCreateImageFuture( + const AsyncSystem& asyncSystem, + ImageCesium& imageCesium); + +} // namespace + +/*static*/ const ExtensionImageCesiumUnreal& +ExtensionImageCesiumUnreal::GetOrCreate( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::ImageCesium& imageCesium, + bool needsMipMaps, + const std::optional& overridePixelFormat) { + auto [extension, maybePromise] = + getOrCreateImageFuture(asyncSystem, imageCesium); + if (!maybePromise) { + // Another thread is already working on this image. + return extension; + } + + // Proceed to load the image in this thread. + TUniquePtr pResource = + FCesiumTextureResource::CreateNew( + imageCesium, + TextureGroup::TEXTUREGROUP_World, + overridePixelFormat, + TextureFilter::TF_Default, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false, + needsMipMaps); + + extension._pTextureResource = + MakeShareable(pResource.Release(), [](FCesiumTextureResource* p) { + FCesiumTextureResource ::Destroy(p); + }); + + maybePromise->resolve(); + + return extension; +} + +ExtensionImageCesiumUnreal::ExtensionImageCesiumUnreal( + const CesiumAsync::SharedFuture& future) + : _pTextureResource(nullptr), _futureCreateResource(future) {} + +const TSharedPtr& +ExtensionImageCesiumUnreal::getTextureResource() const { + return this->_pTextureResource; +} + +CesiumAsync::SharedFuture& ExtensionImageCesiumUnreal::getFuture() { + return this->_futureCreateResource; +} + +const CesiumAsync::SharedFuture& +ExtensionImageCesiumUnreal::getFuture() const { + return this->_futureCreateResource; +} + +namespace { + +// Returns a Future that will resolve when the image is loaded. It _may_ also +// return a Promise, in which case the calling thread is responsible for doing +// the loading and should resolve the Promise when it's done. +std::pair>> +getOrCreateImageFuture( + const AsyncSystem& asyncSystem, + ImageCesium& imageCesium) { + std::scoped_lock lock(createExtensionMutex); + + ExtensionImageCesiumUnreal* pExtension = + imageCesium.getExtension(); + if (!pExtension) { + // This thread will work on this image. + Promise promise = asyncSystem.createPromise(); + ExtensionImageCesiumUnreal& extension = + imageCesium.addExtension( + promise.getFuture().share()); + return {extension, std::move(promise)}; + } else { + // Another thread is already working on this image. + return {*pExtension, std::nullopt}; + } +} + +} // namespace diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h new file mode 100644 index 000000000..84b7618a6 --- /dev/null +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h @@ -0,0 +1,68 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumTextureResource.h" +#include "PixelFormat.h" +#include "Templates/SharedPointer.h" +#include +#include + +namespace CesiumGltf { +struct ImageCesium; +} + +/** + * @brief An extension attached to an ImageCesium in order to hold + * Unreal-specific information about it. + * + * ImageCesium instances are shared between multiple textures on a single model, + * and even between models in some cases, but we strive to have only one copy of + * the image bytes in GPU memory. + * + * The Unreal / GPU resource is held in `pTextureResource`, which may be either + * a `FCesiumCreateNewTextureResource` or a `FCesiumUseExistingTextureResource` + * depending on how it was created. We'll never actually sample directly from + * this resource, however. Instead, a separate + * `FCesiumUseExistingTextureResource` will be created for each glTF Texture + * that references this image, and it will point to the instance managed by this + * extension. + * + * Because we'll never be sampling from this texture resource, the texture + * filtering and addressing parameters have default values. + */ +struct ExtensionImageCesiumUnreal { + static inline constexpr const char* TypeName = "ExtensionImageCesiumUnreal"; + static inline constexpr const char* ExtensionName = + "PRIVATE_ImageCesium_Unreal"; + + /** + * @brief Gets an Unreal texture resource from the given `ImageCesium`, + * creating it if necessary. + * + * When this function is called for the first time on a particular + * `ImageCesium`, the asynchronous process to create an Unreal + * `FTextureResource` from it is kicked off. On successive invocations + * (perhaps from other threads), the existing instance is returned. It is safe + * to call this method on the same `ImageCesium` instance from multiple + * threads simultaneously. + * + * To determine if the asynchronous `FTextureResource` creation process has + * completed, use {@link getFuture}. + */ + static const ExtensionImageCesiumUnreal& GetOrCreate( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::ImageCesium& imageCesium, + bool needsMipMaps, + const std::optional& overridePixelFormat); + + ExtensionImageCesiumUnreal(const CesiumAsync::SharedFuture& future); + + const TSharedPtr& getTextureResource() const; + CesiumAsync::SharedFuture& getFuture(); + const CesiumAsync::SharedFuture& getFuture() const; + +private: + TSharedPtr _pTextureResource; + CesiumAsync::SharedFuture _futureCreateResource; +}; diff --git a/extern/cesium-native b/extern/cesium-native index 6de6b3860..86995ffd4 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 6de6b386035be9cee3e4ede5c50391acd27f921e +Subproject commit 86995ffd49c33774ea3a7b6f3613cfeebd402e2e