mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
Fix HTTP pool callback re-entry deadlock:
Destroy pooled HTTP clients after releasing the pool mutex so joined callbacks can safely re-enter the global pool. Add a regression test covering concurrent pool clearing and callback re-entry. Document the revised texture lifetime architecture and track the prerequisite bugs discovered during the resource ownership audit.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
# Resource-refactor prerequisite bug fixes
|
||||
|
||||
Status: active defect track, 2026-07-12.
|
||||
|
||||
This document isolates correctness defects discovered during the shared-resource ownership audit.
|
||||
They should be fixed before the public resource API refactor wherever practical. Fixes in this track
|
||||
must preserve current ownership APIs unless the defect cannot be corrected safely without the later
|
||||
structural migration.
|
||||
|
||||
Related documents:
|
||||
|
||||
- `resource_shared_ownership_architecture.md`
|
||||
- `resource_shared_ownership_stage0_inventory.md`
|
||||
|
||||
## 1. Landing rules
|
||||
|
||||
- One defect or tightly coupled lifetime defect per change.
|
||||
- Add focused regression coverage before or with the fix.
|
||||
- Do not introduce ResourcePtr, catalogs, scopes or compatibility APIs in this track.
|
||||
- Preserve current TextureFactory ownership until the Stage 2 holder cut.
|
||||
- Run the relevant focused suite plus repeated Engine create/destroy coverage for teardown changes.
|
||||
- Run ASAN for UAF/double-delete defects and TSAN for shared callback/queue synchronization defects.
|
||||
|
||||
## 2. Priority A: shutdown and asynchronous lifetime
|
||||
|
||||
### A1. HTTP Pool destruction while holding its mutex
|
||||
|
||||
Current behavior:
|
||||
|
||||
`Http::Pool::clear()` clears `mHttps` while holding `mMutex`. Destroying an Http joins local async
|
||||
requests. A callback that re-enters the global Pool then waits for the same mutex, while `clear()`
|
||||
waits for the callback to finish.
|
||||
|
||||
Fix:
|
||||
|
||||
- Swap the client map into a local container under the mutex.
|
||||
- Release the mutex.
|
||||
- Destroy/join clients from the local container.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- An async callback re-enters `Pool::get()` while another thread calls `Pool::clear()`.
|
||||
- The test completes under a bounded timeout without deadlock.
|
||||
|
||||
Status: implemented with `Http.poolClearAllowsCallbackReentry`; focused ASAN suite passes.
|
||||
|
||||
### A2. Shared ThreadPool tasks capture a raw Http
|
||||
|
||||
Current behavior:
|
||||
|
||||
When `Http::setThreadPool()` is configured, async lambdas capture raw `this`. Http tracks only its
|
||||
privately created AsyncRequest threads for joining. Pool clear can destroy Http while a queued or
|
||||
running shared-pool lambda still dereferences it.
|
||||
|
||||
Fix requirements:
|
||||
|
||||
- Every scheduled operation has lifetime state independent of raw Http.
|
||||
- Http destruction can cancel and wait for all operations using that Http, regardless of executor.
|
||||
- Waiting never occurs while holding Pool, request-map or callback-visible locks.
|
||||
- Do not destroy an externally owned ThreadPool as part of Http shutdown.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Queue a request behind blocked shared-pool work, clear the Http Pool, then release the blocker.
|
||||
- Running and queued variants complete/cancel without UAF under ASAN.
|
||||
- Callback re-entry does not deadlock.
|
||||
|
||||
### A3. Engine stops HTTP/resource producers too late
|
||||
|
||||
Current behavior:
|
||||
|
||||
Engine clears the global HTTP Pool after textures, Renderer, shaders, framebuffers and vertex buffers
|
||||
have been destroyed. Callbacks may still mutate placeholders, create textures or queue UI delivery.
|
||||
|
||||
Fix requirements:
|
||||
|
||||
- Reject new Engine-owned resource deliveries first.
|
||||
- Cancel/join HTTP/resource-producing operations before scenes and Graphics managers are destroyed.
|
||||
- Preserve callback lock ordering established by A1/A2.
|
||||
- Shared application ThreadPools remain externally owned, but no task may retain Engine-owned state
|
||||
beyond the shutdown barrier.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Destroy Engine with pending HTTP and decode work.
|
||||
- Repeat Engine creation/destruction in the same test process.
|
||||
- Assert no callback touches the destroyed scene/factory and no singleton is recreated.
|
||||
|
||||
### A4. UISceneNode static delivery queue lacks shutdown semantics
|
||||
|
||||
Current behavior:
|
||||
|
||||
Worker/HTTP paths can append main-thread scene deliveries to process-static state. Normal scene
|
||||
updates drain it, but Engine teardown has no explicit reject/purge boundary.
|
||||
|
||||
Fix requirements:
|
||||
|
||||
- Define close/reject/purge operations owned by UI lifecycle state.
|
||||
- Invalidate scene generations before purging captured work.
|
||||
- Release captured resources on the main/update thread according to project contract.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Queue delivery, destroy scene/Engine before update, recreate Engine, and verify old delivery never
|
||||
executes against new state.
|
||||
|
||||
## 3. Priority B: deterministic destruction order
|
||||
|
||||
### B1. Renderer destroyed before ShaderProgramManager
|
||||
|
||||
Current behavior:
|
||||
|
||||
Renderer destruction clears `GLi`; ShaderProgram and Shader destructors subsequently call GL delete
|
||||
through it.
|
||||
|
||||
Fix:
|
||||
|
||||
- Destroy ShaderProgramManager before Renderer while a valid context is current.
|
||||
- Audit Renderer-owned raw program views so manager destruction cannot trigger Renderer callbacks.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Create/link programs, destroy Engine, and repeat under ASAN.
|
||||
|
||||
### B2. TextLayout cache destroyed after FontManager
|
||||
|
||||
Current behavior:
|
||||
|
||||
Cached shaped glyphs retain raw FontTrueType pointers. The global TextLayout cache is currently
|
||||
cleared after FontManager destruction.
|
||||
|
||||
Fix:
|
||||
|
||||
- Clear TextLayout and related shaped-font caches before FontManager.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Populate shaped-layout cache, destroy Engine, and verify repeated Engine lifecycle under ASAN.
|
||||
|
||||
### B3. Scene/global resource manager order
|
||||
|
||||
Current behavior:
|
||||
|
||||
GlobalBatchRenderer and NinePatchManager are destroyed before SceneManager even though scenes can
|
||||
retain or submit their resources.
|
||||
|
||||
Fix requirements:
|
||||
|
||||
- Stop submissions and destroy scenes before global drawable/resource providers they can reference.
|
||||
- Explicitly flush or discard pending batch state before deleting referenced resources.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Destroy an Engine with live scene widgets using nine-patches and a non-empty batch.
|
||||
|
||||
## 4. Priority C: loader and callback lifetime
|
||||
|
||||
### C1. TextureAtlasLoader member destruction order
|
||||
|
||||
Current behavior:
|
||||
|
||||
`ResourceLoader mRL` is declared before callback-visible loader state. C++ destroys members in
|
||||
reverse declaration order, so that state dies before mRL joins its work.
|
||||
|
||||
Fix:
|
||||
|
||||
- Add an explicit destructor shutdown/join before any callback-visible member is destroyed, or move
|
||||
async operation state into a lifetime object that outlives execution.
|
||||
- Do not rely only on member declaration order without an explicit invariant comment/test.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Destroy a loader immediately with queued texture tasks and completion callbacks.
|
||||
|
||||
### C2. TextureLoader static callback registry is unsynchronized and process-persistent
|
||||
|
||||
Current behavior:
|
||||
|
||||
TextureLoader callback state is process-static, can be touched by asynchronous loading, and has no
|
||||
clear test/Engine lifecycle boundary.
|
||||
|
||||
Fix requirements:
|
||||
|
||||
- Synchronize registration/removal/invocation or constrain all access with an asserted thread
|
||||
contract.
|
||||
- Define reset behavior for repeated Engine tests.
|
||||
- Never invoke callbacks while holding the callback registry lock.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Concurrent registration/removal/invocation under TSAN.
|
||||
- Engine recreation does not inherit callbacks from a previous fixture.
|
||||
|
||||
## 5. Priority D: existing ownership and GL-handle defects
|
||||
|
||||
### D1. Models::Variant copies raw drawable ownership
|
||||
|
||||
Current behavior:
|
||||
|
||||
Variant copying duplicates the same Drawable pointer and its owning flag. Two Variants can therefore
|
||||
believe they exclusively own one allocation.
|
||||
|
||||
Near-term options:
|
||||
|
||||
- Make owning Drawable Variants non-copyable until Stage 4, or deep-clone where a correct clone
|
||||
contract exists.
|
||||
- Never silently convert the second copy to a borrow without documenting its dominating owner.
|
||||
|
||||
Final resolution:
|
||||
|
||||
- Stage 4 replaces the manual union/owner flag with `std::variant` and DrawablePtr.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Copy/move/reset/destruct every supported drawable Variant ownership mode under ASAN.
|
||||
|
||||
### D2. UISkin/StateListDrawable shallow ownership copying
|
||||
|
||||
Current behavior:
|
||||
|
||||
StateListDrawable stores raw children plus a separate ownership map. UISkin cloning can shallow-copy
|
||||
child pointers and ownership claims, creating double-delete or shared-mutation behavior.
|
||||
|
||||
Near-term fix:
|
||||
|
||||
- Prevent ownership duplication during clone and define whether children are deep-cloned or borrowed
|
||||
from an explicitly dominant theme owner.
|
||||
|
||||
Final resolution:
|
||||
|
||||
- Stage 4 uses per-consumer instances and shared immutable source handles.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Clone and destroy skins/state lists in both orders under ASAN.
|
||||
|
||||
### D3. Texture copy constructor copies the GL handle
|
||||
|
||||
Current behavior:
|
||||
|
||||
The protected Texture copy constructor copies `mTexture`. If exercised, two Texture objects can
|
||||
delete or mutate the same GL handle while otherwise presenting value-copy semantics.
|
||||
|
||||
Fix:
|
||||
|
||||
- Delete Texture copy construction/assignment unless a real GPU deep-copy operation is explicitly
|
||||
required.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Compile-time non-copyability checks.
|
||||
|
||||
### D4. FrameBufferFBO reload replaces handles without releasing old objects
|
||||
|
||||
Current behavior:
|
||||
|
||||
`FrameBufferFBO::reload()` calls `create()` again. `create()` assigns new framebuffer/renderbuffer
|
||||
handles without an explicit release of the previous objects. Determine whether this is exclusively a
|
||||
context-loss path where old names are already invalid; if it is callable with a live context, it
|
||||
leaks GPU objects.
|
||||
|
||||
Fix requirements:
|
||||
|
||||
- Distinguish context-loss recreation from live-context recreation.
|
||||
- Release existing live handles before replacement, but never delete names from a lost namespace.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- Repeated live-context reload does not increase tracked GL objects.
|
||||
- Context-loss reload does not attempt invalid deletion.
|
||||
|
||||
## 6. Deferred/refactor-bound findings
|
||||
|
||||
These are real hazards but are intentionally resolved in their owning migration stage:
|
||||
|
||||
- FrameBuffer attachment has conflicting direct/factory ownership: resolved in TexturePtr Stage 2.
|
||||
- TextureRegion and TextureAtlas depend on factory lifetime: resolved in the complete Stage 2 holder
|
||||
cut, not piecemeal.
|
||||
- Drawable draw-time mutation and false `isStateful()` classifications: resolved by the Stage 4
|
||||
source/instance split.
|
||||
- Global semantic lookup collisions/isolation: resolved by catalogs/scopes in Stage 3.
|
||||
- Web request partitioning, document leases and coalescing: resolved by WebResourceCache Stage 6.
|
||||
|
||||
## 7. Suggested execution order
|
||||
|
||||
1. A1 HTTP Pool lock fix and regression test.
|
||||
2. A2 shared ThreadPool Http lifetime.
|
||||
3. C1 TextureAtlasLoader destruction order.
|
||||
4. B1/B2/B3 Engine teardown ordering with lifecycle tests.
|
||||
5. A3/A4 producer and delivery shutdown barriers.
|
||||
6. C2 static TextureLoader callbacks.
|
||||
7. D1/D2/D3/D4 isolated ownership/handle defects.
|
||||
8. Re-run the Stage 0 source audit, then begin Stage 1 TextureFactory lifetime scaffolding.
|
||||
@@ -0,0 +1,739 @@
|
||||
# eepp shared-resource ownership architecture
|
||||
|
||||
Status: architecture baseline, revised after lifetime-contract review, 2026-07-12.
|
||||
|
||||
Stage 0 inventory and shutdown dependency graph:
|
||||
`resource_shared_ownership_stage0_inventory.md`.
|
||||
|
||||
Prerequisite defect track:
|
||||
`resource_refactor_prerequisite_bugfixes.md`.
|
||||
|
||||
This document supersedes `resource_shared_ownership_refactor_plan.md`. It incorporates the review
|
||||
of that draft and 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 explicit architecture revision.
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Replace raw manager ownership, global load side effects, manual deletion, and `ownIt` flags with
|
||||
explicit shared ownership throughout eepp and all in-repository consumers.
|
||||
|
||||
The final model is:
|
||||
|
||||
- Consumers, immutable source objects, catalogs, and caches own resources with strong handles.
|
||||
- A live registry observes resources weakly for diagnostics, accounting, context recovery, and leak
|
||||
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.
|
||||
- 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
|
||||
requires a UISceneNode.
|
||||
|
||||
This is an intentional repository-wide API break. There will be no compatibility API, no `Shared`
|
||||
suffixes, and no retained `ownIt` overloads.
|
||||
|
||||
## 2. Confirmed hazards in the current code
|
||||
|
||||
These are not hypothetical migration risks:
|
||||
|
||||
- `TextureAtlasLoader` queues `TextureFactory::loadFromFile()` and `loadFromPack()` calls, discards
|
||||
their results, then later resolves the textures globally by name. Immediately switching the
|
||||
factory to weak, unpinned retention would compile and destroy each loaded texture at the end of
|
||||
the lambda.
|
||||
- `TextureLoader` stores and returns `Texture*`; its callback and unload behavior depend on the
|
||||
factory owning the object.
|
||||
- `TextureRegion` stores `Texture*`, and constructors taking a texture ID resolve that raw pointer
|
||||
through the factory.
|
||||
- `FrameBuffer` stores `Texture*` and deletes it directly in its destructor.
|
||||
- `TextureAtlas`, glyph drawables, SVG icon raster caches, GIF loading, and font caches retain raw
|
||||
texture pointers.
|
||||
- `Texture::~Texture()` performs GL deletion and reaches `Engine::instance()` and
|
||||
`TextureFactory::instance()`. Shared ownership must replace this singleton-dependent destruction
|
||||
with TextureFactory-controlled deferred release on the graphics thread.
|
||||
- `TextureRegion::isStateful()` and `DrawableGroup::isStateful()` return false despite draw/update
|
||||
methods mutating destination size, position, or child state.
|
||||
- `UIImage`, `StateListDrawable`, and `DrawableGroup` temporarily mutate drawable color, alpha,
|
||||
size, position, or children while drawing.
|
||||
- `Models::Variant` copies both an owning flag and the same raw drawable pointer, allowing two
|
||||
copies to believe they exclusively own one allocation.
|
||||
- `EE_MEMORY_MANAGER` requires allocation registration through `eeNew` and removal through
|
||||
`eeDelete`; default `shared_ptr` deletion of an `eeNew` allocation would leave tracking invalid.
|
||||
- `Engine::~Engine()` destroys `Renderer` before `ShaderProgramManager`, while
|
||||
`ShaderProgram::~ShaderProgram()` calls `GLi->deleteProgram()` directly.
|
||||
- The global HTTP pool is cleared after Graphics resources and managers, allowing asynchronous
|
||||
producers to outlive systems they can mutate.
|
||||
|
||||
## 3. Frozen ownership vocabulary
|
||||
|
||||
### 3.1 Public handle representation
|
||||
|
||||
eepp will expose `std::shared_ptr` and `std::weak_ptr` through consistent aliases:
|
||||
|
||||
```cpp
|
||||
template <typename T> using ResourcePtr = std::shared_ptr<T>;
|
||||
template <typename T> using ResourceWeakPtr = std::weak_ptr<T>;
|
||||
|
||||
using TexturePtr = ResourcePtr<Texture>;
|
||||
using TextureWeakPtr = ResourceWeakPtr<Texture>;
|
||||
```
|
||||
|
||||
This is an explicit API/ABI choice. eepp accepts that users can use standard shared-pointer
|
||||
operations. The library nevertheless exposes no API for adopting an arbitrary resource raw pointer,
|
||||
and resource constructors remain protected/private where practical.
|
||||
|
||||
Requirements:
|
||||
|
||||
- A resource allocation has exactly one control block.
|
||||
- Owning APIs return handles. They do not return a raw pointer plus an ownership convention.
|
||||
- Long-lived borrowed raw pointers are forbidden unless the field or API documents the owner that
|
||||
dominates the borrow. Local `.get()` views inside a call are allowed.
|
||||
- Shared-library and static-library builds must test the chosen handle/deleter behavior.
|
||||
|
||||
### 3.2 Centralized creation and deletion
|
||||
|
||||
All ref-counted eepp resource allocations go through one internal creation path with an
|
||||
eepp-compatible deleter:
|
||||
|
||||
```cpp
|
||||
template <typename T> struct ResourceDeleter {
|
||||
void operator()( T* resource ) const noexcept { eeDelete( resource ); }
|
||||
};
|
||||
|
||||
template <typename T, typename... Args>
|
||||
ResourcePtr<T> makeResource( Args&&... args );
|
||||
```
|
||||
|
||||
Factories use an equivalent private helper for protected constructors. `std::make_shared` is not
|
||||
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.
|
||||
|
||||
### 3.3 Identity, keys, and labels
|
||||
|
||||
These concepts are distinct:
|
||||
|
||||
- `ResourceId` is immutable and process-unique across Engine recreation in tests.
|
||||
- `ResourceKey` is the immutable canonical semantic lookup key. Equality compares the complete key,
|
||||
never only a hash.
|
||||
- `displayName` is diagnostic text and may change without changing identity or catalog indexes.
|
||||
- Aliases are catalog entries, not mutable fields used as registry indexes.
|
||||
|
||||
A process-wide monotonic ID source must not reset when an Engine singleton is recreated by tests.
|
||||
|
||||
## 4. Graphics ownership layers
|
||||
|
||||
```text
|
||||
Engine
|
||||
├── Renderer and contexts
|
||||
├── TextureFactory
|
||||
│ ├── weak live-texture registry
|
||||
│ └── deferred released-texture queue
|
||||
├── GlobalResourceCatalog (intentional strong persistence)
|
||||
└── Default ResourceScope (local catalog + explicit imports)
|
||||
|
||||
Application/scene ResourceScope
|
||||
├── local ResourceCatalog
|
||||
├── explicitly imported catalogs
|
||||
└── typed caches
|
||||
|
||||
UI::DrawableResolver
|
||||
├── CSS/image/icon/glyph parsing
|
||||
├── node and UISceneNode context
|
||||
└── delegates texture/source lookup to Graphics::ResourceScope
|
||||
|
||||
UI/Network::WebResourceCache
|
||||
├── request/cache partitioning
|
||||
├── in-flight request coalescing
|
||||
├── TTL/LRU/byte-budget retention
|
||||
└── per-document leases and subscribers
|
||||
```
|
||||
|
||||
Engine coordinates the Graphics lifetime roots directly. TextureFactory coordinates texture
|
||||
creation, weak observation, reload and deferred destruction, but does not provide semantic lookup or
|
||||
normal strong retention. No Graphics class depends on UI.
|
||||
|
||||
### 4.1 LiveResourceRegistry
|
||||
|
||||
The registry observes every instantiated texture weakly:
|
||||
|
||||
```cpp
|
||||
struct TextureRecord {
|
||||
ResourceId id;
|
||||
ResourceKey creationKey;
|
||||
std::string displayName;
|
||||
TextureWeakPtr texture;
|
||||
std::shared_ptr<ResourceMetrics> metrics;
|
||||
ResourceFlags flags;
|
||||
};
|
||||
```
|
||||
|
||||
It has no strong resource field, no `ownerScope`, no semantic name resolution, and no public
|
||||
`unregister()` operation.
|
||||
|
||||
Primary operations are snapshots, expiration purging, and live iteration. Diagnostic snapshots
|
||||
return metadata plus weak handles, not a vector of owning texture handles:
|
||||
|
||||
```cpp
|
||||
TextureRegistrySnapshot snapshotTextures() const;
|
||||
void purgeExpired();
|
||||
```
|
||||
|
||||
Opening `UITextureViewer` must not retain all textures. The viewer may lock a weak handle for one
|
||||
render operation and may strongly retain only a user-selected texture.
|
||||
|
||||
For context loss/reload, the registry locks live weak handles into a temporary strong vector while
|
||||
holding its mutex, releases the mutex, and then performs device work. Destruction, callbacks, and GL
|
||||
operations never occur while a registry lock is held.
|
||||
|
||||
### 4.2 ResourceMetrics
|
||||
|
||||
Texture memory accounting is held in shared record state captured at creation. Texture upload,
|
||||
resize, replacement, and CPU-copy changes update atomics on that state directly; they never find a
|
||||
factory singleton. Registry snapshots can read metrics even while expiration races with inspection.
|
||||
|
||||
### 4.3 ResourceCatalog
|
||||
|
||||
A catalog maps complete canonical keys/aliases to strong handles. It provides semantic lookup and
|
||||
intentional persistence:
|
||||
|
||||
```cpp
|
||||
class ResourceCatalog {
|
||||
public:
|
||||
void publish( ResourceKey key, TexturePtr texture );
|
||||
TexturePtr findTexture( const ResourceKey& key ) const;
|
||||
bool erase( const ResourceKey& key );
|
||||
void clear();
|
||||
};
|
||||
```
|
||||
|
||||
Publishing is the final global pin model. The low-level TextureFactory has no `ResourcePin` argument
|
||||
and no factory-owned strong pin. Independent catalogs/caches naturally provide independent pins.
|
||||
If temporary explicit pinning is needed, `ResourceCatalog::pin()` returns an independent RAII token;
|
||||
there is no shared boolean or one global `strong` field.
|
||||
|
||||
A higher-level scope convenience may accept a retention option and publish into that scope's catalog,
|
||||
but texture creation and decoding remain unpinned operations.
|
||||
|
||||
### 4.4 ResourceScope
|
||||
|
||||
`Graphics::ResourceScope` performs Graphics-only lookup and loading:
|
||||
|
||||
```cpp
|
||||
class ResourceScope {
|
||||
public:
|
||||
TexturePtr findTexture( const ResourceKey& key ) const;
|
||||
TexturePtr loadTexture( const TextureRequest& request );
|
||||
void publishLocal( ResourceKey key, TexturePtr texture );
|
||||
void importCatalog( ResourceCatalogPtr catalog );
|
||||
};
|
||||
```
|
||||
|
||||
Frozen lookup rules:
|
||||
|
||||
- Search the local catalog, then explicitly imported catalogs in deterministic order.
|
||||
- Never search the live registry.
|
||||
- Never implicitly search a parent, host scene, sibling scene, or every live resource.
|
||||
- The default Graphics scope imports the global catalog explicitly.
|
||||
- A UI/application scene receives only the catalogs deliberately imported into it.
|
||||
- A Web document does not inherit host/global resources unless the host exports and imports them
|
||||
intentionally.
|
||||
- Scopes import catalogs, not arbitrary scopes. This avoids recursive lookup and import cycles.
|
||||
|
||||
Pure `EE::Graphics` users may use TextureFactory for unpinned creation or Engine's default Graphics
|
||||
scope/catalog for named persistent resources. No UISceneNode is involved.
|
||||
|
||||
### 4.5 TextureFactory final API role
|
||||
|
||||
TextureFactory creates, decodes, uploads, and updates textures. Creation names return `TexturePtr`
|
||||
directly and do not retain it:
|
||||
|
||||
```cpp
|
||||
TexturePtr createEmptyTexture( ... );
|
||||
TexturePtr loadFromPixels( ... );
|
||||
TexturePtr loadFromPack( ... );
|
||||
TexturePtr loadFromMemory( ... );
|
||||
TexturePtr loadFromStream( ... );
|
||||
TexturePtr loadFromFile( ... );
|
||||
```
|
||||
|
||||
The factory registers every result with its weak live registry. It has no semantic `getByName()` or
|
||||
`getByHash()` API, no public deletion/removal API, no public registry detachment, and no owning
|
||||
`getTextures()` API. Named lookup belongs to a catalog/scope; diagnostics use registry snapshots.
|
||||
|
||||
The `TexturePtr` control block uses a factory-controlled deleter. Final release appends the raw
|
||||
texture to TextureFactory's released queue; it does not execute `Texture::~Texture()` immediately.
|
||||
`Window::display()` flushes pending batches and then calls
|
||||
`TextureFactory::collectReleasedTextures()` while the active context is current. Engine shutdown
|
||||
performs the same collection explicitly because no later display is guaranteed.
|
||||
|
||||
## 5. GPU lifetime and threading
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
### 5.2 Texture deferred destruction
|
||||
|
||||
Final `TexturePtr` release queues the Texture object in TextureFactory. It remains allocated until a
|
||||
safe collection point:
|
||||
|
||||
```cpp
|
||||
void Window::display( bool clear ) {
|
||||
GlobalBatchRenderer::instance()->draw();
|
||||
TextureFactory::instance()->collectReleasedTextures();
|
||||
swapBuffers();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Batch flushing precedes collection because the current renderer still keeps borrowed texture state.
|
||||
Long-lived render queues must eventually retain TexturePtr themselves. The released queue is also
|
||||
drained during Engine shutdown after consumers are released and before TextureFactory, Renderer or
|
||||
contexts are destroyed.
|
||||
|
||||
Other self-contained GPU classes retain their direct, graphics-thread destruction model. They are
|
||||
not routed through TextureFactory and do not acquire generic lifetime machinery unless a later
|
||||
ownership migration demonstrates a concrete need.
|
||||
|
||||
### 5.3 Resource mutation and loading threads
|
||||
|
||||
- Decode and network work may run off-thread.
|
||||
- GPU create/upload/reload/mutation and final ownership release obey the graphics-thread/shared-
|
||||
context contract.
|
||||
- Registries and caches are thread-safe at their boundaries.
|
||||
- UI mutation executes on the scene/main thread and remains protected by scene generation tokens.
|
||||
- A generation token controls whether a subscriber may mutate a scene; it does not own a resource or
|
||||
a shared request.
|
||||
- No callback, resource destruction, or device command executes while a registry/cache mutex is held.
|
||||
|
||||
## 6. Engine shutdown and restart contract
|
||||
|
||||
Engine teardown must be reordered around producer shutdown, consumer release and valid GL contexts:
|
||||
|
||||
1. Mark Engine and Web cache/resource delivery services as shutting down; reject new work.
|
||||
2. Invalidate scene/document async subscribers and stop accepting main-thread resource deliveries.
|
||||
3. Cancel/stop and join resource/network/decode producers that can create resources or callbacks.
|
||||
4. Destroy scenes, documents, UI resolvers, document leases, scene scopes, and application caches.
|
||||
5. Clear application/global catalogs and remaining manager-owned handles.
|
||||
6. Flush/discard pending rendering submissions, then collect TextureFactory's released textures.
|
||||
7. Inspect the weak texture registry. Debug/tests assert that no unexpected strong TexturePtr
|
||||
remains; a defensive shutdown sweep may release the GPU payload of reported survivors.
|
||||
8. Destroy device-dependent managers in audited order while Renderer and contexts remain valid.
|
||||
9. Destroy TextureFactory, Renderer, windows/contexts and backend state in that order.
|
||||
|
||||
The exact manager list will be produced by the Stage 1 GPU audit, but these order constraints are
|
||||
fixed:
|
||||
|
||||
- HTTP/decode producers stop before resource consumers and GPU systems are dismantled.
|
||||
- `ShaderProgramManager` releases programs before Renderer/GL dispatch is destroyed.
|
||||
- TextureFactory's deferred released queue is empty before its context disappears.
|
||||
- A TexturePtr surviving Engine destruction is a project-contract violation, reported by debug
|
||||
builds and tests rather than supported through a second device-lifetime architecture.
|
||||
- Destructors cannot recreate singletons.
|
||||
- Tests release all resource handles before recreating Engine state from zero.
|
||||
|
||||
## 7. Drawable model
|
||||
|
||||
Shared lifetime and shareable instance state are separate concerns. `isStateful()` is not a sharing
|
||||
contract and will not be used as one.
|
||||
|
||||
### 7.1 Frozen source/instance split
|
||||
|
||||
Resource resolution caches immutable source data. UI consumers own per-consumer drawable instances:
|
||||
|
||||
```cpp
|
||||
using DrawableSourcePtr = ResourcePtr<const DrawableSource>;
|
||||
using DrawablePtr = ResourcePtr<Drawable>;
|
||||
|
||||
DrawableSourcePtr DrawableResolver::findSource( const DrawableRequest& request );
|
||||
DrawablePtr DrawableResolver::createDrawable( const DrawableRequest& request );
|
||||
```
|
||||
|
||||
Representative split:
|
||||
|
||||
- `Texture` is shared GPU/resource data, not a globally shared mutable drawable instance.
|
||||
- `TextureRegionSource` contains a TexturePtr, immutable source rectangle, offset, and intrinsic size.
|
||||
- `NinePatchSource` contains immutable region and border data.
|
||||
- `TextureDrawable`/`TextureRegionDrawable` hold per-consumer destination size, tint, alpha, position,
|
||||
and other presentation state while retaining their source.
|
||||
- `StateListDrawable`, `DrawableGroup`, and `Sprite` are per-consumer state machines/instances that
|
||||
refer to source handles or private child instances.
|
||||
|
||||
`DrawableImageParser::createDrawable()` always returns a fresh consumer instance for CSS-generated
|
||||
or resolved content, even when its immutable source came from a cache.
|
||||
|
||||
The migration will remove draw-time mutation of shared child/source objects. Rendering APIs may use
|
||||
external draw parameters where that simplifies an implementation, but no shared source can be
|
||||
temporarily recolored, resized, repositioned, or advanced by a consumer.
|
||||
|
||||
### 7.2 Consumer API
|
||||
|
||||
Consumers store a strong per-consumer instance:
|
||||
|
||||
```cpp
|
||||
UIImage* UIImage::setDrawable( DrawablePtr drawable );
|
||||
DrawablePtr UIImage::getDrawable() const;
|
||||
```
|
||||
|
||||
The same rule applies to CSS layers, buttons, skins, icon instances, state lists, and groups. All
|
||||
`ownIt` flags and manual drawable deletion paths are removed in the same drawable migration stage.
|
||||
|
||||
`Models::Variant` will be structurally migrated to `std::variant` (or an equivalently safe
|
||||
non-union representation) with `DrawablePtr` as a normal non-trivial member. Its old owning flag and
|
||||
raw drawable alternative are removed.
|
||||
|
||||
### 7.3 Resource notifications
|
||||
|
||||
`DrawableResource::Unload` is removed as a consumer lifetime mechanism. A strong owner cannot be
|
||||
notified that its object vanished, and destructor callbacks into a partially destroyed most-derived
|
||||
object are unsafe.
|
||||
|
||||
Mutable/reloadable source data uses a typed change/invalidation signal with RAII connection tokens.
|
||||
Callbacks capture weak lifetime tokens rather than raw consumer `this` pointers. Registry expiration
|
||||
is observed through weak handles and snapshots, not an object destructor callback.
|
||||
|
||||
## 8. UI resolution layering
|
||||
|
||||
`DrawableSearcher` is replaced, but not by putting all of its behavior into Graphics::ResourceScope.
|
||||
|
||||
`UI::DrawableResolver` owns UI-specific interpretation:
|
||||
|
||||
- CSS gradients and functions
|
||||
- `url(...)` and scene-relative URI resolution
|
||||
- icons, glyphs, sprites, and generated drawable instances
|
||||
- node/scene context and UI source-to-instance creation
|
||||
|
||||
It delegates texture/source lookup and creation to the scene's `Graphics::ResourceScope`. A default
|
||||
UI resolver can use the default Graphics scope for UI applications without custom scenes, but pure
|
||||
Graphics does not depend on it.
|
||||
|
||||
Browser request policy, cookies, navigation, and cache leases are not responsibilities of either
|
||||
DrawableResolver or Graphics::ResourceScope.
|
||||
|
||||
## 9. Web document ownership and shared cache
|
||||
|
||||
### 9.1 Topology
|
||||
|
||||
Each UIWebView document has a distinct `DocumentSessionId`, document scope, and cache lease. Multiple
|
||||
documents may use one shared WebResourceCache:
|
||||
|
||||
```text
|
||||
Document scope/session A ─┐
|
||||
├── shared WebResourceCache partition
|
||||
Document scope/session B ─┘
|
||||
```
|
||||
|
||||
Navigation changes/releases only that document's lease. It never directly purges entries required
|
||||
by another document. Widgets retain their currently displayed resource/source through ordinary
|
||||
strong handles independently of cache retention.
|
||||
|
||||
### 9.2 Cache key and isolation
|
||||
|
||||
```cpp
|
||||
struct OriginKey {
|
||||
std::string scheme;
|
||||
std::string normalizedHost;
|
||||
Uint16 effectivePort;
|
||||
};
|
||||
|
||||
struct WebResourceKey {
|
||||
CachePartitionId partition;
|
||||
CanonicalURI uri;
|
||||
ResourceKind kind;
|
||||
DecodeOptions decode;
|
||||
RequestVariant requestVariant;
|
||||
};
|
||||
```
|
||||
|
||||
`CachePartitionId` represents the intentionally shared HTTP/cookie/authentication context. Different
|
||||
cookie jars or credential contexts do not share entries merely because a URI matches. Request
|
||||
variants account for content-affecting headers until full HTTP `Vary` support exists.
|
||||
|
||||
Canonicalization rules are fixed at the cache boundary:
|
||||
|
||||
- Same-origin means normalized scheme, host, and effective port all match.
|
||||
- URI fragments are removed; query strings remain part of the key.
|
||||
- Redirect metadata records both request URI and canonical final URI without merging partitions.
|
||||
- File paths use platform-aware canonicalization with explicit symlink/case behavior.
|
||||
- Data URIs use a content hash plus decode options and enforce resource-size limits.
|
||||
- Hashes accelerate lookup but full keys determine equality.
|
||||
|
||||
### 9.3 Entry state and request coalescing
|
||||
|
||||
```cpp
|
||||
enum class LoadState { Empty, Loading, Ready, Failed, Cancelled };
|
||||
|
||||
struct WebCacheEntry {
|
||||
WebResourceKey key;
|
||||
LoadState state;
|
||||
TexturePtr retainedResource;
|
||||
TextureWeakPtr liveResource;
|
||||
MonotonicTime lastUsed;
|
||||
MonotonicTime expiresAt;
|
||||
std::size_t retainedBytes;
|
||||
UnorderedSet<DocumentSessionId> activeLeases;
|
||||
std::vector<WeakSubscriber> subscribers;
|
||||
};
|
||||
```
|
||||
|
||||
Concurrent requests for one key share one fetch/decode/upload operation. Each subscriber has its own
|
||||
document session and scene generation. A stale subscriber is removed without cancelling delivery to
|
||||
other current subscribers. Cancellation of the shared operation occurs only when policy permits and
|
||||
no subscriber/cache requirement remains.
|
||||
|
||||
Retention uses monotonic TTL, LRU information, and per-partition/global byte budgets. Same-origin
|
||||
navigation may renew a document lease; cross-origin navigation releases that document's old-origin
|
||||
lease. External consumer handles remain valid regardless of cache eviction.
|
||||
|
||||
Failure entries define retry/backoff and do not become permanent accidental cache hits.
|
||||
|
||||
## 10. Migration strategy
|
||||
|
||||
No externally released intermediate state is required. Temporary duplicated retention is allowed on
|
||||
the feature branch to keep behavior valid while the repository-wide API break is assembled.
|
||||
|
||||
### Stage 0: contract freeze and inventories
|
||||
|
||||
Status: complete. See `resource_shared_ownership_stage0_inventory.md`.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- This architecture document accepted or amended.
|
||||
- Complete inventory of GPU resource classes and direct GL deletion sites.
|
||||
- Complete inventory of stored raw `Texture*`, texture-ID lookup, ignored texture-load returns,
|
||||
loader callback signatures, and factory deletion calls.
|
||||
- Complete drawable mutation/shareability inventory.
|
||||
- Build-matrix decision for shared/static libraries and `EE_MEMORY_MANAGER`.
|
||||
- Concrete shutdown dependency graph for Engine-owned producers, consumers, managers, Renderer, and
|
||||
contexts.
|
||||
|
||||
Exit criterion: no unresolved ownership, lookup, last-release-thread, Engine restart, scope import,
|
||||
or drawable-sharing contract blocks substrate implementation.
|
||||
|
||||
### Stage 0.5: prerequisite bug fixes
|
||||
|
||||
Land defects discovered by the ownership audit independently of the API refactor. Each fix receives
|
||||
focused regression coverage and preserves current raw factory ownership. Initial set:
|
||||
|
||||
- HTTP Pool clears clients outside its mutex so joined callbacks can re-enter without deadlock.
|
||||
- Externally executed HTTP tasks cannot retain a dangling raw Http after Pool destruction.
|
||||
- TextureAtlasLoader joins/stops ResourceLoader work before callback-visible loader state is
|
||||
destroyed.
|
||||
- Engine destroys ShaderProgramManager before Renderer and clears TextLayout before FontManager.
|
||||
- Engine stops asynchronous resource producers before resource consumers and GPU managers.
|
||||
- UISceneNode's static async delivery queue has an explicit shutdown purge/rejection boundary.
|
||||
- TextureLoader's process-static callback registry receives a synchronization/reset contract.
|
||||
- Models::Variant drawable copying and UISkin/StateListDrawable shallow ownership bugs receive
|
||||
immediate containment or regression tests before their structural Stage 4 replacement.
|
||||
|
||||
### Stage 1: texture lifetime scaffolding, with old factory retention still active
|
||||
|
||||
Implement stable ResourceId, ResourceMetrics, centralized eepp-compatible handle creation, the weak
|
||||
TextureFactory live registry, TextureFactory's deferred released-texture queue, shutdown diagnostics,
|
||||
and graphics-thread assertions. Integrate collection into Window::display() after batch flush and
|
||||
into Engine shutdown before Renderer/context destruction. Reorder Engine teardown using the audited
|
||||
dependency graph. Do not generalize this substrate to self-contained GPU resource classes.
|
||||
|
||||
The old raw factory ownership remains temporarily so this internal stage cannot make resources
|
||||
disappear. Public texture APIs have not switched yet; the deferred shared-pointer deleter becomes
|
||||
active in the complete Stage 2 TexturePtr cut.
|
||||
|
||||
Exit tests:
|
||||
|
||||
- Released textures are deleted only at display/final shutdown collection points.
|
||||
- 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.
|
||||
- `EE_MEMORY_MANAGER` accurately removes texture allocations through the factory-controlled deleter.
|
||||
|
||||
### Stage 2: one complete TexturePtr ownership cut
|
||||
|
||||
Change creation/acquisition APIs to return TexturePtr and migrate every required holder in the same
|
||||
repository-wide cut. During conversion, TextureFactory temporarily retains strong handles so an
|
||||
unclassified ignored result cannot silently expire.
|
||||
|
||||
At minimum migrate:
|
||||
|
||||
- TextureLoader state, return types, callbacks, unload behavior, and async captures
|
||||
- Texture GIF frame ownership
|
||||
- TextureRegion and region-source texture ownership
|
||||
- TextureAtlas, TextureAtlasLoader, and texture vectors
|
||||
- FrameBuffer attachment ownership
|
||||
- fonts and glyph texture caches
|
||||
- nine-patches and region chains
|
||||
- SVG raster caches and UI icons
|
||||
- sprites and particle systems that retain textures/regions
|
||||
- UIImage and UINodeDrawable texture paths
|
||||
- context reload and debug texture viewer
|
||||
- eepp, ecode, modules, examples, tools, and tests
|
||||
|
||||
Every stored raw Texture pointer is classified as strong, weak, or a short borrow dominated by a
|
||||
documented owner. Add a source audit/clang-tidy check where practical.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Regions, atlases, framebuffers, fonts, glyphs, UI consumers, and async work retain dependencies.
|
||||
- No ignored factory load result is relied upon for later global lookup.
|
||||
- No public texture delete/remove API remains.
|
||||
- Live registry reload sees externally owned textures.
|
||||
- Diagnostic snapshots do not pin resources.
|
||||
- Temporary factory retention can be removed without failing ownership tests.
|
||||
|
||||
### Stage 3: catalog and scope ownership cutover
|
||||
|
||||
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.
|
||||
|
||||
Remove semantic TextureFactory name/hash lookup and migrate every lookup to a scope/catalog.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Dropping the last real consumer/catalog/cache handle expires a texture.
|
||||
- Duplicate names in unrelated scopes resolve independently.
|
||||
- Sibling/document resources are invisible without explicit catalog import.
|
||||
- Global resources persist only because the global catalog owns them.
|
||||
- Live diagnostic records cannot be resolved semantically.
|
||||
- Scope destruction does not invalidate externally retained resources.
|
||||
|
||||
### Stage 4: drawable source/instance conversion
|
||||
|
||||
Introduce source types and per-consumer instances, remove shared draw-state mutation, replace manual
|
||||
ownership with DrawablePtr, remove Unload lifetime callbacks, add RAII change connections, and
|
||||
migrate Variant's storage.
|
||||
|
||||
Convert UIImage, UINodeDrawable layers, StateListDrawable, DrawableGroup, Sprite, UIPushButton,
|
||||
UISkin, UIIcon, themes, and DrawableImageParser in one coherent cut.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Two consumers using one source have independent tint, alpha, size, position, state, and animation.
|
||||
- Nested/reentrant drawing leaves no shared source or child modified.
|
||||
- Drawable groups do not reposition shared child instances.
|
||||
- Variant copying cannot duplicate exclusive ownership.
|
||||
- No drawable `ownIt` or manual child deletion remains.
|
||||
|
||||
### Stage 5: layered UI resolution
|
||||
|
||||
Implement UI::DrawableResolver and replace DrawableSearcher. Scene resolvers delegate Graphics work
|
||||
to their explicit scope. Keep CSS/icon/glyph interpretation in UI and cookie/navigation concerns in
|
||||
Web services.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Pure Graphics works without UI.
|
||||
- UI resolves through its scene/application scope.
|
||||
- Embedded documents cannot see sibling resources accidentally.
|
||||
- Host/application assets require explicit export/import.
|
||||
|
||||
### Stage 6: WebResourceCache and document leases
|
||||
|
||||
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.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Shared tabs reuse eligible resources without sharing document ownership.
|
||||
- Navigation in one tab cannot evict another tab's active lease.
|
||||
- Different cookie/auth partitions cannot reuse credential-dependent responses.
|
||||
- Stale subscribers do not block current subscribers.
|
||||
- Same-origin retention and cross-origin lease release follow policy.
|
||||
- Cache memory remains within configured budgets.
|
||||
|
||||
### Stage 7: remaining resource families
|
||||
|
||||
Migrate fonts, font faces/fallback caches, themes, shader programs/shaders, nine-patch catalogs,
|
||||
atlas managers, and every remaining raw-owning ResourceManager subclass one family at a time. Their
|
||||
self-contained GPU objects retain the established graphics-thread destruction contract unless a
|
||||
concrete migration requires otherwise.
|
||||
|
||||
Remove raw-owning `ResourceManager<T>` only when no subclass or consumer depends on it.
|
||||
|
||||
## 11. Required validation matrix
|
||||
|
||||
### Ownership and registry
|
||||
|
||||
- Unpinned texture expires after its last real owner releases it.
|
||||
- Region/source retains its texture; atlas retains its sources/textures; framebuffer retains its
|
||||
attachment.
|
||||
- Catalog publication retains; erasing one catalog entry does not invalidate other owners.
|
||||
- Independent catalog/pin leases do not interfere.
|
||||
- Registry snapshot and UITextureViewer do not retain all resources.
|
||||
- Context reload includes resources owned only by external handles.
|
||||
- Registry creation, snapshot, expiration, and purging are race-safe.
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
- Context loss/reload includes resources owned only by external handles.
|
||||
- Repeated test-only Engine creation starts with no prior texture handles or registry state.
|
||||
- Relevant suites run under TSAN as well as ASAN/LSAN.
|
||||
|
||||
### Scope isolation
|
||||
|
||||
- Identical keys in sibling scopes resolve independently.
|
||||
- Explicit imported catalog resolves; missing import does not fall back.
|
||||
- Global catalog is visible only to scopes importing it.
|
||||
- Registry records are never semantically resolvable.
|
||||
- Scope/catalog destruction leaves externally retained resources alive.
|
||||
- Catalog import order is deterministic and graph cycles are structurally impossible.
|
||||
|
||||
### Drawable independence
|
||||
|
||||
- One source displayed by two widgets with different tint, alpha, and size.
|
||||
- Two region instances draw at different sizes without shared mutation.
|
||||
- State-list and sprite instances maintain independent state/time.
|
||||
- Nested/reentrant drawing restores nothing because shared objects were not mutated.
|
||||
- DrawableGroup owns/private-instantiates mutable children.
|
||||
- Variant copy/move/reset is safe with DrawablePtr.
|
||||
|
||||
### Web cache
|
||||
|
||||
- Same-origin navigation reuses resources.
|
||||
- Cross-origin navigation releases only the navigating document lease.
|
||||
- Two tabs share eligible entries and survive independent navigation.
|
||||
- Different cookie/auth partitions do not share.
|
||||
- URI plus different decode/request options produces distinct entries.
|
||||
- Concurrent requests coalesce while subscribers retain independent generation guards.
|
||||
- Failed requests retry according to explicit policy.
|
||||
- TTL uses a monotonic clock; byte-budget eviction works independently.
|
||||
|
||||
### Teardown and repeatable tests
|
||||
|
||||
- Pending HTTP, decode, and upload work during scene and Engine destruction.
|
||||
- No destructor recreates Engine, Renderer, TextureFactory, or another manager.
|
||||
- Repeated Engine create/destroy cycles in one process.
|
||||
- Each unit test releases its scopes, catalogs, leases, and resources independently.
|
||||
- Test teardown reports unintended catalog pins and live resource provenance.
|
||||
- `EE_MEMORY_MANAGER`, supported debug/release configurations, static builds, and shared-library
|
||||
builds.
|
||||
|
||||
## 12. First implementation deliverable
|
||||
|
||||
The next coding deliverable is Stage 0.5 prerequisite bug fixing. The Stage 0 inventories and
|
||||
shutdown dependency graph are complete and linked above. Bug fixes land with focused tests while
|
||||
preserving current public APIs and factory ownership.
|
||||
|
||||
Only after those fixes are isolated should Stage 1 add TextureFactory-specific lifetime scaffolding.
|
||||
Stage 2 then changes public texture APIs and migrates all holders in one cut.
|
||||
@@ -0,0 +1,526 @@
|
||||
# Shared-resource refactor Stage 0 inventory
|
||||
|
||||
Status: complete repository audit, lifetime contract revised 2026-07-12.
|
||||
|
||||
This document is the evidence and dependency inventory required by Stage 0 of
|
||||
`resource_shared_ownership_architecture.md`. Searches covered `include/`, `src/eepp/`, in-tree
|
||||
modules, tools, examples, ecode, and tests. Third-party implementation directories were excluded
|
||||
except where eepp invokes their APIs.
|
||||
|
||||
Confirmed defects are tracked for implementation in
|
||||
`resource_refactor_prerequisite_bugfixes.md`.
|
||||
|
||||
The inventory classifies stored relationships, side-effect loads, explicit deletion, callbacks,
|
||||
GPU object namespaces, drawable mutation, asynchronous producers, and Engine teardown dependencies.
|
||||
Line numbers will drift; paths and symbols are the stable references.
|
||||
|
||||
## 1. Stage 0 conclusions
|
||||
|
||||
The architecture contracts can proceed with these refinements:
|
||||
|
||||
1. eepp retains its existing graphics-thread-affine project contract. Shared ownership does not
|
||||
promise arbitrary-thread final destruction. Texture is deferred through TextureFactory and
|
||||
collected from `Window::display()` after batch flush; other self-contained GPU objects retain
|
||||
their direct graphics-thread destruction model.
|
||||
2. Every queued renderer submission owns its dependencies. `BatchRenderer` currently stores a raw
|
||||
texture between `setTexture()` and a later `flush()`; the migrated batch must retain a
|
||||
`TexturePtr` until flushing or discarding its vertices.
|
||||
3. Text layout is an owner/cache boundary. The global `TextLayout` LRU stores layouts containing
|
||||
`ShapedGlyph::font` raw pointers. In the final design layouts retain the required `FontPtr`
|
||||
values; the bounded global LRU is then an intentional cache owner and is explicitly clearable for
|
||||
test isolation.
|
||||
4. Resource-producing work must have an explicit cancellation/lifetime owner independently of an
|
||||
arbitrary shared `ThreadPool`. UISceneNode pools can be shared with a host or application and
|
||||
cannot simply be destroyed by Engine. WebResourceCache and the owning scene/document services
|
||||
track their own operations and subscribers; no generic Graphics ResourceSystem is required.
|
||||
5. Existing shutdown functions cannot only be reordered:
|
||||
- `Http::Pool::clear()` clears clients while holding the pool mutex; each client joins request
|
||||
callbacks, so a callback re-entering the global pool can deadlock.
|
||||
- When `Http::setThreadPool()` is active, queued lambdas capture raw `Http*`. `Http::~Http()`
|
||||
cancels requests but only joins privately created request threads; it does not join shared-pool
|
||||
work. Clearing the Pool can therefore destroy Http while a shared-pool task still uses it.
|
||||
- `TextureAtlasLoader` declares `ResourceLoader mRL` before the state used by its callbacks.
|
||||
Members are destroyed in reverse declaration order, so the loader is destroyed last and can
|
||||
run against already-destroyed state.
|
||||
These are prerequisite bugs and should be fixed independently before the ownership refactor.
|
||||
6. `isStateful()` is unusable as a shareability test. Every current Drawable inherits mutable color
|
||||
and position, and several classes reporting false mutate themselves or children during draw.
|
||||
7. Stage 2 must migrate texture holders, loaders, ID-based construction, and queued batches in one
|
||||
cut. Factory-wide temporary retention may then be removed only in Stage 3 after catalogs/scopes
|
||||
replace global semantic lookup.
|
||||
|
||||
No unresolved architectural choice remains in Stage 0. The confirmed defects are tracked as Stage
|
||||
0.5 bug fixes before texture lifetime scaffolding begins.
|
||||
|
||||
## 2. GPU and device-affinity inventory
|
||||
|
||||
### 2.1 Context topology
|
||||
|
||||
`Engine` owns a map of `Window*` and tracks one current window. SDL2 and SDL3 windows each own a
|
||||
primary GL context and may own a second worker context when `SharedGLContext` is enabled:
|
||||
|
||||
- `src/eepp/window/engine.cpp`: `createWindow()`, `setCurrentWindow()`, `mWindows`.
|
||||
- `src/eepp/window/backend/SDL2/windowsdl2.cpp`: `mGLContext`, `mGLContextThread`,
|
||||
`setGLContextThread()`.
|
||||
- `src/eepp/window/backend/SDL3/windowsdl3.cpp`: equivalent context pair.
|
||||
- `Texture` and `TextureLoader` currently acquire the current window's worker context directly.
|
||||
|
||||
The refactor needs a process-unique `ResourceId` for diagnostics. It does not introduce public
|
||||
device epochs or share-group disposal identities. Existing context-current/shared-worker rules
|
||||
remain authoritative. Tests may destroy and recreate the singleton Engine, but they must first
|
||||
release all GPU resource handles and verify that factory/manager state is empty.
|
||||
|
||||
### 2.2 Device-affine object table
|
||||
|
||||
| Object/payload | Current creation and mutation | Current destruction | Current tracker/owner | Required migration |
|
||||
|---|---|---|---|---|
|
||||
| Texture GL handle | SOIL and texture upload paths in `texture.cpp` and `textureloader.cpp`; lock, unlock, replace, resize, reload and filter operations issue GL | `Texture::~Texture()` calls `glDeleteTextures` and changes worker context | TextureFactory owns raw Texture and binding/memory state | Final TexturePtr release queues the Texture in TextureFactory; `Window::display()` flushes batches then collects it under the current context; shutdown performs a final collection |
|
||||
| Temporary readback FBO | GLES `Texture::iLock()` creates, attaches, reads and deletes a framebuffer | Deleted synchronously inside `iLock()` | Stack-local handle | Keep as a device-thread scoped command; never execute from arbitrary last-release thread |
|
||||
| FrameBuffer FBO | `FrameBufferFBO::create()/resize()/reload()` creates framebuffer and depth/stencil/color renderbuffers | `FrameBufferFBO::~FrameBufferFBO()` directly deletes renderbuffers/FBO and may unbind | FrameBuffer self-registers in non-owning FrameBufferManager; SceneNode/UIWindow/TerminalDisplay own raw objects | Preserve graphics-thread destruction; migrate attachment ownership without a generic GPU disposal layer |
|
||||
| FrameBuffer texture attachment | `FrameBufferFBO::create()` asks TextureFactory for empty texture | `FrameBuffer::~FrameBuffer()` directly deletes Texture | FrameBuffer exclusive raw ownership, factory also believes it owns the same texture | FrameBuffer stores TexturePtr; no direct deletion; factory registry remains weak |
|
||||
| VBO/EBO/VAO | `VertexBufferVBO` creates/updates buffers and VAO | `VertexBufferVBO::clear()` directly deletes buffers/VAO; destructor calls clear | VertexBuffer self-registers in non-owning VertexBufferManager; consumer owns raw object | Preserve graphics-thread destruction; owning consumer eventually uses a handle or value owner |
|
||||
| Renderer streaming VBO/VAO | `RendererGL3CP` owns eight VBOs and one VAO | `RendererGL3CP::~RendererGL3CP()` directly deletes them | Renderer | Preserve Renderer-owned direct destruction before context teardown |
|
||||
| Shader object | `Shader::Init()/reload()` calls `GLi->createShader`, compiles source | `Shader::~Shader()` directly calls `GLi->deleteShader` | ShaderProgram manually owns raw Shader children | Preserve graphics-thread destruction; ShaderProgram eventually owns ShaderPtr/source handles |
|
||||
| Program object | `ShaderProgram::init()/reload()` creates and links program | Destructor directly calls `GLi->deleteProgram`, deletes Shader children, self-removes from manager | ShaderProgramManager raw-owns programs; Renderer stores raw default/current program pointers | Preserve graphics-thread destruction and ensure manager precedes Renderer; ownership becomes explicit later |
|
||||
| Primitive/UI geometry buffers | PrimitiveDrawable, UIBackgroundDrawable and UIBorderDrawable create VertexBuffer objects | Their destructors directly delete VertexBuffer | Per-drawable exclusive raw ownership | Per-consumer drawable owns VertexBuffer handle under the graphics-thread contract |
|
||||
| Terminal geometry/FBO | TerminalDisplay owns FrameBuffer, background/foreground VBs and style VB vector | Explicit delete/recreate paths | TerminalDisplay | Strong handles; release before device gate closes |
|
||||
|
||||
Direct deletion sites found by the audit:
|
||||
|
||||
- `src/eepp/graphics/texture.cpp`: texture and temporary framebuffer deletion.
|
||||
- `src/eepp/graphics/framebufferfbo.cpp`: framebuffer/renderbuffer deletion.
|
||||
- `src/eepp/graphics/vertexbuffervbo.cpp`: buffer and vertex-array deletion.
|
||||
- `src/eepp/graphics/renderer/renderergl3cp.cpp`: renderer VBO/VAO deletion.
|
||||
- `src/eepp/graphics/shader.cpp`: shader deletion.
|
||||
- `src/eepp/graphics/shaderprogram.cpp`: program deletion.
|
||||
|
||||
Renderer wrapper implementations in `src/eepp/graphics/renderer/renderer.cpp` expose the GL delete
|
||||
entry points; they are dispatch, not independent owners.
|
||||
|
||||
### 2.3 Non-owning GPU registries and consumers
|
||||
|
||||
- `FrameBufferManager : Container<FrameBuffer>` and `VertexBufferManager : Container<VertexBuffer>`
|
||||
observe raw self-registering objects and do not delete them.
|
||||
- FrameBuffer owners: SceneNode, UIWindow, TerminalDisplay, and direct application/test callers.
|
||||
- VertexBuffer owners: PrimitiveDrawable, UIBackgroundDrawable, UIBorderDrawable, TerminalDisplay,
|
||||
renderer internals, and direct application/test callers.
|
||||
- Renderer shader arrays (`RendererGL3`, `RendererGL3CP`, `RendererGLES2`) are raw views of programs
|
||||
currently owned by ShaderProgramManager.
|
||||
- `GlobalBatchRenderer` is CPU storage but retains a borrowed Texture pointer until a later flush.
|
||||
It is a real lifetime owner in the new model whenever `mNumVertex != 0`.
|
||||
|
||||
### 2.4 Device-operation rule
|
||||
|
||||
Creation, upload, mutation, context reload, final ownership release and deletion are graphics-
|
||||
affine. Async CPU decode may run elsewhere; GPU work uses the main context or an API that explicitly
|
||||
acquires the existing shared worker context. The refactor does not broaden this contract. Debug
|
||||
assertions and tests should detect wrong-thread release rather than adding generic device scheduling.
|
||||
|
||||
## 3. Texture ownership and lookup inventory
|
||||
|
||||
### 3.1 Persistent raw texture holders
|
||||
|
||||
| Holder | Field/API | Current lifetime assumption | Target classification |
|
||||
|---|---|---|---|
|
||||
| TextureFactory | `mTextures: id -> Texture*` | Sole global owner, semantic lookup, diagnostics and deletion | Weak LiveResourceRegistry records; no semantic lookup or ownership |
|
||||
| TextureLoader | `mTexture`, `getTexture()`, `OnTextureLoaded(Uint32, Texture*)` | Factory owns after loader returns | TexturePtr result/state/callback; operation owns during load |
|
||||
| TextureRegion | `mTexture`, ID constructors and `setTextureId()` | Factory keeps texture alive | Immutable region source stores TexturePtr; per-consumer region drawable stores source |
|
||||
| TextureAtlas | `mTextures` and ResourceManager-owned TextureRegion children | Factory owns textures; atlas owns regions | Atlas/source catalog owns TexturePtr and region-source handles |
|
||||
| TextureAtlasLoader | `mTexturesLoaded` plus ignored queued loads | Relies on factory side effects and later global name lookup | Operation retains TexturePtr results directly and passes them into atlas construction |
|
||||
| FrameBuffer | `mTexture` | FrameBuffer deletes attachment although factory also registers it | FrameBuffer stores TexturePtr |
|
||||
| FontTrueType::Page | `texture` and raw GlyphDrawable cache | Page explicitly removes texture from factory | Page stores TexturePtr; glyph source records retain page texture |
|
||||
| FontBMFont::Page | `texture` and raw GlyphDrawable cache | Same | Same |
|
||||
| FontSprite::Page | `texture` and raw GlyphDrawable cache | Same | Same |
|
||||
| GlyphDrawable | `mTexture` | Font page/factory assumed to outlive glyph | GlyphSource stores TexturePtr; render state is external/per consumer |
|
||||
| UISVGIcon | `mSVGs: size -> Texture*` | Factory owns raster cache | Icon/source cache stores TexturePtr with explicit cache policy |
|
||||
| ParticleSystem | `const Texture* mTexture` resolved by ID | Factory owns | ParticleSystem stores TexturePtr or immutable texture-source handle |
|
||||
| maps::TileMap | `mTileTex` | Factory owns generated/named blank-tile texture | TileMap stores TexturePtr |
|
||||
| BatchRenderer | `const Texture* mTexture` | Caller/resource survives until deferred flush | Strong TexturePtr while queued; reset on flush/discard |
|
||||
| Tests/test harness | vectors, arrays and locals in `src/tests/test_all` and unit tests | Factory teardown cleans up | Test-local handles/catalog fixtures |
|
||||
|
||||
Cursor APIs accept a Texture pointer but immediately lock and copy pixels into an Image. They are
|
||||
synchronous borrowed parameters, not persistent texture holders. They should accept `const
|
||||
TexturePtr&` or a documented borrowed `Texture&` depending on the final lock API.
|
||||
|
||||
UIColorPicker texture-returning helpers, image viewer, diff view, examples, and tool code mostly
|
||||
return/use local pointers but must receive TexturePtr because the result crosses a call boundary.
|
||||
|
||||
### 3.2 Texture-to-region-to-drawable chains
|
||||
|
||||
Raw texture lifetime is also hidden behind raw TextureRegion relationships:
|
||||
|
||||
- Sprite frame vectors store `TextureRegion*`, copy them shallowly, mutate frame region size/offset,
|
||||
and optionally delete regions/textures according to sprite flags.
|
||||
- NinePatch exclusively deletes nine generated TextureRegion children, each borrowing one Texture.
|
||||
- TextureAtlas raw-owns regions through ResourceManager.
|
||||
- GlobalTextureAtlas owns regions created by ID-based Sprite paths.
|
||||
- ScrollParallax, UITextureRegion, UISprite, maps GameObjectVirtual/GameObjectTextureRegion, map
|
||||
editor state, and UI editor image maps retain TextureRegion pointers.
|
||||
- TextureAtlasManager returns raw regions and vectors by name/pattern to Sprite and search callers.
|
||||
|
||||
Stage 2 therefore removes texture-ID construction and migrates all these region edges. Region IDs
|
||||
remain stable metadata if useful; they are not a lifetime acquisition mechanism.
|
||||
|
||||
### 3.3 Global semantic lookup sites
|
||||
|
||||
TextureFactory semantic lookup currently serves unrelated scopes:
|
||||
|
||||
- DrawableSearcher searches TextureAtlasManager, NinePatchManager and TextureFactory globally.
|
||||
- TextureAtlasLoader locates queued results by path after loading.
|
||||
- UIImage and UINodeDrawable use URL/path names as global cache keys.
|
||||
- Sprite, TextureRegion, NinePatch and ParticleSystem resolve numeric texture IDs.
|
||||
- Font page destructors remove by texture ID.
|
||||
- maps::TileMap resolves a generated blank-tile name.
|
||||
- ecode settings and uieditor resolve/remove application textures globally.
|
||||
- Tests rely on `getByName()`, `getTexture()`, and `getTextures()`.
|
||||
|
||||
All semantic name/path/URL acquisition moves to ResourceCatalog/ResourceScope. Numeric ResourceId
|
||||
lookup in LiveResourceRegistry is diagnostic/administrative and returns a weak handle; it is not a
|
||||
substitute for a catalog.
|
||||
|
||||
### 3.4 Loads relying on global side effects
|
||||
|
||||
Confirmed ignored or indirect load results:
|
||||
|
||||
- `TextureAtlasLoader` queued `loadFromPack()` and `loadFromFile()` calls; later lookup by path.
|
||||
- `src/tests/test_all/test.cpp` queues/discards pack loads and later resolves globally.
|
||||
- Theme directory loading directly embeds load results into new TextureRegion/NinePatch/Sprite
|
||||
graphs; those destination objects must retain handles in the same cut.
|
||||
- Font pages assign factory results to raw page fields and explicitly remove them later.
|
||||
- `Texture::loadGif()` returns a vector of raw frames; Sprite assumes ownership flags/global factory.
|
||||
|
||||
Other creation paths return a local pointer and immediately pass it to a current raw holder:
|
||||
|
||||
- FrameBufferFBO attachment creation.
|
||||
- UIImage/UINodeDrawable remote placeholders.
|
||||
- UISVGIcon and UISVG rasterization.
|
||||
- FontTrueType, FontBMFont and FontSprite page creation.
|
||||
- DrawableSearcher file/data/HTTP paths.
|
||||
- UIColorPicker, UIImageViewer, UIDiffView, uieditor and sprite examples.
|
||||
|
||||
The Stage 2 compile cut changes every one to retain or propagate TexturePtr. The temporary factory
|
||||
retention map is removed only after an audit asserts no ignored result is semantically required.
|
||||
|
||||
### 3.5 Explicit deletion and unload semantics
|
||||
|
||||
Current deletion paths that must disappear:
|
||||
|
||||
- `TextureFactory::remove(id)`, `remove(Texture*)`, `unloadTextures()` and `removeReference()`.
|
||||
- `TextureLoader::unload()`.
|
||||
- FontTrueType/FontBMFont/FontSprite Page destructors removing texture IDs.
|
||||
- FrameBuffer directly deleting its attachment.
|
||||
- uieditor explicitly removing textures loaded for its image map.
|
||||
- Sprite cleanup flags that can delete factory textures/regions.
|
||||
|
||||
Final equivalents are handle reset, catalog erase, cache eviction, and operation cancellation. None
|
||||
invalidates another consumer's resource.
|
||||
|
||||
### 3.6 Texture callbacks and global state
|
||||
|
||||
- TextureLoader has a process-static callback map with `Texture*` payloads. It is not synchronized,
|
||||
is not reset with Engine, and UITextureViewer is its only current subscriber.
|
||||
- DrawableResource Change/Unload callbacks use raw resource pointers and integer IDs.
|
||||
- UITextureViewer stores Texture pointer -> callback ID and expects Unload to remove rows.
|
||||
- TextureFactory memory accounting and Texture mutation call each other through the singleton.
|
||||
- TextureFactory performs reload/grab/ungrab while holding its registry lock and invokes texture/GL
|
||||
operations under that lock.
|
||||
|
||||
Target:
|
||||
|
||||
- LiveResourceRegistry emits diagnostic record changes or supplies snapshots; viewer uses weak
|
||||
handles.
|
||||
- Source mutation signals use RAII connections and weak subscriber tokens.
|
||||
- ResourceMetrics is captured state, independent of factory lifetime.
|
||||
- Registry locks protect records only; device work and callbacks occur after unlocking.
|
||||
|
||||
## 4. Drawable mutation and ownership inventory
|
||||
|
||||
### 4.1 Base-class result
|
||||
|
||||
`Drawable` itself stores mutable `mColor` and `mPosition`, and exposes setColor, setAlpha and
|
||||
setPosition. Consequently no existing subclass is generally shareable merely because its
|
||||
`isStateful()` returns false.
|
||||
|
||||
The target source/instance split remains valid, with one performance qualification: high-frequency
|
||||
glyph and text rendering should use immutable glyph sources plus external draw parameters instead of
|
||||
allocating a mutable drawable instance per rendered glyph.
|
||||
|
||||
### 4.2 Class classification
|
||||
|
||||
| Current class/family | Current mutation/ownership behavior | Target form |
|
||||
|---|---|---|
|
||||
| Texture | Mutable color/position from Drawable plus mutable GPU data, filters, clamp, local cache and name | Shared Texture resource only; drawing uses TextureDrawable instance or external draw params |
|
||||
| TextureRegion | Mutates destination size inside `draw(position,size)`; mutable source rect, offset, pixel cache, color/position | Immutable TextureRegionSource retaining TexturePtr; TextureRegionDrawable instance/presentation state |
|
||||
| GlyphDrawable | Cached and shared by font pages but has color, position, draw mode, italic flag, offset, size and advance; UICodeEditor temporarily changes draw mode | Immutable GlyphSource; text/editor pass draw mode, color, position and size as parameters |
|
||||
| NinePatch | Owns nine mutable regions; draw updates own size/position and every child; propagates color/alpha | Immutable NinePatchSource plus private per-consumer NinePatchDrawable layout state |
|
||||
| Sprite | Animation state, callbacks, transforms, current frame, repetitions and shallow-copied region vectors; mutates region size/offset | Per-consumer Sprite instance retaining immutable frame sources |
|
||||
| PrimitiveDrawable and Rectangle/Triangle/Arc/Circle/ConvexShape | Mutable geometry, color, position, fill/blend/line state and owned VertexBuffer cache | Per-consumer drawable instance; optional immutable geometry source only if later useful |
|
||||
| Linear/RadialGradientDrawable | Mutable stops, angle/shape/center/extent, size, color and position | Parsed immutable gradient source plus per-consumer instance, or fresh instance directly from CSS parser |
|
||||
| DrawableGroup | Optional global child-owner bool; draw/update mutates own size/position and child positions/alpha | Per-consumer composite owning private mutable child instances/source handles |
|
||||
| StateListDrawable | Raw state map plus pointer->bool ownership; mutable current state; draw temporarily changes child alpha; state color mutates child | Per-consumer state machine owning instances or source factories; no child mutation shared with another list |
|
||||
| UISkin | StateListDrawable; `clone()` shallow-copies pointers and ownership map, duplicating ownership claims | Skin source/definition in theme catalog; create independent skin/state-list instances |
|
||||
| RichText | Mutable layout/selection; stores raw inline/background/border drawables and temporarily recolors backgrounds during draw | Per-consumer RichText; retained source/instance handles; external color draw parameters |
|
||||
| UINodeDrawable | Node-owned layer map, geometry/cache state and nested background drawable | Per-node/per-consumer instance |
|
||||
| UINodeDrawable::LayerDrawable | Manual `mOwnsDrawable`; mutable repeat/clip/origin/size/offset; draw mutates child alpha/color; async placeholder | Per-node layer owning DrawablePtr instance created by UI::DrawableResolver |
|
||||
| UIBackgroundDrawable/UIBorderDrawable | Owner-node pointer, mutable geometry/radii/colors/position/size, owned VertexBuffer | Per-node instance; VertexBuffer handle |
|
||||
| DrawableResource/StatefulDrawable | Name/ID plus Change/Unload callback lifetime protocol | Source identity plus typed invalidation signal; no destructor Unload callbacks |
|
||||
|
||||
### 4.3 Other drawable holders requiring migration
|
||||
|
||||
- UIImage: raw drawable plus `mDrawableOwner`; temporarily recolors it during draw.
|
||||
- UIPushButton icon delegates the same ownership flag to UIImage.
|
||||
- UINode background/foreground APIs expose `ownIt`.
|
||||
- DrawableImageParser returns `Drawable*` plus `bool& ownIt` for gradients, shapes, URLs and icons.
|
||||
- UIIcon stores size -> Drawable raw pointers; UIGlyphIcon borrows FontTrueType and font-owned glyph
|
||||
drawables; UISVGIcon separately caches textures.
|
||||
- UITheme owns UISkin objects through ResourceManagerMulti; UIIconTheme manually owns UIIcon
|
||||
objects; skins point into global atlas/nine-patch resources.
|
||||
- Models::Variant stores Drawable in a C-style union and copies both pointer and owner flag.
|
||||
- RichText inline boxes/fragments store backgroundColorDrawable/backgroundDrawable/borderDrawable
|
||||
raw pointers.
|
||||
- UICodeEditor stores fold/unfold drawables and temporarily changes GlyphDrawable draw mode.
|
||||
- ClippingMask stores temporary borrowed Drawable pointers and calls draw later; its operation must
|
||||
remain bounded by the owner or retain instance handles while queued.
|
||||
- ecode/tool/plugin configuration and tab splitter structures store icon Drawable pointers.
|
||||
|
||||
### 4.4 Complete manual ownership-flag surface
|
||||
|
||||
The audit found ownership flags in:
|
||||
|
||||
- DrawableGroup (`mDrawableOwner`, `setDrawableOwner()`).
|
||||
- StateListDrawable (`mDrawablesOwnership`, per-state `ownIt`).
|
||||
- UISkin clone copying StateListDrawable ownership state.
|
||||
- UIImage (`mDrawableOwner`, `safeDeleteDrawable()`, `setDrawable(..., ownIt)`).
|
||||
- UINodeDrawable::LayerDrawable (`mOwnsDrawable`).
|
||||
- UINode background/foreground APIs.
|
||||
- UIPushButton icon API.
|
||||
- DrawableImageParser function and return protocol.
|
||||
- Models::Variant (`mOwnsObject` for Drawable).
|
||||
|
||||
All are removed in Stage 4. No compatibility overload remains.
|
||||
|
||||
## 5. Asynchronous producer inventory
|
||||
|
||||
| Producer | Work and current captures | Current stop behavior | Required contract |
|
||||
|---|---|---|---|
|
||||
| Http global Pool/Http AsyncRequest | UIWebView documents, UIImage/UINodeDrawable placeholders, DrawableSearcher, font faces; callbacks can mutate scenes/textures or queue main-thread work | Pool clear erases shared clients under mutex; Http destructor joins private request threads, but optional global-ThreadPool tasks capture raw Http and are not joined | Pool/service close rejects requests, swaps clients out under lock, unlocks, then cancels/joins all tracked operations regardless of executor; operation subscribers use weak session tokens |
|
||||
| UISceneNode ThreadPool | File texture load/upload, SVG/image decode, deferred fonts/styles; pool may be shared with host/application | Scene invalidates generation and deletes children; shared pool may outlive scene; ThreadPool destructor drains queued work by default | Owning scene/document service tracks operations, captures no raw scene, and is joinable; Engine does not destroy arbitrary external pools |
|
||||
| Static UISceneNode async-main queue | Lambdas queued by HTTP/thread workers with resource state/generation | Drained only during UISceneNode scheduled update; no Engine clear | UI-owned delivery queue has close/reject/invalidate/purge semantics before scene destruction |
|
||||
| TextureLoader | Decode and optional direct GL upload on calling/worker thread; static global callbacks | Stack loader; no service-level shutdown; callbacks unsynchronized | TextureLoadOperation owns TexturePtr/result; GPU upload follows explicit current/shared-context rules; typed observers are synchronized |
|
||||
| ResourceLoader | Internal Thread plus temporary ThreadPool drains all tasks before destructor returns | Destructor waits, but cannot cancel running work | Close/cancel token and join; callbacks never use destructed owner state |
|
||||
| TextureAtlasLoader | ResourceLoader tasks call factory, completion callback accesses loader and managers | ResourceLoader member is destroyed last due declaration order | Loader operation/state shared independently; join before state destruction; retain texture results directly |
|
||||
| UIWebView navigation | HTTP callbacks use weak NavigationLoadState and generation, then main-thread document replacement | Good stale-delivery guard, but request is global and cache-unaware | Per-document subscriber/session over shared request; stale tab does not cancel other subscribers |
|
||||
| UIImage/UINodeDrawable remote paths | Capture raw placeholder Texture and raw `this`, partially guarded by alive atomics/generation | Callback may still release/mutate on HTTP thread; factory owns placeholder | Capture TexturePtr and weak consumer token; decode off-thread, upload/device mutation scheduled, scene update generation-guarded |
|
||||
| UISVG and image tools | Shared scene pool tasks often capture raw `this` and later runOnMainThread | Per-widget tags/alive handling varies | Convert resource-producing paths to operation/subscriber tokens; application-only CPU tasks remain app responsibility |
|
||||
|
||||
ThreadPool itself waits for all queued work unless `terminateOnClose` is set. That behavior is useful
|
||||
but is not a global shutdown mechanism because ownership is distributed. Each UI/Web/cache service
|
||||
tracks operations that access its state even when execution uses a provided shared pool.
|
||||
|
||||
## 6. Current Engine teardown dependency audit
|
||||
|
||||
Current order in `Engine::~Engine()`:
|
||||
|
||||
```text
|
||||
GlobalBatchRenderer
|
||||
NinePatchManager
|
||||
SceneManager
|
||||
StyleSheetSpecification / SyntaxDefinitionManager
|
||||
FontManager
|
||||
TextureAtlasManager
|
||||
TextureFactory
|
||||
Renderer
|
||||
ShaderProgramManager
|
||||
PackManager
|
||||
FrameBufferManager / VertexBufferManager
|
||||
VFS
|
||||
SSL end
|
||||
HTTP Pool clear
|
||||
Windows / contexts
|
||||
backend and process caches
|
||||
TextLayout cache
|
||||
SystemFontResolver
|
||||
```
|
||||
|
||||
Concrete violations:
|
||||
|
||||
| Current edge/order | Violation |
|
||||
|---|---|
|
||||
| NinePatchManager before SceneManager | Scenes/widgets can still hold raw global nine-patch/region pointers; relies on Unload callbacks during teardown |
|
||||
| GlobalBatchRenderer destroyed first | Pending submissions are discarded without an explicit dependency release contract; future strong queued texture handles need explicit discard |
|
||||
| TextLayout cache after FontManager | Cached ShapedGlyph objects contain raw FontTrueType pointers after fonts are deleted |
|
||||
| Renderer before ShaderProgramManager | Renderer destruction sets global `GLi = nullptr`; ShaderProgram and Shader destructors then call through GLi |
|
||||
| TextureFactory before external FrameBuffer/VBO/program handles | External resources can outlive managers and their destructors can recreate manager/factory singletons or touch dead GL state |
|
||||
| FrameBufferManager/VertexBufferManager after Renderer | Managers are non-owning today, but any live registered object's deletion needs Renderer/context; order provides no guarantee |
|
||||
| HTTP Pool after all Graphics resources | Callbacks can mutate placeholders, create resources, enqueue scene work, or release final handles after consumers/device systems are gone |
|
||||
| HTTP Pool clear under its own mutex | Http destruction joins callbacks; callback re-entry to global Pool can deadlock |
|
||||
| HTTP using `sGlobalThreadPool` | Queued task captures raw Http; Pool clear can delete Http because its destructor cannot join externally executed work |
|
||||
| Windows last without per-context drain | Direct deletion happens against whichever context happens to be current, not necessarily the object's namespace |
|
||||
| Log before late static cache cleanup | Future abandoned-resource diagnostics would lose logging if any resource/cache remains |
|
||||
|
||||
## 7. Target shutdown dependency graph
|
||||
|
||||
### 7.1 Dependency graph
|
||||
|
||||
```text
|
||||
HTTP / decode / atlas / scene-pool executors
|
||||
│ produce
|
||||
▼
|
||||
Owning UI/document operation state + WebResourceCache requests
|
||||
│ deliver through generation/session tokens
|
||||
▼
|
||||
Scenes / UI documents / drawable instances / app caches
|
||||
│ retain
|
||||
├──────────────► Fonts ─► glyph sources ─► Textures
|
||||
├──────────────► Atlases ─► region sources ─► Textures
|
||||
├──────────────► Nine-patch/sprite sources ─► region sources
|
||||
├──────────────► FrameBuffers ─► attachment Textures
|
||||
└──────────────► VertexBuffers / queued batches
|
||||
|
||||
Global/default catalogs ───────────► any published resource/source
|
||||
TextLayout LRU ────────────────────► Fonts used by cached shaped runs
|
||||
Renderer ──────────────────────────► default Programs + streaming VBO/VAO
|
||||
|
||||
TexturePtr final release ─► TextureFactory released queue ─► Window::display collection
|
||||
Other GPU objects ────────────────────────────────────────► graphics-thread destruction
|
||||
Both paths ───────────────────────────────► Renderer/GL dispatch ─► Window context
|
||||
```
|
||||
|
||||
An arrow means the left side must stop producing or release its dependency before the right side is
|
||||
detached/destroyed. Shared handles make sibling release order less fragile, but graphics-thread and
|
||||
context order remain strict.
|
||||
|
||||
### 7.2 Concrete Engine shutdown sequence
|
||||
|
||||
1. **Enter shutdown.** Mark Engine, WebResourceCache and resource delivery queues as closing. Reject
|
||||
new resource, cache, HTTP-for-resource, upload, reload and main-thread delivery operations.
|
||||
2. **Invalidate subscribers.** Invalidate all document sessions, UISceneNode async generations,
|
||||
widget subscribers, navigation states, and cache leases. UI objects still exist, so cancellation
|
||||
callbacks that must observe them can do so through checked weak tokens.
|
||||
3. **Stop producers without holding service locks.** Swap global HTTP clients/request sets and
|
||||
tracked operations into local containers under their locks; unlock; cancel and join them. Close
|
||||
and join TextureAtlas/Texture load operations and Web cache fetch/decode operations. A provided
|
||||
external ThreadPool remains alive, but no tracked task may still access Engine/UI resource state
|
||||
after this barrier.
|
||||
4. **Purge delivery queues.** Remove pending UIScene/resource main-thread deliveries and release
|
||||
their captured handles. No queue can accept new entries after step 1.
|
||||
5. **Stop rendering submissions.** Discard pending GlobalBatchRenderer vertices and release its
|
||||
TexturePtr; ensure no window/scene draw is active. Do not attempt a cosmetic final render.
|
||||
6. **Destroy scenes/documents.** Destroy SceneManager and all child UISceneNodes/widgets. This
|
||||
releases document scopes, UI resolvers, UI themes/icons/skins, drawables, scene/app cache leases,
|
||||
SceneNode/UIWindow/Terminal framebuffers, primitive/UI/terminal vertex buffers, and scene-owned
|
||||
fonts. Scenes precede global source/catalog teardown.
|
||||
7. **Clear CPU caches retaining resources.** Clear TextLayout LRU and any font/glyph/drawable lookup
|
||||
caches. Clear application and global ResourceCatalogs/default scopes. Destroy StyleSheet and
|
||||
syntax/UI resolver specification state after scenes no longer use it.
|
||||
8. **Release high-level Graphics owners.** Release NinePatch, atlas/region, font/glyph and remaining
|
||||
theme/icon manager/catalog handles in dependency order. Clear font fallback/style links before
|
||||
releasing fonts. TextureFactory/LiveResourceRegistry remain present as weak observers only.
|
||||
9. **Collect released textures.** Make the active context current, flush/discard pending batch
|
||||
submissions, and call `TextureFactory::collectReleasedTextures()`. Purge expired weak records and
|
||||
report/assert any unexpected externally owned TexturePtr. Tests treat every survivor as a fixture
|
||||
teardown failure; a defensive GPU-payload release may make production shutdown safe to continue.
|
||||
10. **Release renderer-owned resources.** Destroy ShaderProgramManager and framebuffer/vertex
|
||||
owners before Renderer. Renderer then releases its default programs and streaming VBO/VAO while
|
||||
the context is valid. Assert TextureFactory has no pending released objects before it is destroyed.
|
||||
11. **Destroy Graphics roots.** Destroy TextureFactory's weak registry and CPU state, Renderer/GL
|
||||
dispatch, and non-owning manager shells. A TexturePtr surviving this point violates the project
|
||||
contract; it is not supported through a second device lifetime.
|
||||
12. **Destroy loading infrastructure.** Destroy PackManager and VFS after all tracked resource work
|
||||
and reload-capable internal owners are gone. End SSL after HTTP clients have joined.
|
||||
13. **Destroy windows and backend.** Destroy each worker/primary GL context and window, then platform,
|
||||
display and backend state.
|
||||
14. **Destroy process caches and logging last.** Clear SystemFontResolver/FreeType resolver state,
|
||||
parser/regex caches, and finally Log. MemoryManager reporting happens after test/application
|
||||
handles and catalog fixtures have been released.
|
||||
|
||||
### 7.3 Window destruction outside Engine teardown
|
||||
|
||||
`Engine::destroyWindow()` can remove one context while Engine and other windows continue. It needs
|
||||
the same per-context mini-sequence:
|
||||
|
||||
1. Reject new operations targeting that window/context.
|
||||
2. Cancel/join its tracked uploads and release window/scene-owned resources.
|
||||
3. Flush pending submissions and collect textures releasable under that current context.
|
||||
4. Release renderer/context-local payloads.
|
||||
5. Destroy the contexts/window.
|
||||
|
||||
The existing multi-window/shared-context contract determines which texture objects are valid under
|
||||
the remaining contexts. This refactor does not introduce a second context ownership model.
|
||||
|
||||
## 8. Test-isolation and build matrix decision
|
||||
|
||||
### 8.1 Required build configurations
|
||||
|
||||
The repository enables `EE_MEMORY_MANAGER` in debug configurations and builds both eepp static and
|
||||
shared libraries. Stage 1/2 validation therefore requires at least:
|
||||
|
||||
| Configuration | Purpose |
|
||||
|---|---|
|
||||
| Linux debug static, EE_MEMORY_MANAGER | Primary unit-test and tracked deleter correctness |
|
||||
| Linux debug shared, EE_MEMORY_MANAGER | ResourcePtr/custom-deleter behavior across library boundary |
|
||||
| Linux release static | Behavior without MemoryManager macros and optimized lifetime paths |
|
||||
| Linux release shared | Public handle ABI and cross-DSO destruction without debug tracking |
|
||||
| Debug static + AddressSanitizer/LeakSanitizer | UAF, double control block, leaks and late callbacks |
|
||||
| Debug static + ThreadSanitizer | Registry/cache/async operation races and contract violations |
|
||||
|
||||
Existing Premake options include `with-static-eepp`, `address-sanitizer`, and `thread-sanitizer`.
|
||||
Platform CI can expand after Linux substrate tests are stable; Windows/macOS context implementations
|
||||
must pass deferred texture collection and teardown-order tests before the public refactor is complete.
|
||||
|
||||
### 8.2 Per-test fixture contract
|
||||
|
||||
Each resource test owns an explicit fixture containing Engine/window if needed, catalogs/scopes, Web
|
||||
cache/session objects, and resource handles. Teardown order is:
|
||||
|
||||
1. invalidate subscribers and stop tracked operations;
|
||||
2. release test widgets/scenes/caches/catalogs/handles;
|
||||
3. flush batches, collect released textures and destroy Engine;
|
||||
4. purge expired registry records and static callbacks/caches;
|
||||
5. assert no unintended catalog entries, operation records, pending deliveries, released textures,
|
||||
or live texture registry entries;
|
||||
6. assert no resource handle survives Engine destruction;
|
||||
7. compare MemoryManager/registry diagnostics to fixture baseline.
|
||||
|
||||
No test may depend on a previous test's global TextureFactory name entry, TextureLoader callback,
|
||||
TextLayout cache, font fallback cache, or Engine ID counter reset.
|
||||
|
||||
## 9. Source-audit gates
|
||||
|
||||
Stage 2 and Stage 4 should preserve repeatable repository checks. The exact implementation may use
|
||||
clang-tidy, but these searches define the initial gates:
|
||||
|
||||
```sh
|
||||
rg '\b(?:const\s+)?Texture\s*\*' include src --glob '!src/thirdparty/**'
|
||||
rg 'TextureFactory::instance\(\)->(getByName|getByHash|getTexture|remove)' include src
|
||||
rg 'TextureFactory::instance\(\)->(loadFrom|createEmptyTexture|pushTexture)' include src
|
||||
rg '(ownIt|mOwnsDrawable|mDrawableOwner|mDrawablesOwnership|setDrawableOwner)' include src
|
||||
rg '(glDelete[A-Za-z]*|GLi->delete[A-Za-z]*)' include/eepp src/eepp
|
||||
```
|
||||
|
||||
The goal is not zero raw pointers everywhere. Allowed remaining matches must be one of:
|
||||
|
||||
- a local borrowed `.get()` view whose owning handle is in the same lexical object/call;
|
||||
- a synchronous reference parameter with documented lifetime;
|
||||
- low-level GL dispatch declarations;
|
||||
- a diagnostic weak-lock result used within the lock's strong-handle scope.
|
||||
|
||||
Every stored raw resource field requires an explicit code-review annotation and should normally be
|
||||
rejected.
|
||||
|
||||
## 10. Stage 0 exit assessment
|
||||
|
||||
Stage 0 deliverables are satisfied:
|
||||
|
||||
- GPU resource classes/direct deletion sites: inventoried.
|
||||
- Graphics-thread and TextureFactory deferred-release requirements: frozen.
|
||||
- Raw Texture holders, ID/name lookup, ignored loads, callbacks and deletion calls: inventoried.
|
||||
- Drawable classes, mutation and manual ownership surfaces: inventoried and classified.
|
||||
- Resource-related asynchronous producers and stop semantics: inventoried.
|
||||
- Current and target Engine shutdown dependency graph: documented.
|
||||
- Shared/static/MemoryManager/sanitizer build matrix: frozen.
|
||||
- Unit-test isolation contract: frozen.
|
||||
|
||||
Stage 0.5 fixes the concrete defects listed by this audit before ownership work resumes. Stage 1
|
||||
then adds TextureFactory-specific lifetime scaffolding without changing public Texture ownership;
|
||||
current factory retention remains active until the complete Stage 2 holder migration.
|
||||
Reference in New Issue
Block a user