diff --git a/.agent/plans/resource_shared_ownership_architecture.md b/.agent/plans/resource_shared_ownership_architecture.md index a9f8d69c7..e8d1ce752 100644 --- a/.agent/plans/resource_shared_ownership_architecture.md +++ b/.agent/plans/resource_shared_ownership_architecture.md @@ -1,6 +1,6 @@ # eepp shared-resource ownership architecture -Status: active implementation baseline; Stage 0 through Stage 5 complete, 2026-07-21. +Status: active implementation baseline; Stage 0 through Stage 6 complete, 2026-07-21. 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 @@ -752,6 +752,28 @@ Exit criteria: ### Stage 6: WebResourceCache and document leases +Status: complete, 2026-07-21. Each UISceneNode now owns a WebResourceCache document session with +an explicit cache partition and navigation generation. UIWebView advances that session at the +existing navigation boundary. Document, stylesheet, remote font, image, and CSS background image +requests share canonical fragment-free keys that include the partition, request method/body and +headers, resource kind, and image decode options. + +Concurrent requests coalesce into one fetch and one image decode/upload. Subscribers retain their +own document generation, so navigation or destruction removes stale delivery without cancelling a +request needed by another session. Applications can install one cache and explicit partition into +multiple WebViews to share eligible public resources; distinct partitions and content-affecting +headers remain isolated. Scene ResourceScope entries remain an explicit override but fetched Web +resources are retained only by consumers, document leases, and the cache. + +Completed entries use monotonic TTL and LRU timestamps plus a configurable byte budget. Active +document leases are not evicted; the TTL starts when the final document lease is released, allowing +Back/Forward navigation to reuse resources regardless of how long the previous document remained +open. Navigation releases only that session's previous leases, and UIWebView performs throttled +cache maintenance so expired unleased entries are collected even when no new requests complete. Failed loads +retry, redirect/final cookies are delivered only to current subscribers, and image upload is +dispatched through the scene's guarded main-thread resource queue. Common lease lists use inline +small vectors, and completed entries release starter request bodies, headers, and dispatchers. + Implement cache partitions, canonical keys/origins, per-document sessions and leases, in-flight coalescing, per-subscriber generation guards, retries, TTL/LRU, and byte budgets. Integrate WebView navigation at its existing document replacement boundary. @@ -837,8 +859,6 @@ Remove raw-owning `ResourceManager` only when no subclass or consumer depends ## 12. Next implementation deliverable -Stage 2 is complete with the repository-wide TexturePtr API and holder migration while temporary -factory retention preserves existing global lookup behavior. The next coding deliverable is Stage -3: introduce catalogs and Graphics scopes, migrate semantic name lookup out of TextureFactory, move -intentional persistence into catalogs/caches, and then remove the factory's temporary strong -retention. +Stage 7 migrates the remaining ResourceManager families one at a time: fonts and font caches, +themes/icons, shaders/programs, nine-patches, atlases, and any remaining raw-owning manager. The +raw-owning ResourceManager template is removed only after its final consumer is migrated. diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index 0765126c1..38488a760 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -180,6 +180,7 @@ #include #include #include +#include #include #endif diff --git a/include/eepp/ui/uinodedrawable.hpp b/include/eepp/ui/uinodedrawable.hpp index 1ad5aa864..c589eedb7 100644 --- a/include/eepp/ui/uinodedrawable.hpp +++ b/include/eepp/ui/uinodedrawable.hpp @@ -145,9 +145,6 @@ class EE_API UINodeDrawable : public Drawable { Origin mOrigin{ Origin::PaddingBox }; Clip mClip{ Clip::BorderBox }; Attachment mAttachment{ Attachment::Scroll }; - std::shared_ptr> mAsyncDrawableAlive; - Uint64 mRemoteDrawableLoadId{ 0 }; - virtual void onPositionChange(); virtual void onColorFilterChange(); diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 8aae83e45..8910452bf 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -850,6 +851,21 @@ class EE_API UISceneNode : public SceneNode { Network::CookieManager& getCookieManager() { return mCookieManager; } + const WebResourceCachePtr& getWebResourceCache() const { return mWebResourceCache; } + + UISceneNode* setWebResourceCache( WebResourceCachePtr cache, CachePartitionId partition = 0 ); + + DocumentSessionId getDocumentSessionId() const { return mDocumentSessionId; } + + Uint64 beginDocumentNavigation( const URI& uri ); + + Uint64 getDocumentGeneration() const; + + void requestWebResource( WebResourceRequest request, WebResourceCache::Callback callback ); + + Graphics::TexturePtr requestWebTexture( WebResourceRequest request, + WebResourceCache::Callback callback = {} ); + void invalidateAsyncResourceLoads(); virtual void invalidate( Node* invalidator ); @@ -918,6 +934,8 @@ class EE_API UISceneNode : public SceneNode { std::shared_ptr mAsyncResourceLoadState; Graphics::ResourceScopePtr mResourceScope; DrawableResolver mDrawableResolver; + WebResourceCachePtr mWebResourceCache; + DocumentSessionId mDocumentSessionId{ 0 }; KeyBindings mKeyBindings; std::map mKeyBindingCommands; UnorderedSet mDirtyStyle; diff --git a/include/eepp/ui/uiwebview.hpp b/include/eepp/ui/uiwebview.hpp index b650c4aa8..4308a26b9 100644 --- a/include/eepp/ui/uiwebview.hpp +++ b/include/eepp/ui/uiwebview.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -72,6 +73,10 @@ class EE_API UIWebView : public UIScrollView { UISceneNode* getDocumentSceneNode() const; + const WebResourceCachePtr& getWebResourceCache() const; + + UIWebView* setWebResourceCache( WebResourceCachePtr cache, CachePartitionId partition = 0 ); + void setStyleSheetDefaultMarker( Uint32 marker ); void setUserAgent( const std::string& userAgent ); @@ -117,6 +122,7 @@ class EE_API UIWebView : public UIScrollView { Uint64 mNavigationGeneration{ 0 }; std::string mUserAgent; Time mDefaultTimeout{ Seconds( 30 ) }; + Time mWebResourceCachePruneElapsed; Uint32 mStyleSheetDefaultMarker{ 0 }; void loadURI( URI uri, bool isHistoryNav ); diff --git a/include/eepp/ui/webresourcecache.hpp b/include/eepp/ui/webresourcecache.hpp new file mode 100644 index 000000000..e7d0dd356 --- /dev/null +++ b/include/eepp/ui/webresourcecache.hpp @@ -0,0 +1,302 @@ +#ifndef EE_UI_WEBRESOURCECACHE_HPP +#define EE_UI_WEBRESOURCECACHE_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace EE { namespace UI { + +/** Shared ownership handle for a WebResourceCache. + * + * A cache can be installed into more than one UIWebView. Sharing the cache does not by itself make + * all resources visible between those views: sessions must also use the same CachePartitionId. */ +using WebResourceCachePtr = std::shared_ptr; + +/** Identifies one document consumer of a WebResourceCache. + * + * UISceneNode creates a session for its current document. A session tracks the current navigation + * generation and the cache entries leased by that document. IDs are process-wide and opaque. */ +using DocumentSessionId = Uint64; + +/** Identifies a cache sharing and privacy boundary. + * + * Sessions in the same partition may reuse and coalesce resources. Sessions in different + * partitions never share entries, even when requesting the same URI. A partition should therefore + * represent one intentionally shared HTTP state, normally one cookie jar/authentication context. + * + * For example, browser tabs that share cookies should use the same WebResourceCache and + * CachePartitionId. A private tab or a view logged into a different account must use another + * partition. Passing zero to createSession() creates a new private partition automatically. + * Partition IDs are process-wide and opaque. + * + * @code + * auto cache = WebResourceCache::New(); + * auto normalProfile = cache->createPartition(); + * firstTab->setWebResourceCache( cache, normalProfile ); + * secondTab->setWebResourceCache( cache, normalProfile ); // May reuse firstTab resources. + * privateTab->setWebResourceCache( cache, cache->createPartition() ); // Fully isolated entries. + * @endcode */ +using CachePartitionId = Uint64; + +/** Canonical network origin used to describe a document navigation. + * + * Origins compare scheme, host, and port. Consequently HTTP and HTTPS origins are distinct, as are + * equal hosts using different ports. Paths, queries, and fragments are not part of an origin. */ +struct EE_API WebOriginKey { + /** Lowercase URI scheme, such as "http" or "https". */ + std::string scheme; + /** Lowercase host name. */ + std::string host; + /** URI port, or the URI implementation's default/empty port value. */ + Uint16 port{ 0 }; + + /** @return True when no origin components have been assigned. */ + bool empty() const { return scheme.empty() && host.empty() && port == 0; } + /** @return True when both values describe the same scheme, host, and port. */ + bool operator==( const WebOriginKey& other ) const; + /** @return True when at least one origin component differs. */ + bool operator!=( const WebOriginKey& other ) const { return !( *this == other ); } + + /** Creates an origin key from a URI, normalizing the scheme and host to lowercase. */ + static WebOriginKey fromURI( const Network::URI& uri ); +}; + +/** Semantic type of a cached web resource. + * + * The kind is part of cache identity, so one URI requested as a document and as a stylesheet uses + * distinct entries. Images additionally carry texture decode options in WebResourceRequest. */ +enum class WebResourceKind : Uint8 { + /** Top-level HTML or another document response. */ + Document, + /** CSS stylesheet response. */ + StyleSheet, + /** Downloadable font response. */ + Font, + /** Image response decoded into a Graphics::Texture. */ + Image +}; + +/** Current lifecycle state of a cache entry. */ +enum class WebResourceLoadState : Uint8 { + /** Entry exists but has not started loading. */ + Empty, + /** One fetch is active; additional subscribers join it. */ + Loading, + /** Resource completed successfully and is available for reuse. */ + Ready, + /** Last fetch failed and remains subject to the retry delay. */ + Failed, + /** Entry was cleared and any late fetch completion will be ignored. */ + Cancelled +}; + +/** Complete input required to identify and fetch one web resource variant. + * + * Cache identity includes the partition, canonical URI, resource kind, method, non-navigation + * headers, body, and image decode options. Cookie and Referer are deliberately excluded from entry + * identity: the partition isolates cookie contexts, while these two values naturally change during + * navigation. They are still sent with the HTTP request. + * + * Transport policy such as timeout, proxy, and completionDispatcher controls a fetch but does not + * describe the resulting resource and is therefore not part of cache identity. */ +struct EE_API WebResourceRequest { + /** Dispatches a completion function to the thread on which it is safe to finalize a resource. + */ + using CompletionDispatcher = std::function )>; + + /** Absolute URI to fetch. Fragments are removed and the URI is normalized for cache lookup. */ + Network::URI uri; + /** Semantic resource type. */ + WebResourceKind kind{ WebResourceKind::Document }; + /** HTTP method. The method is part of cache identity. */ + Network::Http::Request::Method method{ Network::Http::Request::Method::Get }; + /** HTTP request headers. See the type documentation for cache-key rules. */ + Network::Http::Request::FieldTable headers; + /** HTTP request body. The complete body is part of cache identity. */ + std::string body; + /** Maximum duration allowed for the HTTP request. */ + System::Time timeout{ System::Seconds( 5 ) }; + /** Whether HTTPS certificates must be validated. */ + bool validateCertificate{ true }; + /** Optional HTTP proxy URI. An empty URI uses a direct connection. */ + Network::URI proxy; + /** Whether HTTP redirects should be followed. */ + bool followRedirect{ true }; + /** Image-only texture wrapping policy. True selects ClampToEdge. */ + bool clampToEdge{ true }; + /** Image-only option controlling mipmap generation. */ + bool mipmaps{ false }; + /** Image-only option requesting texture compression. */ + bool compressTexture{ false }; + /** Image-only SVG rasterization scale. */ + Float svgScale{ 1.f }; + /** Image-only completion dispatcher. + * + * Image decode/upload completion must execute where a graphics context is current. This policy + * is not part of cache identity and only the request that starts a coalesced load supplies it. + */ + CompletionDispatcher completionDispatcher; +}; + +/** Result delivered to a WebResourceCache subscriber. + * + * Data resources set data, while image resources set texture. A texture request may already have + * returned the same TexturePtr as a transparent placeholder before this result is delivered. */ +struct EE_API WebResourceResult { + /** True when the request completed and produced a usable resource. */ + bool success{ false }; + /** Final cache-entry state observed by this completion. */ + WebResourceLoadState state{ WebResourceLoadState::Empty }; + /** Numeric HTTP response status. */ + int status{ 0 }; + /** Failure description. Empty for successful results. */ + std::string error; + /** Set-Cookie value from the final response, or from its redirect chain when applicable. */ + std::string setCookie; + /** Shared immutable response body for documents, stylesheets, and fonts. */ + std::shared_ptr data; + /** Shared texture for image resources. */ + Graphics::TexturePtr texture; +}; + +/** Shared HTTP-backed document resource cache. + * + * The cache separates three concepts: + * - A partition defines which HTTP/cookie context may share entries. + * - A session represents one active document using entries from that partition. + * - A navigation generation prevents an obsolete document from receiving asynchronous results. + * + * Entries are strongly retained by the cache after their final document lease is released. Their + * TTL starts at that release point, allowing Back/Forward navigation to reuse them. Expired entries + * are removed by prune(); UIWebView invokes it periodically. A byte budget additionally evicts the + * least-recently-used unleased entries. Active leases and in-flight loads are not evicted. + * + * Requests for the same key are coalesced into one HTTP operation. Each current subscriber receives + * the result, while callbacks belonging to obsolete navigation generations are discarded. Public + * operations are internally synchronized. Subscriber callbacks run without the cache mutex held. */ +class EE_API WebResourceCache : public std::enable_shared_from_this { + public: + /** Receives the completed result of a data or texture request. */ + using Callback = std::function; + + /** @return A newly allocated shared web-resource cache. */ + static WebResourceCachePtr New(); + + /** Creates an empty cache with a 30-second TTL, 1-second retry delay, and 64 MiB budget. */ + WebResourceCache(); + /** Cancels cache delivery and releases all retained entries and sessions. */ + ~WebResourceCache(); + + /** Allocates a new process-wide partition identifier. + * @return An opaque, non-zero partition ID. */ + CachePartitionId createPartition(); + + /** Creates a document session. + * @param partition Partition to join. Zero creates a new private partition. + * @return An opaque, non-zero document session ID. */ + DocumentSessionId createSession( CachePartitionId partition = 0 ); + + /** Destroys a session and releases all entries leased by it. + * + * Releasing the final lease starts each entry's TTL; it does not normally erase entries + * immediately. Pending callbacks for the destroyed session will not be delivered. */ + void destroySession( DocumentSessionId session ); + + /** Begins a new document navigation in an existing session. + * + * Previous document leases are released and the generation is incremented. Pending subscribers + * from older generations become stale, but shared in-flight requests continue for other current + * subscribers. + * @param session Session performing the navigation. + * @param uri Destination document URI, used to record its origin. + * @return The new generation, or zero when the session does not exist. */ + Uint64 beginNavigation( DocumentSessionId session, const Network::URI& uri ); + + /** @return The current generation for a session, or zero when it does not exist. */ + Uint64 getSessionGeneration( DocumentSessionId session ) const; + /** @return The partition used by a session, or zero when it does not exist. */ + CachePartitionId getSessionPartition( DocumentSessionId session ) const; + + /** Requests a document, stylesheet, font, or other non-texture response body. + * + * A ready cache hit may invoke callback synchronously. A miss starts or joins an asynchronous + * fetch. The callback is omitted when the session/generation is stale. + * @param session Requesting document session. + * @param generation Generation returned by beginNavigation(). + * @param request Resource and transport description. + * @param callback Optional completion callback. */ + void requestData( DocumentSessionId session, Uint64 generation, WebResourceRequest request, + Callback callback ); + + /** Requests an image as a shared texture. + * + * On a miss, this creates and returns a transparent placeholder immediately. The same + * TexturePtr is populated after download and decode. A ready hit returns the existing texture + * without a new HTTP request. This function must be called where texture creation is valid, and + * the request must provide an appropriate completionDispatcher for asynchronous GPU upload. + * @return The cached/new texture, or null for an invalid or stale session. */ + Graphics::TexturePtr requestTexture( DocumentSessionId session, Uint64 generation, + WebResourceRequest request, Callback callback = {} ); + + /** Sets the retention duration used when an entry's final document lease is released. */ + void setTTL( System::Time ttl ); + /** @return The current post-lease retention duration. */ + System::Time getTTL() const; + /** Sets how long a failed entry suppresses another fetch attempt. */ + void setRetryDelay( System::Time delay ); + /** @return The current failed-request retry delay. */ + System::Time getRetryDelay() const; + /** Sets the maximum retained response/texture byte estimate. + * + * Applying a smaller budget immediately evicts least-recently-used unleased entries where + * possible. Active entries may temporarily make retained bytes exceed the budget. */ + void setByteBudget( std::size_t bytes ); + /** @return The configured retained-byte budget. */ + std::size_t getByteBudget() const; + /** @return Estimated bytes retained by ready data bodies and decoded image textures. */ + std::size_t getRetainedBytes() const; + /** @return Number of cache entries in all states and partitions. */ + std::size_t getEntryCount() const; + /** @return Number of HTTP requests currently owned by the cache. */ + std::size_t getInFlightCount() const; + + /** Removes expired entries and enforces the byte budget. + * + * Active document leases and loading entries are preserved. UIWebView calls this periodically; + * other cache owners should provide their own maintenance point. */ + void prune(); + + /** Removes every entry immediately and suppresses delivery from outstanding fetches. + * + * Sessions remain valid but no longer lease entries. This does not cancel the underlying HTTP + * operation; a late completion is ignored because its entry no longer exists. */ + void clear(); + + /** Completion supplied to a custom Fetcher. */ + using FetchCompletion = std::function; + /** Custom fetch implementation, primarily intended for deterministic tests. + * + * The fetcher must eventually invoke FetchCompletion exactly once. Production uses + * Http::requestAsync when no custom fetcher is installed. */ + using Fetcher = std::function; + + /** Installs or clears a custom HTTP fetch implementation. */ + void setFetcher( Fetcher fetcher ); + + private: + /** Private implementation containing synchronized sessions and cache entries. */ + struct Impl; + /** Exclusive implementation state. */ + std::unique_ptr mImpl; +}; + +}} // namespace EE::UI + +#endif diff --git a/projects/linux/ee.files b/projects/linux/ee.files index 46d4890f7..3f333207e 100644 --- a/projects/linux/ee.files +++ b/projects/linux/ee.files @@ -58,6 +58,7 @@ ../../include/eepp/graphics/drawable.hpp ../../include/eepp/graphics/drawableresource.hpp ../../include/eepp/ui/drawableresolver.hpp +../../include/eepp/ui/webresourcecache.hpp ../../include/eepp/graphics/fontbmfont.hpp ../../include/eepp/graphics/font.hpp ../../include/eepp/graphics/fontfamily.hpp @@ -556,6 +557,7 @@ ../../src/eepp/graphics/drawablegroup.cpp ../../src/eepp/graphics/drawableresource.cpp ../../src/eepp/ui/drawableresolver.cpp +../../src/eepp/ui/webresourcecache.cpp ../../src/eepp/graphics/fontbmfont.cpp ../../src/eepp/graphics/font.cpp ../../src/eepp/graphics/fontfamily.cpp diff --git a/projects/macos/ee.files b/projects/macos/ee.files index a320ffa16..a2525b6d2 100644 --- a/projects/macos/ee.files +++ b/projects/macos/ee.files @@ -58,6 +58,7 @@ ../../include/eepp/graphics/drawable.hpp ../../include/eepp/graphics/drawableresource.hpp ../../include/eepp/ui/drawableresolver.hpp +../../include/eepp/ui/webresourcecache.hpp ../../include/eepp/graphics/fontbmfont.hpp ../../include/eepp/graphics/font.hpp ../../include/eepp/graphics/fontfamily.hpp @@ -547,6 +548,7 @@ ../../src/eepp/graphics/drawablegroup.cpp ../../src/eepp/graphics/drawableresource.cpp ../../src/eepp/ui/drawableresolver.cpp +../../src/eepp/ui/webresourcecache.cpp ../../src/eepp/graphics/fontbmfont.cpp ../../src/eepp/graphics/font.cpp ../../src/eepp/graphics/fontfamily.cpp diff --git a/projects/windows/ee.files b/projects/windows/ee.files index 1991069ec..f3cdd029c 100644 --- a/projects/windows/ee.files +++ b/projects/windows/ee.files @@ -57,6 +57,7 @@ ../../include/eepp/graphics/drawable.hpp ../../include/eepp/graphics/drawableresource.hpp ../../include/eepp/ui/drawableresolver.hpp +../../include/eepp/ui/webresourcecache.hpp ../../include/eepp/graphics/fontbmfont.hpp ../../include/eepp/graphics/font.hpp ../../include/eepp/graphics/fontmanager.hpp @@ -539,6 +540,7 @@ ../../src/eepp/graphics/drawablegroup.cpp ../../src/eepp/graphics/drawableresource.cpp ../../src/eepp/ui/drawableresolver.cpp +../../src/eepp/ui/webresourcecache.cpp ../../src/eepp/graphics/fontbmfont.cpp ../../src/eepp/graphics/font.cpp ../../src/eepp/graphics/fontmanager.cpp diff --git a/src/eepp/ui/drawableresolver.cpp b/src/eepp/ui/drawableresolver.cpp index 2b5e8d35a..d287ad94f 100644 --- a/src/eepp/ui/drawableresolver.cpp +++ b/src/eepp/ui/drawableresolver.cpp @@ -103,7 +103,21 @@ DrawablePtr DrawableResolver::resolve( const std::string& name, bool firstSearch drawable = texture ? texture->clone() : DrawablePtr{}; } else if ( String::startsWith( name, "http://" ) || String::startsWith( name, "https://" ) ) { TexturePtr texture = resourceScope.findTexture( name ); - if ( !texture && Engine::instance()->isSharedGLContextEnabled() ) { + if ( mSceneNode && Engine::instance()->isSharedGLContextEnabled() ) { + if ( !texture ) { + WebResourceRequest request; + request.uri = URI( name ); + request.kind = WebResourceKind::Image; + texture = mSceneNode->requestWebTexture( + std::move( request ), [name]( const WebResourceResult& result ) { + if ( !result.success ) + Log::debug( "DrawableResolver::resolve: could not download image: %s. " + "Error: %d\n%s", + name, result.status, result.error ); + } ); + } + } + if ( !mSceneNode && !texture && Engine::instance()->isSharedGLContextEnabled() ) { texture = TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, name ); diff --git a/src/eepp/ui/uiimage.cpp b/src/eepp/ui/uiimage.cpp index 36db9eaef..fdeb8523b 100644 --- a/src/eepp/ui/uiimage.cpp +++ b/src/eepp/ui/uiimage.cpp @@ -64,10 +64,7 @@ UIImage* UIImage::NewWithTag( const std::string& tag ) { } UIImage::UIImage( const std::string& tag ) : - UIWidget( tag ), - mScaleType( UIScaleType::None ), - mColor(), - mAlignOffset( 0, 0 ) { + UIWidget( tag ), mScaleType( UIScaleType::None ), mColor(), mAlignOffset( 0, 0 ) { mFlags |= UI_AUTO_SIZE; applyDefaultTheme(); @@ -379,65 +376,38 @@ void UIImage::loadRemoteDrawable( const Network::URI& uri ) { return; std::string url = uri.toString(); - ResourceScopePtr resourceScope = scene->getResourceScope(); - if ( TexturePtr texture = resourceScope->findTexture( url ) ) { - TextureDrawable* textureDrawable = + if ( TexturePtr texture = scene->getResourceScope()->findTexture( url ) ) { + TextureDrawable* current = mDrawable && mDrawable->getDrawableType() == Drawable::TEXTUREDRAWABLE ? static_cast( mDrawable.get() ) : nullptr; - if ( !textureDrawable || textureDrawable->getTexture() != texture ) { + if ( !current || current->getTexture() != texture ) { ++mRemoteImageLoadId; setDrawable( std::move( texture ) ); } return; } - - auto resourceState = scene->getAsyncResourceLoadState(); - Uint64 resourceGeneration = - resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0; - Uint64 loadId = ++mRemoteImageLoadId; - if ( !mAsyncImageAlive ) - mAsyncImageAlive = std::make_shared>( true ); - auto alive = mAsyncImageAlive; - TexturePtr texture = TextureFactory::instance()->createEmptyTexture( - 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, url ); - if ( texture ) { - resourceScope->publishLocal( url, texture ); - setDrawable( texture ); - } - - Http::Request::FieldTable headers; - if ( !scene->getReferer().empty() ) - headers["referer"] = scene->getReferer().toString(); - Http::getAsync( - [resourceState, resourceGeneration, alive, loadId, texture, url = std::move( url ), - this]( const Http&, Http::Request&, Http::Response& response ) { - if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) || - !alive || !alive->load( std::memory_order_acquire ) || texture == nullptr ) - return; - - if ( response.isOK() && !response.getBody().empty() ) { - std::string imageData( response.getBody() ); - UISceneNode::runAsyncResourceOnMainThread( - resourceState, resourceGeneration, - [alive, loadId, texture, imageData = std::move( imageData ), - this]( UISceneNode* ) mutable { - if ( !alive || !alive->load( std::memory_order_acquire ) || - loadId != mRemoteImageLoadId ) - return; - - Image image( reinterpret_cast( imageData.data() ), - imageData.size() ); - if ( image.getPixels() != nullptr ) - texture->replace( &image ); - } ); - } else { + WebResourceRequest request; + request.uri = uri; + request.kind = WebResourceKind::Image; + request.proxy = Http::getEnvProxyURI(); + TexturePtr texture = scene->requestWebTexture( + std::move( request ), [url = std::move( url )]( const WebResourceResult& result ) { + if ( !result.success ) Log::debug( "UIImage::loadRemoteDrawable: could not download image: %s. Error: " "%d\n%s", - url, response.getStatus(), response.getBody() ); - } - }, - uri, Seconds( 5 ), {}, headers, "", true, Http::getEnvProxyURI() ); + url, result.status, result.error ); + } ); + if ( texture ) { + TextureDrawable* current = + mDrawable && mDrawable->getDrawableType() == Drawable::TEXTUREDRAWABLE + ? static_cast( mDrawable.get() ) + : nullptr; + if ( !current || current->getTexture() != texture ) { + ++mRemoteImageLoadId; + setDrawable( std::move( texture ) ); + } + } } void UIImage::onSizeChange() { diff --git a/src/eepp/ui/uinodedrawable.cpp b/src/eepp/ui/uinodedrawable.cpp index b6cecd7bc..5f3b5f7ce 100644 --- a/src/eepp/ui/uinodedrawable.cpp +++ b/src/eepp/ui/uinodedrawable.cpp @@ -418,9 +418,6 @@ UINodeDrawable::LayerDrawable::LayerDrawable( UINodeDrawable* container ) : mAttachment( Attachment::Scroll ) {} UINodeDrawable::LayerDrawable::~LayerDrawable() { - if ( mAsyncDrawableAlive ) - mAsyncDrawableAlive->store( false, std::memory_order_release ); - mResourceChangeConnection.disconnect(); } @@ -677,65 +674,34 @@ bool UINodeDrawable::LayerDrawable::loadRemoteDrawable( const std::string& value return true; std::string url = uri.toString(); - ResourceScopePtr resourceScope = scene->getResourceScope(); - if ( TexturePtr texture = resourceScope->findTexture( url ) ) { - TextureDrawable* textureDrawable = + if ( TexturePtr texture = scene->getResourceScope()->findTexture( url ) ) { + TextureDrawable* current = mDrawable && mDrawable->getDrawableType() == Drawable::TEXTUREDRAWABLE ? static_cast( mDrawable.get() ) : nullptr; - if ( !textureDrawable || textureDrawable->getTexture() != texture ) { - ++mRemoteDrawableLoadId; + if ( !current || current->getTexture() != texture ) setDrawable( std::move( texture ) ); - } return true; } - - auto resourceState = scene->getAsyncResourceLoadState(); - Uint64 resourceGeneration = - resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0; - Uint64 loadId = ++mRemoteDrawableLoadId; - if ( !mAsyncDrawableAlive ) - mAsyncDrawableAlive = std::make_shared>( true ); - auto alive = mAsyncDrawableAlive; - TexturePtr texture = TextureFactory::instance()->createEmptyTexture( - 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, url ); - if ( texture ) { - resourceScope->publishLocal( url, texture ); - setDrawable( texture ); - } - - Http::Request::FieldTable headers; - if ( !scene->getReferer().empty() ) - headers["referer"] = scene->getReferer().toString(); - Http::getAsync( - [resourceState, resourceGeneration, alive, loadId, texture, url = std::move( url ), - this]( const Http&, Http::Request&, Http::Response& response ) { - if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) || - !alive || !alive->load( std::memory_order_acquire ) || texture == nullptr ) - return; - - if ( response.isOK() && !response.getBody().empty() ) { - std::string imageData( response.getBody() ); - UISceneNode::runAsyncResourceOnMainThread( - resourceState, resourceGeneration, - [alive, loadId, texture, imageData = std::move( imageData ), - this]( UISceneNode* ) mutable { - if ( !alive || !alive->load( std::memory_order_acquire ) || - loadId != mRemoteDrawableLoadId ) - return; - - Image image( reinterpret_cast( imageData.data() ), - imageData.size() ); - if ( image.getPixels() != nullptr ) - texture->replace( &image ); - } ); - } else { + WebResourceRequest request; + request.uri = uri; + request.kind = WebResourceKind::Image; + request.proxy = Http::getEnvProxyURI(); + TexturePtr texture = scene->requestWebTexture( + std::move( request ), [url = std::move( url )]( const WebResourceResult& result ) { + if ( !result.success ) Log::debug( "UINodeDrawable::LayerDrawable::loadRemoteDrawable: could not " "download image: %s. Error: %d\n%s", - url, response.getStatus(), response.getBody() ); - } - }, - uri, Seconds( 5 ), {}, headers, "", true, Http::getEnvProxyURI() ); + url, result.status, result.error ); + } ); + if ( texture ) { + TextureDrawable* current = + mDrawable && mDrawable->getDrawableType() == Drawable::TEXTUREDRAWABLE + ? static_cast( mDrawable.get() ) + : nullptr; + if ( !current || current->getTexture() != texture ) + setDrawable( std::move( texture ) ); + } return true; } diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 03b921e93..834a59631 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -143,6 +143,7 @@ UISceneNode::UISceneNode( EE::Window::Window* window ) : mAsyncResourceLoadState( std::make_shared() ), mResourceScope( ResourceScope::New() ), mDrawableResolver( *this ), + mWebResourceCache( WebResourceCache::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. @@ -160,6 +161,7 @@ UISceneNode::UISceneNode( EE::Window::Window* window ) : mRoot->setParent( this )->setPosition( 0, 0 )->setId( "uiscenenode_root_node" ); mRoot->enableReportSizeChangeToChildren(); mAsyncResourceLoadState->owner.store( this, std::memory_order_release ); + mDocumentSessionId = mWebResourceCache->createSession(); resizeNode( mWindow ); } @@ -175,6 +177,8 @@ UISceneNode::~UISceneNode() { mHostUISceneNode->unregisterChildUISceneNode( this ); clearFontFaces(); + if ( mWebResourceCache && mDocumentSessionId ) + mWebResourceCache->destroySession( mDocumentSessionId ); eeSAFE_DELETE( mUIThemeManager ); eeSAFE_DELETE( mUIIconThemeManager ); @@ -762,6 +766,94 @@ UISceneNode* UISceneNode::setResourceScope( ResourceScopePtr resourceScope ) { return this; } +UISceneNode* UISceneNode::setWebResourceCache( WebResourceCachePtr cache, + CachePartitionId partition ) { + if ( !cache ) + cache = WebResourceCache::New(); + if ( cache == mWebResourceCache && + ( partition == 0 || partition == cache->getSessionPartition( mDocumentSessionId ) ) ) + return this; + if ( mWebResourceCache && mDocumentSessionId ) + mWebResourceCache->destroySession( mDocumentSessionId ); + mWebResourceCache = std::move( cache ); + mDocumentSessionId = mWebResourceCache->createSession( partition ); + return this; +} + +Uint64 UISceneNode::beginDocumentNavigation( const URI& uri ) { + return mWebResourceCache && mDocumentSessionId + ? mWebResourceCache->beginNavigation( mDocumentSessionId, uri ) + : 0; +} + +Uint64 UISceneNode::getDocumentGeneration() const { + return mWebResourceCache && mDocumentSessionId + ? mWebResourceCache->getSessionGeneration( mDocumentSessionId ) + : 0; +} + +void UISceneNode::requestWebResource( WebResourceRequest request, + WebResourceCache::Callback callback ) { + if ( !mWebResourceCache || !mDocumentSessionId ) + return; + if ( !mReferer.empty() ) + request.headers.emplace( "referer", mReferer.toString() ); + std::string cookie = mCookieManager.getCookieHeader( request.uri.getAuthority() ); + if ( !cookie.empty() ) + request.headers["Cookie"] = std::move( cookie ); + auto resourceState = mAsyncResourceLoadState; + Uint64 resourceGeneration = + resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0; + auto wrapped = [resourceState, resourceGeneration, callback = std::move( callback ), + authority = request.uri.getAuthority()]( const WebResourceResult& result ) { + if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) ) + return; + UISceneNode* scene = resourceState->owner.load( std::memory_order_acquire ); + if ( !scene ) + return; + if ( !result.setCookie.empty() ) + scene->mCookieManager.storeCookiesFromHeader( authority, result.setCookie ); + if ( callback ) + callback( result ); + }; + mWebResourceCache->requestData( mDocumentSessionId, getDocumentGeneration(), + std::move( request ), std::move( wrapped ) ); +} + +TexturePtr UISceneNode::requestWebTexture( WebResourceRequest request, + WebResourceCache::Callback callback ) { + if ( !mWebResourceCache || !mDocumentSessionId ) + return {}; + if ( !mReferer.empty() ) + request.headers.emplace( "referer", mReferer.toString() ); + std::string cookie = mCookieManager.getCookieHeader( request.uri.getAuthority() ); + if ( !cookie.empty() ) + request.headers["Cookie"] = std::move( cookie ); + auto resourceState = mAsyncResourceLoadState; + Uint64 resourceGeneration = + resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0; + request.completionDispatcher = [resourceState, + resourceGeneration]( std::function completion ) { + UISceneNode::runAsyncResourceOnMainThread( + resourceState, resourceGeneration, + [completion = std::move( completion )]( UISceneNode* ) { completion(); } ); + }; + auto wrapped = [resourceState, resourceGeneration, callback = std::move( callback ), + authority = request.uri.getAuthority()]( const WebResourceResult& result ) { + if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) ) + return; + UISceneNode* scene = resourceState->owner.load( std::memory_order_acquire ); + if ( !scene ) + return; + if ( !result.setCookie.empty() ) + scene->mCookieManager.storeCookiesFromHeader( authority, result.setCookie ); + if ( callback ) + callback( result ); + }; + return mWebResourceCache->requestTexture( mDocumentSessionId, getDocumentGeneration(), + std::move( request ), std::move( wrapped ) ); +} + 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; @@ -1632,47 +1724,48 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles, URI baseUR auto resourceState = mAsyncResourceLoadState; Uint64 resourceGeneration = resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0; - Http::getAsync( - [resourceState, resourceGeneration, internalFontName, authorFamily, fontStyle, - fontWeight, path]( const Http&, Http::Request&, Http::Response& response ) { - if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, - resourceGeneration ) ) - return; + WebResourceRequest request; + request.uri = URI( path ); + request.kind = WebResourceKind::Font; + request.timeout = Seconds( 5 ); + requestWebResource( std::move( request ), [resourceState, resourceGeneration, + internalFontName, authorFamily, fontStyle, + fontWeight, + path]( const WebResourceResult& result ) { + if ( !UISceneNode::isAsyncResourceLoadCurrent( resourceState, resourceGeneration ) ) + return; - if ( response.isOK() && !response.getBody().empty() ) { - std::string fontData( response.getBody() ); - UISceneNode::runAsyncResourceOnMainThread( - resourceState, resourceGeneration, - [fontData = std::move( fontData ), internalFontName, authorFamily, - fontStyle, fontWeight]( UISceneNode* scene ) mutable { - FontTrueType* font = FontTrueType::New( internalFontName ); - if ( font->loadFromMemory( &fontData[0], fontData.size() ) && - font->loaded() ) { - font->setVariableFontWeight( fontWeight ); - scene->registerFontFaceAlias( authorFamily, fontStyle, - fontWeight, font ); - scene->mFontFaces.push_back( font ); - if ( scene->mRoot ) - scene->mRoot->reloadFontFamily(); - } else { - eeSAFE_DELETE( font ); - } - } ); - } else { - UISceneNode::runAsyncResourceOnMainThread( - resourceState, resourceGeneration, - [internalFontName, path, status = response.getStatus(), - statusDescription = - std::string( response.getStatusDescription() )]( UISceneNode* ) { - Log::error( "UISceneNode::loadFontFaces: Failed to load font " - "\"%s\", from: %s. Request response status code: %d " - "(%s)", - internalFontName, path, status, - statusDescription.c_str() ); - } ); - } - }, - URI( path ), Seconds( 5 ) ); + if ( result.success && result.data && !result.data->empty() ) { + std::string fontData( *result.data ); + UISceneNode::runAsyncResourceOnMainThread( + resourceState, resourceGeneration, + [fontData = std::move( fontData ), internalFontName, authorFamily, + fontStyle, fontWeight]( UISceneNode* scene ) mutable { + FontTrueType* font = FontTrueType::New( internalFontName ); + if ( font->loadFromMemory( &fontData[0], fontData.size() ) && + font->loaded() ) { + font->setVariableFontWeight( fontWeight ); + scene->registerFontFaceAlias( authorFamily, fontStyle, fontWeight, + font ); + scene->mFontFaces.push_back( font ); + if ( scene->mRoot ) + scene->mRoot->reloadFontFamily(); + } else { + eeSAFE_DELETE( font ); + } + } ); + } else { + UISceneNode::runAsyncResourceOnMainThread( + resourceState, resourceGeneration, + [internalFontName, path, status = result.status, + statusDescription = result.error]( UISceneNode* ) { + Log::error( "UISceneNode::loadFontFaces: Failed to load font " + "\"%s\", from: %s. Request response status code: %d " + "(%s)", + internalFontName, path, status, statusDescription.c_str() ); + } ); + } + } ); } else if ( VFS::instance()->fileExists( path ) ) { FontTrueType* font = FontTrueType::New( makeInternalFontName( authorFamily, fontStyle, fontWeight ) ); @@ -1786,26 +1879,27 @@ void UISceneNode::loadCSS( URI uri, std::optional