diff --git a/.agent/plans/resource_shared_ownership_architecture.md b/.agent/plans/resource_shared_ownership_architecture.md index 52190240d..45bdc5e74 100644 --- a/.agent/plans/resource_shared_ownership_architecture.md +++ b/.agent/plans/resource_shared_ownership_architecture.md @@ -1,7 +1,7 @@ # eepp shared-resource ownership architecture -Status: active implementation baseline; Stage 0, prerequisite fixes, Stage 1, and Stage 2 complete; -Stage 3 is next, 2026-07-19. +Status: active implementation baseline; Stage 0 through Stage 3 complete; Stage 4 is next, +2026-07-19. This document freezes the contracts that must be true before the public texture API is changed. The implementation may refine names and small mechanics, but changing an invariant below requires an @@ -19,9 +19,9 @@ The final model is: reporting. It is never searched for semantic names. - Catalogs define names and persistence. - Scopes define which catalogs and typed caches are visible. -- GPU resources remain graphics-thread-affine. The project contract requires final owning releases - and destruction to run through the graphics/update lifecycle rather than supporting arbitrary - last-release threads. +- GPU resources remain graphics-thread-affine. A final owning release may happen on a worker, but + the texture deleter only performs a thread-safe handoff to TextureFactory. Actual destruction runs + through the graphics/display lifecycle. - UI drawable resolution is layered over Graphics resource lookup; browser caching and navigation remain outside Graphics. - A UISceneNode can own a scope and resolver, but neither texture lifetime nor pure Graphics usage @@ -105,10 +105,10 @@ Factories use an equivalent private helper for protected constructors. `std::mak used for tracked eepp resources unless the memory manager is redesigned to understand its combined allocation. No second control block may be created from `handle.get()`. -Texture is the deliberate exception to immediate `eeDelete`: its factory-controlled deleter queues -the final raw object for graphics-thread destruction. `TextureFactory::collectReleasedTextures()` -performs the eventual `eeDelete` after queued rendering has been flushed. This is the same deferred -destruction contract used by scene nodes; it is not a general arbitrary-thread GPU disposal system. +Texture is the deliberate exception to immediate `eeDelete`: its factory-controlled deleter may +queue the final raw object from any thread. `TextureFactory::collectReleasedTextures()` performs the +eventual `eeDelete` on the graphics thread after queued rendering has been flushed. This is the same +deferred destruction contract used by scene nodes; it is not a general GPU disposal system. ### 3.3 Identity, keys, and labels @@ -270,14 +270,14 @@ performs the same collection explicitly because no later display is guaranteed. ### 5.1 Graphics-thread lifetime contract -GPU resources are graphics-thread-affine. Creating, mutating and finally releasing owning handles -must follow the engine's graphics/update lifecycle. `std::shared_ptr` provides ownership safety; it -does not expand eepp's supported threading contract. Async CPU decoding may run elsewhere, but -ownership handoff and final release are marshalled to the main/scene update path unless an existing -API explicitly acquires a shared GL context. +GPU operations remain graphics-context-affine and follow the existing graphics/update or explicitly +shared-context rules. Releasing the final `TexturePtr` is different: it performs no GPU operation and +may enqueue the raw texture from any thread. Only collection and actual destruction require the +graphics thread and a current context. -Debug builds should assert this contract at factory release and collection boundaries. The design -does not add a generic device state, epoch or arbitrary-thread disposal queue for unsupported usage. +Debug builds assert the collection boundary. The design does not add a generic device state, epoch, +or disposal mechanism for other GPU resource families; TextureFactory's small deferred-release queue +is the texture-specific lifetime boundary already required by batched rendering. ### 5.2 Texture deferred destruction @@ -571,7 +571,7 @@ Exit tests: - Pending batches flush before texture collection. - Engine teardown leaves no pending released textures or unexpected live registry entries. - Repeated test-only Engine create/destroy cycles start with empty resource state. -- Wrong-thread final release triggers the documented debug contract assertion. +- Worker-thread final release only queues the texture; it does not run GL or destruction work. - `EE_MEMORY_MANAGER` accurately removes texture allocations through the factory-controlled deleter. ### Stage 2: one complete TexturePtr ownership cut @@ -619,6 +619,15 @@ Exit criteria: ### Stage 3: catalog and scope ownership cutover +Status: complete, 2026-07-19. Engine now owns the global catalog and default Graphics scope; +UISceneNode owns an isolated scope that can be shared explicitly. TextureFactory is an unpinned +creator and weak live registry with no semantic name/hash lookup. Atlas, map, UI image/background, +DrawableSearcher, ecode, tests, and other name-based consumers publish to and resolve through their +explicit scope. Catalog aliases and imports provide intentional persistence and deterministic +sharing. Worker-thread final TexturePtr release is handed to the factory's thread-safe queue and +actual deletion remains display/shutdown-bound. The full cut also corrected TextureLoader's decoder +pixel allocator provenance, which asynchronous scoped loading exposed. + Implement the global catalog, default Graphics scope, application/scene catalogs, explicit imports, immutable keys, and aliases. Move intended persistent resources from temporary factory retention into catalogs/caches. Remove factory-wide strong retention and activate final unpinned creation. @@ -703,7 +712,7 @@ Remove raw-owning `ResourceManager` only when no subclass or consumer depends ### GPU/thread lifetime - Final texture release on the graphics thread queues rather than immediately deleting. -- Wrong-thread final release is detected as a project-contract violation in debug builds. +- Worker-thread final release safely queues; display performs the destruction with a current context. - Display flushes batches before collecting released textures under the current context. - Engine shutdown performs a final collection before TextureFactory/Renderer/context destruction. - No deletion/callback occurs while registry/cache locks are held. diff --git a/include/eepp/graphics.hpp b/include/eepp/graphics.hpp index 4cd5df98a..52cc62dc9 100644 --- a/include/eepp/graphics.hpp +++ b/include/eepp/graphics.hpp @@ -47,6 +47,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/include/eepp/graphics/drawablesearcher.hpp b/include/eepp/graphics/drawablesearcher.hpp index 81efb4da8..b42d1e20c 100644 --- a/include/eepp/graphics/drawablesearcher.hpp +++ b/include/eepp/graphics/drawablesearcher.hpp @@ -7,10 +7,13 @@ namespace EE { namespace Graphics { +class ResourceScope; + class EE_API DrawableSearcher { public: static Drawable* searchByName( const std::string& name, bool firstSearchSprite = false, - Network::URI referer = "" ); + Network::URI referer = "", + ResourceScope* resourceScope = nullptr ); static Drawable* searchById( const Uint32& id ); diff --git a/include/eepp/graphics/resource.hpp b/include/eepp/graphics/resource.hpp index 8158269b3..afd5979a3 100644 --- a/include/eepp/graphics/resource.hpp +++ b/include/eepp/graphics/resource.hpp @@ -2,6 +2,8 @@ #define EE_GRAPHICS_RESOURCE_HPP #include +#include +#include #include @@ -25,6 +27,22 @@ class ResourceId { Uint64 mValue{ 0 }; }; +/** Immutable semantic lookup key. Catalog equality always compares the complete key value. */ +class ResourceKey { + public: + ResourceKey() = default; + explicit ResourceKey( std::string value ) : mValue( std::move( value ) ) {} + + const std::string& value() const { return mValue; } + bool empty() const { return mValue.empty(); } + + bool operator==( const ResourceKey& other ) const { return mValue == other.mValue; } + bool operator!=( const ResourceKey& other ) const { return !( *this == other ); } + + private: + std::string mValue; +}; + template using ResourcePtr = std::shared_ptr; template using ResourceWeakPtr = std::weak_ptr; diff --git a/include/eepp/graphics/resourcecatalog.hpp b/include/eepp/graphics/resourcecatalog.hpp new file mode 100644 index 000000000..82bf92ed9 --- /dev/null +++ b/include/eepp/graphics/resourcecatalog.hpp @@ -0,0 +1,37 @@ +#ifndef EE_GRAPHICS_RESOURCECATALOG_HPP +#define EE_GRAPHICS_RESOURCECATALOG_HPP + +#include +#include +#include +#include + +namespace EE { namespace Graphics { + +class ResourceCatalog; +using ResourceCatalogPtr = ResourcePtr; + +/** Strong, named ownership for Graphics resources. The live registry is never searched here. */ +class EE_API ResourceCatalog { + public: + static ResourceCatalogPtr New(); + + void publish( ResourceKey key, TexturePtr texture ); + void publish( std::string key, TexturePtr texture ); + + TexturePtr findTexture( const ResourceKey& key ) const; + TexturePtr findTexture( const std::string& key ) const; + + bool erase( const ResourceKey& key ); + bool erase( const std::string& key ); + void clear(); + std::size_t size() const; + + private: + mutable System::Mutex mMutex; + UnorderedMap mTextures; +}; + +}} // namespace EE::Graphics + +#endif diff --git a/include/eepp/graphics/resourcescope.hpp b/include/eepp/graphics/resourcescope.hpp new file mode 100644 index 000000000..5d6719ae6 --- /dev/null +++ b/include/eepp/graphics/resourcescope.hpp @@ -0,0 +1,45 @@ +#ifndef EE_GRAPHICS_RESOURCESCOPE_HPP +#define EE_GRAPHICS_RESOURCESCOPE_HPP + +#include + +namespace EE { namespace Graphics { + +class ResourceScope; +using ResourceScopePtr = ResourcePtr; + +/** Graphics-only semantic lookup boundary with a local catalog and explicit catalog imports. */ +class EE_API ResourceScope { + public: + static ResourceScopePtr New(); + + ResourceScope(); + + TexturePtr findTexture( const ResourceKey& key ) const; + TexturePtr findTexture( const std::string& key ) const; + + void publishLocal( ResourceKey key, TexturePtr texture ); + void publishLocal( std::string key, TexturePtr texture ); + bool eraseLocal( const ResourceKey& key ); + bool eraseLocal( const std::string& key ); + void clearLocal(); + + void importCatalog( ResourceCatalogPtr catalog ); + bool removeCatalog( const ResourceCatalogPtr& catalog ); + void clearImports(); + + ResourceCatalogPtr getLocalCatalog() const; + + private: + ResourceCatalogPtr mLocalCatalog; + std::vector mImports; + mutable System::Mutex mMutex; +}; + +/** Engine-owned process defaults for pure Graphics and legacy application-wide resolution. */ +EE_API ResourceCatalog& globalResourceCatalog(); +EE_API ResourceScope& defaultResourceScope(); + +}} // namespace EE::Graphics + +#endif diff --git a/include/eepp/graphics/textureatlasloader.hpp b/include/eepp/graphics/textureatlasloader.hpp index 451418fca..de5bd70b7 100644 --- a/include/eepp/graphics/textureatlasloader.hpp +++ b/include/eepp/graphics/textureatlasloader.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -161,6 +162,11 @@ class EE_API TextureAtlasLoader { void setTextureFilter( const Texture::Filter& textureFilter ); + /** Sets the semantic texture lookup boundary used by subsequent loads. */ + void setResourceScope( ResourceScopePtr resourceScope ); + + const ResourceScopePtr& getResourceScope() const; + protected: std::string mTextureAtlasPath; bool mThreaded; @@ -170,6 +176,7 @@ class EE_API TextureAtlasLoader { std::atomic mIsLoading; TextureAtlas* mTextureAtlas; GLLoadCallback mLoadCallback; + ResourceScopePtr mResourceScope; std::vector mTexturesLoaded; struct sTempTexAtlas { diff --git a/include/eepp/graphics/texturefactory.hpp b/include/eepp/graphics/texturefactory.hpp index 71a9874c3..ed8fe5389 100644 --- a/include/eepp/graphics/texturefactory.hpp +++ b/include/eepp/graphics/texturefactory.hpp @@ -21,7 +21,7 @@ struct TextureRegistryRecord { using TextureRegistrySnapshot = std::vector; -/** @brief The Texture Manager Class. Here we do all the textures stuff. (Singleton Class) */ +/** Creates textures and weakly observes every live texture for diagnostics. */ class EE_API TextureFactory : protected Mutex { SINGLETON_DECLARE_HEADERS( TextureFactory ) @@ -172,7 +172,7 @@ class EE_API TextureFactory : protected Mutex { */ void setCurrentTexture( const int& textureHandle, const Uint32& TextureUnit ); - /** Returns the number of textures loaded */ + /** Returns the number of currently live textures. */ Uint32 getTextureCount(); /** @return A non-owning diagnostic snapshot of every currently live texture. */ @@ -201,10 +201,10 @@ class EE_API TextureFactory : protected Mutex { */ unsigned int getValidTextureSize( const unsigned int& Size ); - /** Determines whether the texture identity exists in the factory. */ + /** Determines whether the texture identity is currently live. */ bool existsId( ResourceId textureId ); - /** @return The texture matching @p textureId, or null if it is not factory-retained. */ + /** @return The live texture matching @p textureId, or null if it has expired. */ TexturePtr getTexture( ResourceId textureId ); /** @return The memory used by the textures (in bytes) */ @@ -233,32 +233,17 @@ class EE_API TextureFactory : protected Mutex { const Texture::ClampMode& ClampMode, const bool& CompressTexture, const bool& LocalCopy = false, const Uint32& MemSize = 0 ); - /** Return a texture by it file path name - * @param Name File path name - * @return The texture, NULL if not exists. - */ - TexturePtr getByName( const std::string& Name ); - - /** Return a texture by it hash path name - * @param Hash The file path hash - * @return The texture, NULL if not exists - */ - TexturePtr getByHash( const String::HashType& hash ); - ~TextureFactory(); const Texture::CoordinateType& getLastCoordinateType() const; protected: friend class Texture; - friend class TextureLoader; TextureFactory(); std::vector mCurrentTexture; - using TextureMap = UnorderedMap; - struct LiveTextureRecord { ResourceId id; TextureWeakPtr texture; @@ -268,19 +253,16 @@ class EE_API TextureFactory : protected Mutex { void operator()( Texture* texture ) const noexcept; }; - TextureMap mTextures; UnorderedMap mLiveTextures; std::vector mReleasedTextures; std::atomic mLiveTextureGeneration{ 0 }; Texture::CoordinateType mLastCoordinateType; - void unloadTextures(); - void resetTextureBinding( const Texture* texture ); - bool releaseRetainedTexture( ResourceId textureId ); - + /** Thread-safe final-handle handoff. Actual Texture destruction remains graphics-thread-only. + */ void queueReleasedTexture( Texture* texture ); void diagnoseLiveTexturesAtShutdown(); diff --git a/include/eepp/graphics/textureloader.hpp b/include/eepp/graphics/textureloader.hpp index 00d59e2ef..1307240df 100644 --- a/include/eepp/graphics/textureloader.hpp +++ b/include/eepp/graphics/textureloader.hpp @@ -137,12 +137,14 @@ class EE_API TextureLoader { bool mLoaded{ false }; bool mTexLoaded{ false }; bool mDirectUpload{ false }; + bool mPixelsUseSystemFree{ false }; Image::Format mImgType{ 0 }; int mIsCompressed{ 0 }; Clock mTE; void loadFile(); + void freePixels(); void loadFromFile(); void loadFromMemory(); void loadFromPack(); diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 121674a63..26d73a761 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -1,6 +1,7 @@ #ifndef EE_UISCENENODE_HPP #define EE_UISCENENODE_HPP +#include #include #include #include @@ -153,8 +154,8 @@ class EE_API UISceneNode : public SceneNode { * * Copies only shared platform/configuration services: dispatcher, DPI/window pointer, * thread pool, color/contrast preferences, and default font/theme pointers. Stylesheets, - * URI, referer, cookies, navigation callbacks, actions, roots, and dirty queues remain owned - * by this scene. + * URI, referer, cookies, navigation callbacks, actions, roots, resource scope, and dirty queues + * remain owned by this scene. */ void initializeEmbeddedFromHost( UISceneNode* hostScene ); @@ -770,6 +771,12 @@ class EE_API UISceneNode : public SceneNode { */ void setThreadPool( const std::shared_ptr& threadPool ); + /** @return The Graphics resource lookup and ownership boundary of this scene. */ + const Graphics::ResourceScopePtr& getResourceScope() const; + + /** Replaces this scene's resource boundary, allowing intentional sharing between scenes. */ + UISceneNode* setResourceScope( Graphics::ResourceScopePtr resourceScope ); + /** * @brief Sets the theme for the entire UI scene. * @@ -902,6 +909,7 @@ class EE_API UISceneNode : public SceneNode { UnorderedMap mFontFaceAliases; UnorderedMap mFontFaceFamilies; std::shared_ptr mAsyncResourceLoadState; + Graphics::ResourceScopePtr mResourceScope; KeyBindings mKeyBindings; std::map mKeyBindingCommands; UnorderedSet mDirtyStyle; diff --git a/include/eepp/window/engine.hpp b/include/eepp/window/engine.hpp index 233c64ec8..d03d97852 100644 --- a/include/eepp/window/engine.hpp +++ b/include/eepp/window/engine.hpp @@ -6,6 +6,13 @@ #include #include +#include + +namespace EE { namespace Graphics { +class ResourceCatalog; +class ResourceScope; +}} // namespace EE::Graphics + namespace EE { namespace System { class IniFile; class Pack; @@ -140,6 +147,12 @@ class EE_API Engine { /** @return The display manager. Holds the physical displays information. */ DisplayManager* getDisplayManager(); + /** @return The catalog used for resources intentionally exported application-wide. */ + std::shared_ptr getGlobalResourceCatalog() const; + + /** @return The default Graphics scope. It explicitly imports the global resource catalog. */ + std::shared_ptr getDefaultResourceScope() const; + /** Open a URL in a separate, system-provided application. * @return true if success */ @@ -156,6 +169,8 @@ class EE_API Engine { PlatformHelper* mPlatformHelper; Pack* mZip; DisplayManager* mDisplayManager; + std::shared_ptr mGlobalResourceCatalog; + std::shared_ptr mDefaultResourceScope; Engine(); diff --git a/src/eepp/graphics/drawablesearcher.cpp b/src/eepp/graphics/drawablesearcher.cpp index 3e84c3ef9..c707bd42f 100644 --- a/src/eepp/graphics/drawablesearcher.cpp +++ b/src/eepp/graphics/drawablesearcher.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -33,7 +34,7 @@ static Drawable* getSprite( const std::string& sprite ) { return NULL; } -static Drawable* searchByNameInternal( const std::string& name ) { +static Drawable* searchByNameInternal( const std::string& name, ResourceScope& resourceScope ) { String::HashType id = String::hash( name ); Drawable* drawable = TextureAtlasManager::instance()->getTextureRegionById( id ); @@ -42,15 +43,15 @@ static Drawable* searchByNameInternal( const std::string& name ) { } if ( NULL == drawable ) { - drawable = TextureFactory::instance()->getByHash( id ).get(); + drawable = resourceScope.findTexture( name ).get(); } return drawable; } -static Drawable* parseDataURI( const std::string& name ) { +static Drawable* parseDataURI( const std::string& name, ResourceScope& scope ) { auto hash = MD5::fromString( name ).toHexString(); - TexturePtr texture = TextureFactory::instance()->getByName( hash ); + TexturePtr texture = scope.findTexture( hash ); Drawable* drawable = texture.get(); std::string::size_type formatAndEncSep; if ( nullptr == drawable && @@ -101,6 +102,7 @@ static Drawable* parseDataURI( const std::string& name ) { if ( tex ) { tex->setName( hash ); + scope.publishLocal( hash, tex ); drawable = tex.get(); } } @@ -108,10 +110,13 @@ static Drawable* parseDataURI( const std::string& name ) { } Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSearchSprite, - Network::URI referer ) { + Network::URI referer, + ResourceScope* requestedResourceScope ) { Drawable* drawable = NULL; if ( name.size() ) { + ResourceScope& resourceScope = + requestedResourceScope ? *requestedResourceScope : defaultResourceScope(); bool searchedSprite = false; if ( firstSearchSprite ) { @@ -133,17 +138,17 @@ Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSea drawable = TextureAtlasManager::instance()->getTextureRegionByName( name.substr( 12 ) ); } else if ( String::startsWith( name, "@image/" ) ) { - drawable = TextureFactory::instance()->getByName( name.substr( 7 ) ).get(); + drawable = resourceScope.findTexture( name.substr( 7 ) ).get(); } else if ( String::startsWith( name, "@texture/" ) ) { - drawable = TextureFactory::instance()->getByName( name.substr( 9 ) ).get(); + drawable = resourceScope.findTexture( name.substr( 9 ) ).get(); } else if ( String::startsWith( name, "@sprite/" ) && !searchedSprite ) { drawable = getSprite( name.substr( 8 ) ); } else if ( String::startsWith( name, "@drawable/" ) ) { - drawable = searchByNameInternal( name.substr( 10 ) ); + drawable = searchByNameInternal( name.substr( 10 ), resourceScope ); } else if ( String::startsWith( name, "@9p/" ) ) { drawable = NinePatchManager::instance()->getByName( name.substr( 4 ) ); } else { - drawable = searchByNameInternal( name ); + drawable = searchByNameInternal( name, resourceScope ); } } else if ( String::startsWith( name, "file://" ) ) { std::string filePath( name.substr( 7 ) ); @@ -157,22 +162,26 @@ Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSea FileSystem::filePathRemoveProcessPath( filePath ); - drawable = TextureFactory::instance()->getByName( filePath ).get(); + drawable = resourceScope.findTexture( filePath ).get(); if ( NULL == drawable ) { TexturePtr tex = TextureFactory::instance()->loadFromFile( filePath ); - if ( tex ) + if ( tex ) { + resourceScope.publishLocal( filePath, tex ); drawable = tex.get(); + } } } else if ( String::startsWith( name, "http://" ) || String::startsWith( name, "https://" ) ) { - TexturePtr texture = TextureFactory::instance()->getByName( name ); + TexturePtr texture = resourceScope.findTexture( name ); if ( NULL == texture && Engine::instance()->isSharedGLContextEnabled() ) { texture = TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, name ); + if ( texture ) + resourceScope.publishLocal( name, texture ); std::map headers; if ( !referer.empty() ) @@ -197,9 +206,9 @@ Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSea drawable = texture.get(); } else if ( String::startsWith( name, "data:image/" ) ) { - drawable = parseDataURI( name ); + drawable = parseDataURI( name, resourceScope ); } else { - drawable = searchByNameInternal( name ); + drawable = searchByNameInternal( name, resourceScope ); } } @@ -212,10 +221,6 @@ Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSea Drawable* DrawableSearcher::searchById( const Uint32& id ) { Drawable* drawable = TextureAtlasManager::instance()->getTextureRegionById( id ); - if ( NULL == drawable ) { - drawable = TextureFactory::instance()->getByHash( id ).get(); - } - if ( NULL == drawable && sPrintWarnings ) Log::warning( "DrawableSearcher::searchById: \"%ld\" not found", id ); diff --git a/src/eepp/graphics/image.cpp b/src/eepp/graphics/image.cpp index 51e902b06..400d9b83e 100644 --- a/src/eepp/graphics/image.cpp +++ b/src/eepp/graphics/image.cpp @@ -929,7 +929,7 @@ void Image::webpLoad( const Uint8* imageData, size_t imageDataSize ) { } if ( errdec || nullptr == dstImage ) { - eeSAFE_FREE( dstImage ); + free( dstImage ); return; } diff --git a/src/eepp/graphics/resourcecatalog.cpp b/src/eepp/graphics/resourcecatalog.cpp new file mode 100644 index 000000000..729f76ad5 --- /dev/null +++ b/src/eepp/graphics/resourcecatalog.cpp @@ -0,0 +1,87 @@ +#include +#include + +using namespace EE::System; + +namespace EE { namespace Graphics { + +ResourceCatalogPtr ResourceCatalog::New() { + return ResourceCatalogPtr( eeNew( ResourceCatalog, () ), ResourceDeleter() ); +} + +void ResourceCatalog::publish( ResourceKey key, TexturePtr texture ) { + publish( key.value(), std::move( texture ) ); +} + +void ResourceCatalog::publish( std::string key, TexturePtr texture ) { + if ( key.empty() ) + return; + + if ( !texture ) { + erase( key ); + return; + } + + TexturePtr previous; + { + Lock lock( mMutex ); + auto it = mTextures.find( key ); + if ( it == mTextures.end() ) { + mTextures.emplace( std::move( key ), std::move( texture ) ); + return; + } + + previous = std::move( it->second ); + it->second = std::move( texture ); + } + + // A replaced handle may be the final owner. Release it without holding the catalog mutex. + previous.reset(); +} + +TexturePtr ResourceCatalog::findTexture( const ResourceKey& key ) const { + return findTexture( key.value() ); +} + +TexturePtr ResourceCatalog::findTexture( const std::string& key ) const { + Lock lock( mMutex ); + auto it = mTextures.find( key ); + return it != mTextures.end() ? it->second : TexturePtr{}; +} + +bool ResourceCatalog::erase( const ResourceKey& key ) { + return erase( key.value() ); +} + +bool ResourceCatalog::erase( const std::string& key ) { + TexturePtr texture; + { + Lock lock( mMutex ); + auto it = mTextures.find( key ); + if ( it == mTextures.end() ) + return false; + + texture = std::move( it->second ); + mTextures.erase( it ); + } + + texture.reset(); + return true; +} + +void ResourceCatalog::clear() { + UnorderedMap textures; + { + Lock lock( mMutex ); + textures = std::move( mTextures ); + } + + textures.clear(); +} + +std::size_t ResourceCatalog::size() const { + Lock lock( mMutex ); + return mTextures.size(); +} + +}} // namespace EE::Graphics diff --git a/src/eepp/graphics/resourcescope.cpp b/src/eepp/graphics/resourcescope.cpp new file mode 100644 index 000000000..4100b4d5a --- /dev/null +++ b/src/eepp/graphics/resourcescope.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include + +using namespace EE::System; +using namespace EE::Window; + +namespace EE { namespace Graphics { + +ResourceScopePtr ResourceScope::New() { + return ResourceScopePtr( eeNew( ResourceScope, () ), ResourceDeleter() ); +} + +ResourceScope::ResourceScope() : mLocalCatalog( ResourceCatalog::New() ) {} + +TexturePtr ResourceScope::findTexture( const ResourceKey& key ) const { + return findTexture( key.value() ); +} + +TexturePtr ResourceScope::findTexture( const std::string& key ) const { + if ( TexturePtr texture = mLocalCatalog->findTexture( key ) ) + return texture; + + Lock lock( mMutex ); + for ( const ResourceCatalogPtr& catalog : mImports ) { + if ( TexturePtr texture = catalog->findTexture( key ) ) + return texture; + } + return {}; +} + +void ResourceScope::publishLocal( ResourceKey key, TexturePtr texture ) { + publishLocal( key.value(), std::move( texture ) ); +} + +void ResourceScope::publishLocal( std::string key, TexturePtr texture ) { + mLocalCatalog->publish( std::move( key ), std::move( texture ) ); +} + +bool ResourceScope::eraseLocal( const ResourceKey& key ) { + return mLocalCatalog->erase( key ); +} + +bool ResourceScope::eraseLocal( const std::string& key ) { + return mLocalCatalog->erase( key ); +} + +void ResourceScope::clearLocal() { + mLocalCatalog->clear(); +} + +void ResourceScope::importCatalog( ResourceCatalogPtr catalog ) { + if ( !catalog || catalog == mLocalCatalog ) + return; + + Lock lock( mMutex ); + if ( std::find( mImports.begin(), mImports.end(), catalog ) == mImports.end() ) + mImports.emplace_back( std::move( catalog ) ); +} + +bool ResourceScope::removeCatalog( const ResourceCatalogPtr& catalog ) { + ResourceCatalogPtr removed; + { + Lock lock( mMutex ); + auto it = std::find( mImports.begin(), mImports.end(), catalog ); + if ( it == mImports.end() ) + return false; + + removed = std::move( *it ); + mImports.erase( it ); + } + + removed.reset(); + return true; +} + +void ResourceScope::clearImports() { + std::vector imports; + { + Lock lock( mMutex ); + imports = std::move( mImports ); + } + + imports.clear(); +} + +ResourceCatalogPtr ResourceScope::getLocalCatalog() const { + return mLocalCatalog; +} + +ResourceCatalog& globalResourceCatalog() { + return *Engine::instance()->getGlobalResourceCatalog(); +} + +ResourceScope& defaultResourceScope() { + return *Engine::instance()->getDefaultResourceScope(); +} + +}} // namespace EE::Graphics diff --git a/src/eepp/graphics/textureatlasloader.cpp b/src/eepp/graphics/textureatlasloader.cpp index 03042a357..4c6db9f44 100644 --- a/src/eepp/graphics/textureatlasloader.cpp +++ b/src/eepp/graphics/textureatlasloader.cpp @@ -10,8 +10,11 @@ #include #include #include +#include #include +using namespace EE::Window; + namespace EE { namespace Graphics { using namespace Private; @@ -123,7 +126,18 @@ void TextureAtlasLoader::setTextureFilter( const Texture::Filter& textureFilter mTextureAtlas->getTexture( i )->setFilter( textureFilter ); } +void TextureAtlasLoader::setResourceScope( ResourceScopePtr resourceScope ) { + mResourceScope = std::move( resourceScope ); +} + +const ResourceScopePtr& TextureAtlasLoader::getResourceScope() const { + return mResourceScope; +} + void TextureAtlasLoader::loadFromStream( IOStream& IOS ) { + if ( !mResourceScope ) + mResourceScope = Engine::instance()->getDefaultResourceScope(); + mRL.setThreaded( mThreaded ); if ( IOS.isOpen() ) { @@ -143,22 +157,28 @@ void TextureAtlasLoader::loadFromStream( IOStream& IOS ) { std::string name( &tTextureHdr.Name[0] ); std::string path( FileSystem::fileRemoveFileName( mTextureAtlasPath ) + name ); + FileSystem::filePathRemoveProcessPath( path ); //! Checks if the texture is already loaded - TexturePtr tTex = TextureFactory::instance()->getByName( path ); + TexturePtr tTex = mResourceScope->findTexture( path ); tTexAtlas.LoadedTexture = tTex; if ( !mSkipResourceLoad && NULL == tTex ) { const std::size_t textureIndex = mTempAtlass.size(); if ( NULL != mPack ) { mRL.add( [this, textureIndex, path = std::move( path )] { - mTempAtlass[textureIndex].LoadedTexture = + TexturePtr texture = TextureFactory::instance()->loadFromPack( mPack, path ); + if ( texture ) + mResourceScope->publishLocal( path, texture ); + mTempAtlass[textureIndex].LoadedTexture = std::move( texture ); } ); } else { mRL.add( [this, textureIndex, path = std::move( path )] { - mTempAtlass[textureIndex].LoadedTexture = - TextureFactory::instance()->loadFromFile( path ); + TexturePtr texture = TextureFactory::instance()->loadFromFile( path ); + if ( texture ) + mResourceScope->publishLocal( path, texture ); + mTempAtlass[textureIndex].LoadedTexture = std::move( texture ); } ); } } diff --git a/src/eepp/graphics/texturefactory.cpp b/src/eepp/graphics/texturefactory.cpp index db12b0c66..fa166b428 100644 --- a/src/eepp/graphics/texturefactory.cpp +++ b/src/eepp/graphics/texturefactory.cpp @@ -32,7 +32,6 @@ const Texture::CoordinateType& TextureFactory::getLastCoordinateType() const { } TextureFactory::~TextureFactory() { - unloadTextures(); collectReleasedTextures(); diagnoseLiveTexturesAtShutdown(); } @@ -142,7 +141,6 @@ TexturePtr TextureFactory::pushTexture( const std::string& Filepath, const Uint3 Tex->create( textureHandle, Width, Height, ImgWidth, ImgHeight, Mipmap, Channels, FPath, ClampMode, CompressTexture, MemSize ); TextureWeakPtr weakTexture( texture ); - mTextures.emplace( resourceId.value(), texture ); mLiveTextures.emplace( resourceId.value(), LiveTextureRecord{ resourceId, std::move( weakTexture ) } ); mLiveTextureGeneration.fetch_add( 1, std::memory_order_release ); @@ -203,38 +201,6 @@ void TextureFactory::bind( ResourceId textureId, Texture::CoordinateType coordin bind( getTexture( textureId ).get(), coordinateType, textureUnit, forceRebind ); } -void TextureFactory::unloadTextures() { - TextureMap textures; - { - Lock l( *this ); - textures = std::move( mTextures ); - std::fill( mCurrentTexture.begin(), mCurrentTexture.end(), 0 ); - } - - // DrawableResource destruction emits callbacks, so release factory ownership without holding - // the registry/factory mutex. - textures.clear(); - - Log::debug( "Textures Unloaded." ); -} - -bool TextureFactory::releaseRetainedTexture( ResourceId textureId ) { - TexturePtr texture; - { - Lock l( *this ); - auto it = mTextures.find( textureId.value() ); - if ( it == mTextures.end() ) - return false; - - texture = std::move( it->second ); - mTextures.erase( it ); - resetTextureBinding( texture.get() ); - } - - texture.reset(); - return true; -} - void TextureFactory::resetTextureBinding( const Texture* texture ) { if ( !texture ) return; @@ -302,9 +268,6 @@ Uint64 TextureFactory::getLiveTextureGeneration() const { } void TextureFactory::queueReleasedTexture( Texture* texture ) { - eeASSERTM( Window::Engine::existsSingleton() && Window::Engine::isMainThread(), - Texture_final_release_must_run_on_the_graphics_thread ); - Lock l( *this ); mReleasedTextures.push_back( texture ); mLiveTextureGeneration.fetch_add( 1, std::memory_order_release ); @@ -382,53 +345,38 @@ unsigned int TextureFactory::getValidTextureSize( const unsigned int& Size ) { bool TextureFactory::existsId( ResourceId textureId ) { Lock l( *this ); - - return mTextures.find( textureId.value() ) != mTextures.end(); + auto it = mLiveTextures.find( textureId.value() ); + return it != mLiveTextures.end() && !it->second.texture.expired(); } TexturePtr TextureFactory::getTexture( ResourceId textureId ) { Lock l( *this ); - auto it = mTextures.find( textureId.value() ); - return it != mTextures.end() ? it->second : TexturePtr{}; -} - -TexturePtr TextureFactory::getByName( const std::string& Name ) { - return getByHash( String::hash( Name ) ); + auto it = mLiveTextures.find( textureId.value() ); + return it != mLiveTextures.end() ? it->second.texture.lock() : TexturePtr{}; } Uint32 TextureFactory::getTextureCount() { + purgeExpiredTextures(); Lock l( *this ); - - return (Uint32)mTextures.size(); + return static_cast( mLiveTextures.size() ); } unsigned int TextureFactory::getTextureMemorySize() { - Lock l( *this ); - - std::size_t memorySize = 0; - for ( const auto& texture : mLiveTextures ) { - if ( TexturePtr liveTexture = texture.second.texture.lock() ) - memorySize += liveTexture->getMemSize(); - } - return static_cast( memorySize ); -} - -TexturePtr TextureFactory::getByHash( const String::HashType& hash ) { - Lock l( *this ); - - Uint64 latestId = 0; - TexturePtr latestTexture; - for ( const auto& texture : mTextures ) { - const TexturePtr& tTex = texture.second; - - if ( NULL != tTex && texture.first > latestId && tTex->getHashName() == hash ) { - latestId = texture.first; - latestTexture = tTex; + std::vector liveTextures; + { + Lock l( *this ); + liveTextures.reserve( mLiveTextures.size() ); + for ( const auto& record : mLiveTextures ) { + if ( TexturePtr texture = record.second.texture.lock() ) + liveTextures.emplace_back( std::move( texture ) ); } } - return latestTexture; + std::size_t memorySize = 0; + for ( const TexturePtr& texture : liveTextures ) + memorySize += texture->getMemSize(); + return static_cast( memorySize ); } }} // namespace EE::Graphics diff --git a/src/eepp/graphics/textureloader.cpp b/src/eepp/graphics/textureloader.cpp index 72ab35565..5b0f6951b 100644 --- a/src/eepp/graphics/textureloader.cpp +++ b/src/eepp/graphics/textureloader.cpp @@ -83,9 +83,27 @@ TextureLoader::TextureLoader( const unsigned char* Pixels, const unsigned int& W TextureLoader::~TextureLoader() { eeSAFE_DELETE( mColorKey ); + freePixels(); +} - if ( TEX_LT_PIXELS != mLoadType ) - eeSAFE_FREE( mPixels ); +void TextureLoader::freePixels() { + if ( !mPixels ) { + mPixelsUseSystemFree = false; + return; + } + + if ( TEX_LT_PIXELS == mLoadType ) { + mPixels = nullptr; + mPixelsUseSystemFree = false; + return; + } + + if ( mPixelsUseSystemFree ) + ::free( mPixels ); + else + eeFree( mPixels ); + mPixels = nullptr; + mPixelsUseSystemFree = false; } void TextureLoader::load() { @@ -146,6 +164,7 @@ void TextureLoader::loadFromFile() { mFormatConfiguration ); image.avoidFreeImage( true ); mPixels = image.getPixels(); + mPixelsUseSystemFree = true; mImgWidth = image.getWidth(); mImgHeight = image.getHeight(); mChannels = image.getChannels(); @@ -200,6 +219,7 @@ void TextureLoader::loadFromMemory() { mFormatConfiguration ); image.avoidFreeImage( true ); mPixels = image.getPixels(); + mPixelsUseSystemFree = true; mImgWidth = image.getWidth(); mImgHeight = image.getHeight(); mChannels = image.getChannels(); @@ -257,6 +277,7 @@ void TextureLoader::loadFromStream() { mFormatConfiguration ); image.avoidFreeImage( true ); mPixels = image.getPixels(); + mPixelsUseSystemFree = true; mImgWidth = image.getWidth(); mImgHeight = image.getHeight(); mChannels = image.getChannels(); @@ -446,16 +467,7 @@ void TextureLoader::setFormatConfiguration( } void TextureLoader::reset() { - if ( mTexture ) { - if ( TextureFactory* factory = TextureFactory::existsSingleton() ) - factory->releaseRetainedTexture( mTexture->getTextureId() ); - } - - if ( TEX_LT_PIXELS != mLoadType ) { - eeSAFE_FREE( mPixels ); - } else { - mPixels = nullptr; - } + freePixels(); mTexture.reset(); mImgWidth = 0; mImgHeight = 0; diff --git a/src/eepp/ui/css/drawableimageparser.cpp b/src/eepp/ui/css/drawableimageparser.cpp index f17c1e440..67d40446e 100644 --- a/src/eepp/ui/css/drawableimageparser.cpp +++ b/src/eepp/ui/css/drawableimageparser.cpp @@ -166,7 +166,8 @@ Drawable* DrawableImageParser::createDrawable( const std::string& value, const S if ( exists( functionType.getName() ) ) return mFuncs[functionType.getName()]( functionType, size, ownIt, node ); } else if ( NULL != ( res = DrawableSearcher::searchByName( - value, false, node->getUISceneNode()->getReferer() ) ) ) { + value, false, node->getUISceneNode()->getReferer(), + node->getUISceneNode()->getResourceScope().get() ) ) ) { if ( res->getDrawableType() == Drawable::SPRITE ) ownIt = true; return res; @@ -926,7 +927,8 @@ void DrawableImageParser::registerBaseParsers() { !String::startsWith( param, "data:image/" ) ) { return DrawableSearcher::searchByName( node->getUISceneNode()->solveRelativePath( param ).toString(), false, - node->getUISceneNode()->getReferer() ); + node->getUISceneNode()->getReferer(), + node->getUISceneNode()->getResourceScope().get() ); } else if ( functionType.getParameters().size() > 1 && String::startsWith( param, "data:image/" ) ) { auto cparam = functionType.getParameters().at( 0 ); @@ -934,10 +936,12 @@ void DrawableImageParser::registerBaseParsers() { cparam += ','; cparam += functionType.getParameters().at( i ); } - return DrawableSearcher::searchByName( cparam, false, - node->getUISceneNode()->getReferer() ); + return DrawableSearcher::searchByName( + cparam, false, node->getUISceneNode()->getReferer(), + node->getUISceneNode()->getResourceScope().get() ); } - return DrawableSearcher::searchByName( param, false, node->getUISceneNode()->getReferer() ); + return DrawableSearcher::searchByName( param, false, node->getUISceneNode()->getReferer(), + node->getUISceneNode()->getResourceScope().get() ); }; mFuncs["icon"] = []( const FunctionString& functionType, const Sizef& size, bool&, diff --git a/src/eepp/ui/uiimage.cpp b/src/eepp/ui/uiimage.cpp index 72017f08c..51924a47e 100644 --- a/src/eepp/ui/uiimage.cpp +++ b/src/eepp/ui/uiimage.cpp @@ -38,15 +38,19 @@ std::string getTextureCacheName( const Network::URI& uri ) { return filePath; } -TexturePtr loadFileTextureCached( const std::string& filePath, const std::string& cacheName ) { +TexturePtr loadFileTextureCached( const ResourceScopePtr& scope, const std::string& filePath, + const std::string& cacheName ) { static std::mutex loadMutex; std::lock_guard lock( loadMutex ); - if ( TexturePtr texture = TextureFactory::instance()->getByName( cacheName ) ) + if ( TexturePtr texture = scope->findTexture( cacheName ) ) return texture; - return TextureFactory::instance()->loadFromFile( + TexturePtr texture = TextureFactory::instance()->loadFromFile( filePath, false, Texture::ClampMode::ClampToEdge, false, false ); + if ( texture ) + scope->publishLocal( cacheName, texture ); + return texture; } } // namespace @@ -353,7 +357,8 @@ bool UIImage::loadFileDrawable( const Network::URI& uri ) { Uint64 loadId = ++mRemoteImageLoadId; std::string filePath = uri.getFSPath(); std::string cacheName = getTextureCacheName( uri ); - if ( TexturePtr texture = TextureFactory::instance()->getByName( cacheName ) ) { + ResourceScopePtr resourceScope = scene->getResourceScope(); + if ( TexturePtr texture = resourceScope->findTexture( cacheName ) ) { setDrawable( std::move( texture ) ); return true; } @@ -363,14 +368,14 @@ bool UIImage::loadFileDrawable( const Network::URI& uri ) { resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0; auto alive = mAsyncImageAlive; - scene->getThreadPool()->run( [resourceState, resourceGeneration, alive, loadId, + scene->getThreadPool()->run( [resourceState, resourceGeneration, resourceScope, alive, loadId, filePath = std::move( filePath ), cacheName = std::move( cacheName ), this] { if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) || !alive || !alive->load( std::memory_order_acquire ) ) return; - TexturePtr texture = loadFileTextureCached( filePath, cacheName ); + TexturePtr texture = loadFileTextureCached( resourceScope, filePath, cacheName ); if ( texture == nullptr ) return; @@ -393,7 +398,8 @@ void UIImage::loadRemoteDrawable( const Network::URI& uri ) { return; std::string url = uri.toString(); - if ( TexturePtr texture = TextureFactory::instance()->getByName( url ) ) { + ResourceScopePtr resourceScope = scene->getResourceScope(); + if ( TexturePtr texture = resourceScope->findTexture( url ) ) { if ( mDrawable != texture.get() ) { ++mRemoteImageLoadId; setDrawable( std::move( texture ) ); @@ -408,8 +414,10 @@ void UIImage::loadRemoteDrawable( const Network::URI& uri ) { auto alive = mAsyncImageAlive; TexturePtr texture = TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, url ); - if ( texture ) + if ( texture ) { + resourceScope->publishLocal( url, texture ); setDrawable( texture ); + } Http::Request::FieldTable headers; if ( !scene->getReferer().empty() ) @@ -562,7 +570,8 @@ bool UIImage::applyProperty( const StyleSheetProperty& attribute ) { } else { Drawable* res = NULL; if ( NULL != ( res = DrawableSearcher::searchByName( - path, false, scene ? scene->getReferer() : URI() ) ) ) + path, false, scene ? scene->getReferer() : URI(), + scene ? scene->getResourceScope().get() : nullptr ) ) ) setDrawable( res, res->getDrawableType() == Drawable::SPRITE ); } break; diff --git a/src/eepp/ui/uinodedrawable.cpp b/src/eepp/ui/uinodedrawable.cpp index c9399343b..467ccd1e6 100644 --- a/src/eepp/ui/uinodedrawable.cpp +++ b/src/eepp/ui/uinodedrawable.cpp @@ -712,7 +712,8 @@ bool UINodeDrawable::LayerDrawable::loadRemoteDrawable( const std::string& value return true; std::string url = uri.toString(); - if ( TexturePtr texture = TextureFactory::instance()->getByName( url ) ) { + ResourceScopePtr resourceScope = scene->getResourceScope(); + if ( TexturePtr texture = resourceScope->findTexture( url ) ) { if ( mDrawable != texture.get() ) { ++mRemoteDrawableLoadId; setDrawable( std::move( texture ) ); @@ -727,8 +728,10 @@ bool UINodeDrawable::LayerDrawable::loadRemoteDrawable( const std::string& value auto alive = mAsyncDrawableAlive; TexturePtr texture = TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, url ); - if ( texture ) + if ( texture ) { + resourceScope->publishLocal( url, texture ); setDrawable( texture ); + } Http::Request::FieldTable headers; if ( !scene->getReferer().empty() ) diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index e59c57f81..3831d3f04 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -33,6 +33,7 @@ #define PUGIXML_HEADER_ONLY #include +using namespace EE::Graphics; using namespace EE::Network; namespace EE { namespace UI { @@ -140,6 +141,7 @@ UISceneNode::UISceneNode( EE::Window::Window* window ) : mUIThemeManager( UIThemeManager::New() ), mUIIconThemeManager( UIIconThemeManager::New()->setFallbackThemeManager( mUIThemeManager ) ), mAsyncResourceLoadState( std::make_shared() ), + mResourceScope( ResourceScope::New() ), mKeyBindings( mWindow->getInput() ) { // Reset size since the SceneNode already set it but needs to set the size from zero to emit // the required events to its children. @@ -750,6 +752,15 @@ void UISceneNode::setThreadPool( const std::shared_ptr& threadPool ) mThreadPool = threadPool; } +const ResourceScopePtr& UISceneNode::getResourceScope() const { + return mResourceScope; +} + +UISceneNode* UISceneNode::setResourceScope( ResourceScopePtr resourceScope ) { + mResourceScope = resourceScope ? std::move( resourceScope ) : ResourceScope::New(); + return this; +} + static std::string getErrorContext( size_t offset, std::string_view content ) { static constexpr std::size_t CONTEXT_LENGTH = 50; std::size_t minVal = offset >= CONTEXT_LENGTH ? offset - CONTEXT_LENGTH : 0; diff --git a/src/eepp/ui/uisprite.cpp b/src/eepp/ui/uisprite.cpp index 1c05aa4b0..91f34e117 100644 --- a/src/eepp/ui/uisprite.cpp +++ b/src/eepp/ui/uisprite.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace EE { namespace UI { @@ -238,7 +239,14 @@ bool UISprite::applyProperty( const StyleSheetProperty& attribute ) { } Drawable* res = NULL; - if ( NULL != ( res = DrawableSearcher::searchByName( path, true ) ) ) { + UISceneNode* scene = getUISceneNode(); + if ( scene ) + res = DrawableSearcher::searchByName( path, true, scene->getReferer(), + scene->getResourceScope().get() ); + else + res = DrawableSearcher::searchByName( path, true ); + + if ( NULL != res ) { setIsSpriteOwner( true ); if ( res->getDrawableType() == Drawable::SPRITE ) { diff --git a/src/eepp/window/engine.cpp b/src/eepp/window/engine.cpp index 983280783..d764069d8 100644 --- a/src/eepp/window/engine.cpp +++ b/src/eepp/window/engine.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,8 @@ #endif +using namespace EE::Graphics; + namespace EE { namespace Window { static UintPtr sMainThreadId{ 0 }; @@ -62,7 +65,10 @@ Engine::Engine() : mSharedGLContext( true ), mPlatformHelper( NULL ), mZip( NULL ), - mDisplayManager( NULL ) { + mDisplayManager( NULL ), + mGlobalResourceCatalog( ResourceCatalog::New() ), + mDefaultResourceScope( ResourceScope::New() ) { + mDefaultResourceScope->importCatalog( mGlobalResourceCatalog ); #if EE_PLATFORM == EE_PLATFORM_ANDROID mZip = Zip::New(); mZip->open( getPlatformHelper()->getApkPath() ); @@ -112,6 +118,11 @@ Engine::~Engine() { Graphics::Private::VertexBufferManager::destroySingleton(); + // Catalogs are the final intentional texture owners. Clear them while the factory and current + // graphics context are still available for deferred release collection. + mDefaultResourceScope.reset(); + mGlobalResourceCatalog.reset(); + if ( TextureFactory* textureFactory = TextureFactory::existsSingleton() ) textureFactory->collectReleasedTextures(); @@ -153,6 +164,14 @@ Engine::~Engine() { Log::destroySingleton(); } +std::shared_ptr Engine::getGlobalResourceCatalog() const { + return mGlobalResourceCatalog; +} + +std::shared_ptr Engine::getDefaultResourceScope() const { + return mDefaultResourceScope; +} + void Engine::destroy() { for ( auto& it : mWindows ) { eeSAFE_DELETE( it.second ); diff --git a/src/modules/maps/src/eepp/maps/tilemap.cpp b/src/modules/maps/src/eepp/maps/tilemap.cpp index 005b0c8b4..ddfbf3082 100644 --- a/src/modules/maps/src/eepp/maps/tilemap.cpp +++ b/src/modules/maps/src/eepp/maps/tilemap.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -124,11 +125,12 @@ void TileMap::createEmptyTile() { //! I create a texture representing an empty tile to render instead of rendering with primitives //! because is a lot faster, at least with NVIDIA GPUs. TextureFactory* TF = TextureFactory::instance(); + ResourceScope& resourceScope = defaultResourceScope(); std::string tileName( String::format( "maptile-%dx%d-%u", mTileSize.getWidth(), mTileSize.getHeight(), mGridLinesColor.getValue() ) ); - TexturePtr texture = TF->getByName( tileName ); + TexturePtr texture = resourceScope.findTexture( tileName ); if ( NULL == texture ) { Uint32 x, y; @@ -151,6 +153,8 @@ void TileMap::createEmptyTile() { mTileTex = TF->loadFromPixels( Img.getPixelsPtr(), Img.getWidth(), Img.getHeight(), Img.getChannels(), true, Texture::ClampMode::ClampToEdge, false, false, tileName ); + if ( mTileTex ) + resourceScope.publishLocal( tileName, mTileTex ); } else { mTileTex = std::move( texture ); } diff --git a/src/tests/unit_tests/resource_prerequisite_tests.cpp b/src/tests/unit_tests/resource_prerequisite_tests.cpp index d99b31590..ba8e2394f 100644 --- a/src/tests/unit_tests/resource_prerequisite_tests.cpp +++ b/src/tests/unit_tests/resource_prerequisite_tests.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -250,6 +252,7 @@ UTEST( ResourcePrerequisites, textureRegistryTracksStableIdentityAndMemoryWithou EXPECT_EQ( factory->getTextureMemorySize(), 4u * 3u * 4u + 4u ); retainedFirst.reset(); EXPECT_TRUE( firstWeak.expired() ); + EXPECT_TRUE( factory->getTexture( firstId ) == nullptr ); snapshot = factory->snapshotTextures(); EXPECT_TRUE( std::none_of( snapshot.begin(), snapshot.end(), @@ -267,6 +270,123 @@ UTEST( ResourcePrerequisites, textureRegistryTracksStableIdentityAndMemoryWithou Engine::destroySingleton(); } +UTEST( ResourcePrerequisites, resourceCatalogOwnsPublishedTextures ) { + EE::Window::Window* window = createLifecycleTestWindow( "Resource catalog ownership test" ); + TextureFactory* factory = TextureFactory::instance(); + ResourceCatalogPtr catalog = ResourceCatalog::New(); + TexturePtr texture = factory->createEmptyTexture( 1, 1 ); + ASSERT_TRUE( texture != nullptr ); + TextureWeakPtr weakTexture = texture; + + catalog->publish( "catalog-texture", texture ); + catalog->publish( "catalog-texture-alias", texture ); + texture.reset(); + EXPECT_FALSE( weakTexture.expired() ); + EXPECT_TRUE( catalog->findTexture( "catalog-texture" ) != nullptr ); + + EXPECT_TRUE( catalog->erase( "catalog-texture" ) ); + EXPECT_FALSE( weakTexture.expired() ); + EXPECT_TRUE( catalog->erase( "catalog-texture-alias" ) ); + EXPECT_TRUE( weakTexture.expired() ); + EXPECT_EQ( factory->getPendingReleaseCount(), static_cast( 1 ) ); + window->display( false ); + EXPECT_EQ( factory->getPendingReleaseCount(), static_cast( 0 ) ); + + catalog.reset(); + Engine::destroySingleton(); +} + +UTEST( ResourcePrerequisites, resourceScopesResolveOnlyLocalAndExplicitlyImportedCatalogs ) { + EE::Window::Window* window = createLifecycleTestWindow( "Resource scope isolation test" ); + TextureFactory* factory = TextureFactory::instance(); + ResourceScopePtr firstScope = ResourceScope::New(); + ResourceScopePtr secondScope = ResourceScope::New(); + ResourceCatalogPtr sharedCatalog = ResourceCatalog::New(); + ResourceCatalogPtr laterCatalog = ResourceCatalog::New(); + TexturePtr first = factory->createEmptyTexture( 1, 1 ); + TexturePtr second = factory->createEmptyTexture( 1, 1 ); + TexturePtr shared = factory->createEmptyTexture( 1, 1 ); + TexturePtr later = factory->createEmptyTexture( 1, 1 ); + TexturePtr observedOnly = factory->createEmptyTexture( 1, 1, 4, Color::Transparent, false, + Texture::ClampMode::ClampToEdge, false, + false, "observed-only" ); + ASSERT_TRUE( first != nullptr ); + ASSERT_TRUE( second != nullptr ); + ASSERT_TRUE( shared != nullptr ); + ASSERT_TRUE( later != nullptr ); + ASSERT_TRUE( observedOnly != nullptr ); + + firstScope->publishLocal( "same-name", first ); + secondScope->publishLocal( "same-name", second ); + sharedCatalog->publish( "shared-name", shared ); + laterCatalog->publish( "shared-name", later ); + firstScope->importCatalog( sharedCatalog ); + firstScope->importCatalog( laterCatalog ); + + EXPECT_EQ( first.get(), firstScope->findTexture( "same-name" ).get() ); + EXPECT_EQ( second.get(), secondScope->findTexture( "same-name" ).get() ); + EXPECT_EQ( shared.get(), firstScope->findTexture( "shared-name" ).get() ); + EXPECT_TRUE( secondScope->findTexture( "shared-name" ) == nullptr ); + EXPECT_TRUE( firstScope->findTexture( "observed-only" ) == nullptr ); + EXPECT_TRUE( secondScope->findTexture( "observed-only" ) == nullptr ); + EXPECT_TRUE( firstScope->removeCatalog( sharedCatalog ) ); + EXPECT_EQ( later.get(), firstScope->findTexture( "shared-name" ).get() ); + EXPECT_FALSE( firstScope->removeCatalog( sharedCatalog ) ); + + TextureWeakPtr externallyRetainedWeak = first; + TexturePtr externallyRetained = first; + firstScope.reset(); + EXPECT_FALSE( externallyRetainedWeak.expired() ); + + first.reset(); + second.reset(); + shared.reset(); + later.reset(); + observedOnly.reset(); + externallyRetained.reset(); + secondScope.reset(); + sharedCatalog.reset(); + laterCatalog.reset(); + window->display( false ); + Engine::destroySingleton(); +} + +UTEST( ResourcePrerequisites, defaultResourceScopeImportsGlobalCatalog ) { + EE::Window::Window* window = createLifecycleTestWindow( "Default resource scope test" ); + TexturePtr texture = TextureFactory::instance()->createEmptyTexture( 1, 1 ); + ASSERT_TRUE( texture != nullptr ); + globalResourceCatalog().publish( "global-texture", texture ); + + EXPECT_EQ( texture.get(), defaultResourceScope().findTexture( "global-texture" ).get() ); + ResourceScopePtr isolatedScope = ResourceScope::New(); + EXPECT_TRUE( isolatedScope->findTexture( "global-texture" ) == nullptr ); + + texture.reset(); + isolatedScope.reset(); + window->display( false ); + Engine::destroySingleton(); +} + +UTEST( ResourcePrerequisites, uiScenesOwnIsolatedScopesThatCanBeSharedExplicitly ) { + EE::Window::Window* window = createLifecycleTestWindow( "UI scene resource scope test" ); + UISceneNode* firstScene = UISceneNode::New( window ); + UISceneNode* secondScene = UISceneNode::New( window ); + TexturePtr texture = TextureFactory::instance()->createEmptyTexture( 1, 1 ); + ASSERT_TRUE( texture != nullptr ); + + firstScene->getResourceScope()->publishLocal( "scene-texture", texture ); + EXPECT_TRUE( secondScene->getResourceScope()->findTexture( "scene-texture" ) == nullptr ); + secondScene->setResourceScope( firstScene->getResourceScope() ); + EXPECT_EQ( texture.get(), + secondScene->getResourceScope()->findTexture( "scene-texture" ).get() ); + + texture.reset(); + eeDelete( secondScene ); + eeDelete( firstScene ); + window->display( false ); + Engine::destroySingleton(); +} + UTEST( ResourcePrerequisites, pendingBatchRetainsTextureUntilDisplayCollection ) { EE::Window::Window* window = createLifecycleTestWindow( "Texture deferred release test" ); TextureFactory* factory = TextureFactory::instance(); @@ -310,6 +430,38 @@ UTEST( ResourcePrerequisites, pendingBatchRetainsTextureUntilDisplayCollection ) Engine::destroySingleton(); } +UTEST( ResourcePrerequisites, workerFinalReleaseDefersDestructionUntilDisplay ) { + EE::Window::Window* window = createLifecycleTestWindow( "Worker texture release test" ); + TextureFactory* factory = TextureFactory::instance(); + TexturePtr texture = factory->createEmptyTexture( 1, 1 ); + ASSERT_TRUE( texture != nullptr ); + TextureWeakPtr weakTexture = texture; + + std::thread worker( [texture = std::move( texture )]() mutable { texture.reset(); } ); + worker.join(); + + EXPECT_TRUE( weakTexture.expired() ); + EXPECT_EQ( factory->getPendingReleaseCount(), static_cast( 1 ) ); + window->display( false ); + EXPECT_EQ( factory->getPendingReleaseCount(), static_cast( 0 ) ); + Engine::destroySingleton(); +} + +UTEST( ResourcePrerequisites, workerFileTextureLoadReleasesDecoderPixelsCorrectly ) { + EE::Window::Window* window = createLifecycleTestWindow( "Worker file texture load test" ); + TexturePtr texture; + std::thread worker( [&texture] { + texture = TextureFactory::instance()->loadFromFile( Sys::getProcessPath() + + "../assets/atlases/bnb/007.png" ); + } ); + worker.join(); + ASSERT_TRUE( texture != nullptr ); + + texture.reset(); + window->display( false ); + Engine::destroySingleton(); +} + UTEST( ResourcePrerequisites, textureRegionRetainsItsTexture ) { EE::Window::Window* window = createLifecycleTestWindow( "Texture region ownership test" ); TextureFactory* factory = TextureFactory::instance(); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index cad48def8..e4cfef37f 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -3825,6 +3825,7 @@ UTEST( UIBackground, RemoteImageReusesCachedTexture ) { TexturePtr cached = TextureFactory::instance()->createEmptyTexture( 8, 8, 4, Color::White, false, Texture::ClampMode::ClampToEdge, false, false, imageURL ); ASSERT_TRUE( cached != nullptr ); + sceneNode->getResourceScope()->publishLocal( imageURL, cached ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html( @@ -5117,7 +5118,8 @@ UTEST( UIHTML, DeferredFileImageReusesCachedTexture ) { sceneNode->setURI( URI( "file://" + processPath ) ); URI imageURI = sceneNode->solveRelativePath( URI( "../assets/icon/ee.png" ) ); ASSERT_TRUE( FileSystem::fileExists( imageURI.getFSPath() ) ); - Drawable* cached = DrawableSearcher::searchByName( imageURI.toString() ); + Drawable* cached = DrawableSearcher::searchByName( + imageURI.toString(), false, sceneNode->getReferer(), sceneNode->getResourceScope().get() ); ASSERT_TRUE( cached != nullptr ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html( @@ -5160,6 +5162,7 @@ UTEST( UIHTML, RemoteImageReusesCachedTexture ) { TexturePtr cached = TextureFactory::instance()->createEmptyTexture( 8, 8, 4, Color::White, false, Texture::ClampMode::ClampToEdge, false, false, imageURL ); ASSERT_TRUE( cached != nullptr ); + sceneNode->getResourceScope()->publishLocal( imageURL, cached ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html( diff --git a/src/tools/ecode/settingsactions.cpp b/src/tools/ecode/settingsactions.cpp index 5cb80197d..959977147 100644 --- a/src/tools/ecode/settingsactions.cpp +++ b/src/tools/ecode/settingsactions.cpp @@ -117,11 +117,14 @@ void SettingsActions::aboutEcode() { UIImage* image = UIImage::New(); image->setParent( msgBox->getContainer()->getFirstChild() ); auto tf = TextureFactory::instance(); - TexturePtr tex = tf->getByName( "ecode-logo" ); + auto resourceScope = mApp->getUISceneNode()->getResourceScope(); + TexturePtr tex = resourceScope->findTexture( "ecode-logo" ); if ( tex == nullptr ) { tex = tf->loadFromFile( mApp->resPath() + "icon/ecode.png" ); - if ( tex ) + if ( tex ) { tex->setName( "ecode-logo" ); + resourceScope->publishLocal( "ecode-logo", tex ); + } } image->setDrawable( std::move( tex ) ); image->setLayoutGravity( UI_NODE_ALIGN_CENTER );