mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 03:00:23 +03:00
fix: harden resource loading and memory tracking
- fix TextureAtlasLoader filter application across all textures - preserve unsigned Variant values as Type::Uint - explicitly delete unsafe Texture copy operations - define ResourceLoader progress for empty task lists - synchronize ResourceLoader status, tasks, and callbacks - join atlas loading before destroying callback-visible state - fix MemoryManager initialization and accounting races - add focused correctness and loader lifetime tests - document completed resource-refactor prerequisites
This commit is contained in:
@@ -155,6 +155,22 @@ Regression coverage:
|
|||||||
|
|
||||||
## 4. Priority C: loader and callback lifetime
|
## 4. Priority C: loader and callback lifetime
|
||||||
|
|
||||||
|
### C0. MemoryManager first-use synchronization
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
`MemoryManager::addPointer()` skips its mutex until `sHasInit` becomes true. Two concurrent first
|
||||||
|
tracked allocations can therefore race on the initialization flag, allocation map, and accounting
|
||||||
|
counters.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Use thread-safe function-local initialization for process-lifetime tracker state.
|
||||||
|
- Always lock map and accounting operations.
|
||||||
|
- Keep tracker state valid through process-static destruction.
|
||||||
|
|
||||||
|
Status: fixed, 2026-07-13. Unsuppressed focused TSAN coverage passes after the change.
|
||||||
|
|
||||||
### C1. TextureAtlasLoader member destruction order
|
### C1. TextureAtlasLoader member destruction order
|
||||||
|
|
||||||
Current behavior:
|
Current behavior:
|
||||||
@@ -164,14 +180,17 @@ reverse declaration order, so that state dies before mRL joins its work.
|
|||||||
|
|
||||||
Fix:
|
Fix:
|
||||||
|
|
||||||
- Add an explicit destructor shutdown/join before any callback-visible member is destroyed, or move
|
- Declare `mRL` last so its destructor joins before callback-visible members are destroyed.
|
||||||
async operation state into a lifetime object that outlives execution.
|
- Document and regression-test the member-order invariant.
|
||||||
- Do not rely only on member declaration order without an explicit invariant comment/test.
|
|
||||||
|
|
||||||
Regression coverage:
|
Regression coverage:
|
||||||
|
|
||||||
- Destroy a loader immediately with queued texture tasks and completion callbacks.
|
- Destroy a loader immediately with queued texture tasks and completion callbacks.
|
||||||
|
|
||||||
|
Status: fixed, 2026-07-13. `TextureAtlasLoader` now declares `mRL` last, causing its worker join to
|
||||||
|
run before callback-visible members are destroyed. Loader status/progress synchronization and a
|
||||||
|
focused ASAN lifetime test were added as part of the same fix.
|
||||||
|
|
||||||
### C2. TextureLoader static callback registry is unsynchronized and process-persistent
|
### C2. TextureLoader static callback registry is unsynchronized and process-persistent
|
||||||
|
|
||||||
Current behavior:
|
Current behavior:
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
# Resource-refactor prerequisite bug-fix execution plan
|
||||||
|
|
||||||
|
Status: active; work packages 1 and 2 completed, 2026-07-13.
|
||||||
|
|
||||||
|
This plan defines the bounded correctness work to complete before Stage 1 of the shared-resource
|
||||||
|
ownership refactor. It turns the findings in `resource_refactor_prerequisite_bugfixes.md` into an
|
||||||
|
ordered implementation sequence. The defect ledger remains the source of detailed evidence; this
|
||||||
|
document defines execution order, dependencies, validation, and the point at which prerequisite
|
||||||
|
work stops.
|
||||||
|
|
||||||
|
Related documents:
|
||||||
|
|
||||||
|
- `resource_refactor_prerequisite_bugfixes.md`
|
||||||
|
- `resource_shared_ownership_architecture.md`
|
||||||
|
- `resource_shared_ownership_stage0_inventory.md`
|
||||||
|
|
||||||
|
## 1. Objective and boundary
|
||||||
|
|
||||||
|
Fix current correctness defects that would make the ownership migration unsafe or unnecessarily
|
||||||
|
difficult, without introducing `ResourcePtr`, resource catalogs, scopes, deferred texture release,
|
||||||
|
or compatibility APIs.
|
||||||
|
|
||||||
|
This is not a general cleanup phase. A defect belongs here only when at least one of these is true:
|
||||||
|
|
||||||
|
- It can currently cause a deadlock, use-after-free, double deletion, stale cross-Engine work, or
|
||||||
|
invalid GL access.
|
||||||
|
- It prevents deterministic destruction of current Engine-owned systems.
|
||||||
|
- It prevents loaders and asynchronous producers from being stopped safely before resource
|
||||||
|
teardown.
|
||||||
|
- It is a small, independent correctness bug found during the audit and can be fixed without
|
||||||
|
designing an API that Stage 1 or a later migration will immediately replace.
|
||||||
|
|
||||||
|
Once the work packages below satisfy their exit criteria, begin Stage 1. Do not delay Stage 1 for
|
||||||
|
raw resource ownership problems already assigned to Stages 2 through 7.
|
||||||
|
|
||||||
|
## 2. Landing rules
|
||||||
|
|
||||||
|
- Land one defect or one tightly coupled lifetime cluster per change.
|
||||||
|
- Add focused regression coverage before or with each fix.
|
||||||
|
- Preserve current TextureFactory ownership and current public resource APIs.
|
||||||
|
- Avoid temporary ownership abstractions that compete with the accepted final architecture.
|
||||||
|
- Do not invoke callbacks, destroy callback-visible objects, perform GL work, or join threads while
|
||||||
|
holding a registry/pool mutex.
|
||||||
|
- Regenerate the build before compiling, format modified C++ sources, and run the relevant focused
|
||||||
|
unit-test suite under `xvfb`.
|
||||||
|
- Use ASAN for lifetime/destruction changes and TSAN where concurrent state is changed.
|
||||||
|
- For Engine teardown changes, run repeated Engine creation/destruction in one process.
|
||||||
|
|
||||||
|
## 3. Work package 1: small independent correctness fixes
|
||||||
|
|
||||||
|
These fixes are low risk and do not depend on the larger lifetime changes.
|
||||||
|
|
||||||
|
### 3.1 TextureAtlasLoader texture-filter count
|
||||||
|
|
||||||
|
Current defect:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
size_t count = getTextureAtlas()->getTexturesCount() == 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
The expression stores a boolean instead of the texture count. When textures exist, `count` becomes
|
||||||
|
zero and no filter is applied. When no textures exist, it becomes one and the loop may request
|
||||||
|
texture index zero.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Store the actual texture count.
|
||||||
|
- Apply the filter to every loaded atlas texture.
|
||||||
|
- Handle a null or not-yet-created atlas consistently with the surrounding loader API.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- An atlas with multiple textures updates every texture.
|
||||||
|
- An empty/not-yet-loaded atlas performs no invalid access.
|
||||||
|
|
||||||
|
Status: implemented and covered by `ResourcePrerequisites` unit tests. Focused ASAN tests pass.
|
||||||
|
|
||||||
|
### 3.2 Unsigned Models::Variant type
|
||||||
|
|
||||||
|
Current defect:
|
||||||
|
|
||||||
|
`Variant(const unsigned int&)` stores the value in `asUint` but sets `mType` to `Type::Int`.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Set `mType` to `Type::Uint`.
|
||||||
|
- Add construction, copy, move, assignment, `is(Type::Uint)`, `asUint()`, and `toString()` coverage.
|
||||||
|
- Include a value greater than `INT_MAX` so signed reinterpretation cannot pass unnoticed.
|
||||||
|
|
||||||
|
Status: implemented and covered by `ResourcePrerequisites.unsignedVariantPreservesTypeAndValue`.
|
||||||
|
The focused test and existing `StringMapModel` tests pass under ASAN.
|
||||||
|
|
||||||
|
### 3.3 Texture copy-construction trap
|
||||||
|
|
||||||
|
`Texture` already inherits privately from `NonCopyable`, but it still implements a protected copy
|
||||||
|
constructor that copies the GL texture handle. This is dangerous if an internal/friend path ever
|
||||||
|
uses it.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Explicitly delete the Texture copy constructor and copy assignment in `texture.hpp`.
|
||||||
|
- Remove the copy-constructor implementation.
|
||||||
|
- Add compile-time non-copyability assertions.
|
||||||
|
|
||||||
|
This is defensive cleanup rather than a currently observed public copy path and must not block the
|
||||||
|
following packages if it exposes unrelated legacy code.
|
||||||
|
|
||||||
|
Status: implemented. The obsolete implementation was removed and compile-time non-copyability
|
||||||
|
checks cover both construction and assignment.
|
||||||
|
|
||||||
|
### 3.4 Empty ResourceLoader progress
|
||||||
|
|
||||||
|
Current defect:
|
||||||
|
|
||||||
|
`ResourceLoader::getProgress()` divides by `mTasks.size()` without handling an empty loader.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Define empty-loader progress explicitly. Prefer `100%` when an empty load is considered complete;
|
||||||
|
otherwise use `0%` consistently with `isLoaded()` semantics.
|
||||||
|
- Add focused coverage for empty, partially completed, and completed loaders.
|
||||||
|
|
||||||
|
Status: implemented. An empty loader reports `0%` before loading and `100%` after completing an
|
||||||
|
empty load. Focused unit coverage verifies both states.
|
||||||
|
|
||||||
|
### 3.5 MemoryManager concurrent bootstrap
|
||||||
|
|
||||||
|
Current defect found during Work Package 2 TSAN validation:
|
||||||
|
|
||||||
|
`MemoryManager::addPointer()` conditionally skips `sAllocMutex` until a process-global `sHasInit`
|
||||||
|
flag is set. Concurrent first tracked allocations race on that flag and can enter the allocation
|
||||||
|
map and accounting counters without mutual exclusion.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Replace translation-unit bootstrap globals with one thread-safe function-local state.
|
||||||
|
- Keep that tracking state alive through process-static destruction so late `eeDelete()` calls do
|
||||||
|
not depend on static destruction order.
|
||||||
|
- Always lock allocation-map and accounting access, including metric getters.
|
||||||
|
- Return the biggest-allocation snapshot by value instead of exposing an unlocked mutable record.
|
||||||
|
|
||||||
|
Status: implemented. The focused TSAN suite initially reproduced the race in
|
||||||
|
`MemoryManager::addPointer()`. After the fix, all six `ResourcePrerequisites` tests pass under
|
||||||
|
TSAN without suppressions. The ASAN build and focused tests also pass.
|
||||||
|
|
||||||
|
Work-package exit criteria:
|
||||||
|
|
||||||
|
- Each fix has an isolated regression test.
|
||||||
|
- No resource ownership API has changed.
|
||||||
|
|
||||||
|
## 4. Work package 2: ResourceLoader and TextureAtlasLoader lifetime
|
||||||
|
|
||||||
|
This package establishes reliable loader destruction before changing texture ownership.
|
||||||
|
|
||||||
|
### 4.1 ResourceLoader synchronization audit
|
||||||
|
|
||||||
|
Current worker and caller threads read and write `mLoaded`, `mLoading`, and `mTotalLoaded`. Treat
|
||||||
|
these accesses as shared state rather than relying on timing.
|
||||||
|
|
||||||
|
Fix requirements:
|
||||||
|
|
||||||
|
- Synchronize status and progress state with atomics or a narrowly scoped mutex.
|
||||||
|
- Define which thread invokes completion callbacks. Preserve current behavior unless deliberately
|
||||||
|
changing it with documented caller migration.
|
||||||
|
- Never invoke completion callbacks while holding the loader-state mutex.
|
||||||
|
- Ensure task and callback containers cannot be modified while worker execution reads them.
|
||||||
|
|
||||||
|
### 4.2 ResourceLoader destruction contract
|
||||||
|
|
||||||
|
`ResourceLoader` owns and joins its worker from its destructor. There is no consumer-facing need
|
||||||
|
for a separate terminal shutdown state or public shutdown operation.
|
||||||
|
|
||||||
|
Contract:
|
||||||
|
|
||||||
|
- The destructor waits for the runner and its internal ThreadPool work before clearing tasks and
|
||||||
|
callbacks.
|
||||||
|
- A loader is not destroyed from one of its own tasks or completion callbacks.
|
||||||
|
- Owners whose callbacks access sibling members must encode a destruction order that destroys the
|
||||||
|
loader before those sibling members.
|
||||||
|
|
||||||
|
### 4.3 TextureAtlasLoader member order
|
||||||
|
|
||||||
|
Current member order destroys callback-visible atlas state before `mRL`, whose destructor performs
|
||||||
|
the join.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Declare `mRL` as the final data member so it is destroyed first and joins before callback-visible
|
||||||
|
atlas state is destroyed.
|
||||||
|
- Document the required order beside the member.
|
||||||
|
- Keep a destruction regression test protecting the invariant.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Destroy a loader immediately after queuing several tasks.
|
||||||
|
- Verify under ASAN that no task or callback accesses destroyed loader members.
|
||||||
|
- Run synchronization coverage under TSAN where available.
|
||||||
|
|
||||||
|
Work-package exit criteria:
|
||||||
|
|
||||||
|
- TextureAtlasLoader's required destruction order is documented and covered by a regression test.
|
||||||
|
- ResourceLoader status/progress reads are data-race-free.
|
||||||
|
- No callbacks execute under internal synchronization locks.
|
||||||
|
|
||||||
|
Status: implemented. Status/progress counters are atomic, task and callback container access is
|
||||||
|
synchronized, and callbacks execute after releasing internal locks. `ResourceLoader` joins its
|
||||||
|
worker in its destructor without exposing a terminal shutdown API. `TextureAtlasLoader` declares
|
||||||
|
`mRL` last, documents the order invariant, and publishes asynchronous status through atomics.
|
||||||
|
Focused ASAN coverage verifies atlas destruction while tasks and a completion callback are pending.
|
||||||
|
All six `ResourcePrerequisites` tests pass under both ASAN and unsuppressed TSAN.
|
||||||
|
|
||||||
|
## 5. Work package 3: shared ThreadPool HTTP operation lifetime
|
||||||
|
|
||||||
|
This follows the already fixed `Http::Pool::clear()` lock-order defect.
|
||||||
|
|
||||||
|
Current defect:
|
||||||
|
|
||||||
|
When `Http::setThreadPool()` is active, queued lambdas capture raw `Http*`. `Http` tracks and joins
|
||||||
|
only its privately created `AsyncRequest` threads, so a shared-pool operation may begin or continue
|
||||||
|
after its Http object has been destroyed.
|
||||||
|
|
||||||
|
Fix requirements:
|
||||||
|
|
||||||
|
- Register every asynchronous operation before publishing it to an executor.
|
||||||
|
- Give queued/running operations lifetime and cancellation state independent of raw `Http*`.
|
||||||
|
- `Http::~Http()` rejects new work, cancels all registered operations, and waits until no operation
|
||||||
|
can dereference the object.
|
||||||
|
- Handle destruction initiated from an operation callback without joining/waiting on the same
|
||||||
|
operation.
|
||||||
|
- Do not destroy or drain an externally owned shared ThreadPool.
|
||||||
|
- Do not wait while holding the global Http Pool mutex, operation-map mutex, or any lock visible to
|
||||||
|
callbacks.
|
||||||
|
- Preserve cancellation callback behavior deliberately and document it.
|
||||||
|
- Cover all three async forms: response in memory, external IOStream, and output path.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Queue a request behind blocked shared-pool work, destroy/clear its Http owner, then unblock it.
|
||||||
|
- Destroy while a request is running.
|
||||||
|
- Re-enter `Http::Pool` from a callback during concurrent pool clearing.
|
||||||
|
- Initiate final-owner release from a callback and verify no self-deadlock.
|
||||||
|
- Run under ASAN and TSAN.
|
||||||
|
|
||||||
|
Work-package exit criteria:
|
||||||
|
|
||||||
|
- No shared-pool lambda depends on an untracked raw Http lifetime.
|
||||||
|
- Http destruction provides a complete operation barrier without taking ownership of the executor.
|
||||||
|
|
||||||
|
## 6. Work package 4: deterministic Engine shutdown
|
||||||
|
|
||||||
|
Reorder shutdown only after HTTP and loader barriers are reliable.
|
||||||
|
|
||||||
|
### 6.1 Required dependency order
|
||||||
|
|
||||||
|
The exact implementation may group calls differently, but it must preserve this dependency graph:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mark Engine shutting down / reject new deliveries
|
||||||
|
-> stop and join HTTP and resource-producing work
|
||||||
|
-> invalidate and purge UI main-thread deliveries
|
||||||
|
-> destroy scenes
|
||||||
|
-> flush or discard GlobalBatchRenderer submissions
|
||||||
|
-> clear TextLayout and shaped-font caches
|
||||||
|
-> destroy UI/global drawable providers and resource managers
|
||||||
|
-> destroy fonts, atlases and textures in dependency order
|
||||||
|
-> destroy shader programs and shaders
|
||||||
|
-> destroy framebuffer and vertex-buffer registries/owners
|
||||||
|
-> destroy Renderer
|
||||||
|
-> destroy windows and GL contexts
|
||||||
|
-> destroy process utilities and backend state
|
||||||
|
```
|
||||||
|
|
||||||
|
Concrete corrections required:
|
||||||
|
|
||||||
|
- Stop `Network::Http::Pool` and Engine-owned resource producers before Graphics consumers.
|
||||||
|
- Destroy `SceneManager` before `GlobalBatchRenderer` and `NinePatchManager` resources scenes can
|
||||||
|
reference.
|
||||||
|
- Flush or explicitly discard pending batches before releasing their borrowed dependencies.
|
||||||
|
- Call `TextLayout::clearLayoutCache()` before `FontManager::destroySingleton()`.
|
||||||
|
- Destroy `ShaderProgramManager` before `Renderer` while `GLi` and a valid context still exist.
|
||||||
|
- Keep windows/context state alive through every GPU-object destruction step.
|
||||||
|
|
||||||
|
### 6.2 Validation
|
||||||
|
|
||||||
|
- Engine destruction with a live scene using fonts, nine-patches, textures, batches, shaders, FBOs,
|
||||||
|
and vertex buffers.
|
||||||
|
- Engine destruction with pending HTTP and decode/resource-loader work.
|
||||||
|
- Multiple Engine create/destroy cycles in one test process.
|
||||||
|
- Assert no destructor recreates an Engine or manager singleton.
|
||||||
|
- ASAN/LSAN clean teardown in supported configurations.
|
||||||
|
|
||||||
|
Work-package exit criteria:
|
||||||
|
|
||||||
|
- All asynchronous producers are behind a shutdown barrier before their consumers are destroyed.
|
||||||
|
- Every GPU-owning manager is destroyed while its required Renderer/context services remain valid.
|
||||||
|
- Repeated Engine lifecycle tests pass.
|
||||||
|
|
||||||
|
## 7. Work package 5: UISceneNode async delivery lifecycle
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
Async resource deliveries are stored in a process-static queue. Generation/alive checks prevent
|
||||||
|
many stale callbacks from mutating a dead scene, but queued closures and their captures can survive
|
||||||
|
until an unrelated future scene update and cross an Engine test boundary.
|
||||||
|
|
||||||
|
Fix requirements:
|
||||||
|
|
||||||
|
- Add explicit accept/reject/drain/purge lifecycle operations for the queue.
|
||||||
|
- Reject new deliveries once UI/Engine shutdown begins.
|
||||||
|
- Invalidate scene generation/alive state before purging queued closures.
|
||||||
|
- Release captures on the main/update thread, following the existing project destruction contract.
|
||||||
|
- Re-open/reset the delivery mechanism deliberately for a recreated test Engine.
|
||||||
|
- Do not allow work queued by Engine lifecycle A to execute during lifecycle B.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Queue immediate and delayed deliveries, destroy the scene before update, and verify neither runs.
|
||||||
|
- Destroy and recreate Engine, then update a new UISceneNode and verify no old closure executes.
|
||||||
|
- Race worker submission with queue shutdown under TSAN.
|
||||||
|
|
||||||
|
Work-package exit criteria:
|
||||||
|
|
||||||
|
- The static queue has an explicit Engine lifecycle boundary.
|
||||||
|
- Purging releases all stale captures deterministically.
|
||||||
|
|
||||||
|
## 8. Work package 6: FrameBufferFBO recreation correctness
|
||||||
|
|
||||||
|
First determine the intended callers and semantics of `FrameBufferFBO::reload()`.
|
||||||
|
|
||||||
|
Required distinction:
|
||||||
|
|
||||||
|
- Live-context recreation must release the previous framebuffer/renderbuffer objects before
|
||||||
|
replacing their handles.
|
||||||
|
- Context-loss recreation must forget names from the lost namespace without issuing invalid delete
|
||||||
|
calls against the replacement context.
|
||||||
|
|
||||||
|
Additional create-path audit:
|
||||||
|
|
||||||
|
- Restore prior framebuffer/renderbuffer bindings on every failure return.
|
||||||
|
- Release partially created objects on live-context failure.
|
||||||
|
- Leave the object in a destructible, clearly invalid state after failure.
|
||||||
|
- Do not change texture-attachment ownership in this package; that belongs to TexturePtr Stage 2.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Repeated live-context recreation does not grow tracked GL object counts.
|
||||||
|
- Context-loss recreation performs no deletion in the lost namespace.
|
||||||
|
- Forced create failures restore previous bindings and do not leak partial objects.
|
||||||
|
|
||||||
|
Work-package exit criteria:
|
||||||
|
|
||||||
|
- `reload()` has an explicit live/lost-context contract.
|
||||||
|
- Replacement and failure paths have deterministic GL-handle cleanup.
|
||||||
|
|
||||||
|
## 9. Work package 7: TextureLoader callback registry safety
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
`TextureLoader::sCbs` is process-static and unsynchronized. Loading may notify from a worker while
|
||||||
|
UITextureViewer or another caller registers/removes callbacks.
|
||||||
|
|
||||||
|
Fix requirements:
|
||||||
|
|
||||||
|
- Synchronize callback registration and removal.
|
||||||
|
- Copy/snapshot callbacks under the lock and invoke the snapshot after unlocking.
|
||||||
|
- Define removal-during-notification behavior.
|
||||||
|
- Add an explicit test/Engine lifecycle reset operation if the registry remains process-static.
|
||||||
|
- Ensure callbacks from an old Engine lifecycle cannot target UI state in a recreated Engine.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Concurrent registration, removal, and notification under TSAN.
|
||||||
|
- Callback re-entry into registration/removal does not deadlock.
|
||||||
|
- Repeated Engine lifecycle does not inherit callback subscriptions.
|
||||||
|
|
||||||
|
This package may be omitted only if Stage 1 immediately replaces this registry with the accepted
|
||||||
|
weak live-texture diagnostics mechanism. The omission must be an explicit Stage 1 scope decision,
|
||||||
|
not an assumption.
|
||||||
|
|
||||||
|
## 10. Deferred findings: do not solve in this plan
|
||||||
|
|
||||||
|
The following defects remain recorded but should normally be resolved by their owning migration
|
||||||
|
stage because a raw-pointer workaround would be short-lived or semantically incomplete:
|
||||||
|
|
||||||
|
- `Variant` copying an owning Drawable pointer: resolve with the Stage 4 handle/`std::variant`
|
||||||
|
redesign unless a current reproducer requires an emergency restriction.
|
||||||
|
- `UISkin::clone()` and `StateListDrawable` duplicating ownership maps: resolve with Stage 4
|
||||||
|
source/instance semantics unless a current owning-child clone path is demonstrated.
|
||||||
|
- FrameBuffer texture attachment direct/factory ownership: resolve in the complete TexturePtr
|
||||||
|
holder cut in Stage 2.
|
||||||
|
- TextureRegion, TextureAtlas, fonts, glyphs, sprites, particles, and batches borrowing textures:
|
||||||
|
resolve together in Stage 2.
|
||||||
|
- Mutable Drawable sharing and incorrect `isStateful()` classifications: resolve in Stage 4.
|
||||||
|
- TextureFactory lock scope, weak diagnostics, semantic lookup, metrics, and deferred release:
|
||||||
|
these are Stage 1 through Stage 3 architecture work, not prerequisite patches.
|
||||||
|
|
||||||
|
## 11. Final prerequisite gate
|
||||||
|
|
||||||
|
Stage 1 may begin when all mandatory packages satisfy these invariants:
|
||||||
|
|
||||||
|
- HTTP and ResourceLoader work cannot outlive the objects they dereference.
|
||||||
|
- Engine shutdown rejects and joins asynchronous resource producers before destroying scenes or
|
||||||
|
Graphics systems.
|
||||||
|
- UISceneNode queued delivery cannot cross an Engine lifecycle boundary.
|
||||||
|
- Scenes and caches are destroyed before the resources they borrow.
|
||||||
|
- Shader, font-layout, Renderer, and context destruction order is valid.
|
||||||
|
- TextureAtlasLoader destruction is safe with active work.
|
||||||
|
- FrameBuffer recreation cannot leak or delete objects from the wrong context namespace.
|
||||||
|
- Focused ASAN tests and repeated Engine create/destroy tests pass.
|
||||||
|
|
||||||
|
After this gate, start Stage 1 immediately. Any newly discovered issue is added to this prerequisite
|
||||||
|
track only if it violates one of these invariants; otherwise it is assigned to its resource-family
|
||||||
|
migration stage.
|
||||||
@@ -64,7 +64,7 @@ class EE_API MemoryManager {
|
|||||||
|
|
||||||
static size_t getTotalMemoryUsage();
|
static size_t getTotalMemoryUsage();
|
||||||
|
|
||||||
static const AllocatedPointer& getBiggestAllocation();
|
static AllocatedPointer getBiggestAllocation();
|
||||||
};
|
};
|
||||||
#if defined( __GNUC__ ) && __GNUC__ >= 12
|
#if defined( __GNUC__ ) && __GNUC__ >= 12
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
|
|||||||
@@ -340,7 +340,9 @@ class EE_API Texture : public DrawableResource, public Image, private NonCopyabl
|
|||||||
const Texture::ClampMode& clampMode, const bool& CompressedTexture,
|
const Texture::ClampMode& clampMode, const bool& CompressedTexture,
|
||||||
const Uint32& memSize = 0, const Uint8* data = NULL );
|
const Uint32& memSize = 0, const Uint8* data = NULL );
|
||||||
|
|
||||||
Texture( const Texture& copy );
|
Texture( const Texture& copy ) = delete;
|
||||||
|
|
||||||
|
Texture& operator=( const Texture& copy ) = delete;
|
||||||
|
|
||||||
void create( const Uint32& texture, const unsigned int& width, const unsigned int& height,
|
void create( const Uint32& texture, const unsigned int& width, const unsigned int& height,
|
||||||
const unsigned int& imgwidth, const unsigned int& imgheight, const bool& UseMipmap,
|
const unsigned int& imgwidth, const unsigned int& imgheight, const bool& UseMipmap,
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ class EE_API TextureAtlasLoader {
|
|||||||
void setThreaded( const bool& threaded );
|
void setThreaded( const bool& threaded );
|
||||||
|
|
||||||
/** @return True if the texture atlas is loaded. */
|
/** @return True if the texture atlas is loaded. */
|
||||||
const bool& isLoaded() const;
|
bool isLoaded() const;
|
||||||
|
|
||||||
/** @return True if the texture atlas is loading. */
|
/** @return True if the texture atlas is loading. */
|
||||||
const bool& isLoading() const;
|
bool isLoading() const;
|
||||||
|
|
||||||
/** The function will check if the texture atlas is updated. Checks if all the images inside the
|
/** The function will check if the texture atlas is updated. Checks if all the images inside the
|
||||||
* images path are inside the texture atlas, and if they have the same date and size, otherwise
|
* images path are inside the texture atlas, and if they have the same date and size, otherwise
|
||||||
@@ -162,13 +162,12 @@ class EE_API TextureAtlasLoader {
|
|||||||
void setTextureFilter( const Texture::Filter& textureFilter );
|
void setTextureFilter( const Texture::Filter& textureFilter );
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
ResourceLoader mRL;
|
|
||||||
std::string mTextureAtlasPath;
|
std::string mTextureAtlasPath;
|
||||||
bool mThreaded;
|
bool mThreaded;
|
||||||
bool mLoaded;
|
std::atomic<bool> mLoaded;
|
||||||
Pack* mPack;
|
Pack* mPack;
|
||||||
bool mSkipResourceLoad;
|
bool mSkipResourceLoad;
|
||||||
bool mIsLoading;
|
std::atomic<bool> mIsLoading;
|
||||||
TextureAtlas* mTextureAtlas;
|
TextureAtlas* mTextureAtlas;
|
||||||
GLLoadCallback mLoadCallback;
|
GLLoadCallback mLoadCallback;
|
||||||
std::vector<Texture*> mTexturesLoaded;
|
std::vector<Texture*> mTexturesLoaded;
|
||||||
@@ -181,6 +180,10 @@ class EE_API TextureAtlasLoader {
|
|||||||
sTextureAtlasHdr mTexGrHdr;
|
sTextureAtlasHdr mTexGrHdr;
|
||||||
std::vector<sTempTexAtlas> mTempAtlass;
|
std::vector<sTempTexAtlas> mTempAtlass;
|
||||||
|
|
||||||
|
// Must remain the last data member: it joins its worker in its destructor before any
|
||||||
|
// callback-visible loader state above is destroyed.
|
||||||
|
ResourceLoader mRL;
|
||||||
|
|
||||||
void createTextureRegions();
|
void createTextureRegions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#ifndef EE_SYSTEMCRESOURCELOADER
|
#ifndef EE_SYSTEMCRESOURCELOADER
|
||||||
#define EE_SYSTEMCRESOURCELOADER
|
#define EE_SYSTEMCRESOURCELOADER
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <eepp/core.hpp>
|
#include <eepp/core.hpp>
|
||||||
#include <eepp/system/thread.hpp>
|
#include <eepp/system/thread.hpp>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace EE { namespace System {
|
namespace EE { namespace System {
|
||||||
@@ -61,12 +63,13 @@ class EE_API ResourceLoader {
|
|||||||
Uint32 getCount() const;
|
Uint32 getCount() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool mLoaded;
|
std::atomic<bool> mLoaded;
|
||||||
bool mLoading;
|
std::atomic<bool> mLoading;
|
||||||
bool mThreaded;
|
std::atomic<bool> mThreaded;
|
||||||
Uint32 mThreads;
|
Uint32 mThreads;
|
||||||
Uint32 mTotalLoaded;
|
std::atomic<Uint32> mTotalLoaded;
|
||||||
Thread mThread;
|
Thread mThread;
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
|
||||||
std::vector<ResLoadCallback> mLoadCbs;
|
std::vector<ResLoadCallback> mLoadCbs;
|
||||||
std::vector<ObjectLoaderTask> mTasks;
|
std::vector<ObjectLoaderTask> mTasks;
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ class EE_API Variant {
|
|||||||
explicit Variant( const String& string ) : mType( Type::String ) {
|
explicit Variant( const String& string ) : mType( Type::String ) {
|
||||||
mValue.asString = eeNew( String, ( string ) );
|
mValue.asString = eeNew( String, ( string ) );
|
||||||
}
|
}
|
||||||
explicit Variant( const String* string ) : mType( Type::StringPtr ) { mValue.asStringPtr = string; }
|
explicit Variant( const String* string ) : mType( Type::StringPtr ) {
|
||||||
|
mValue.asStringPtr = string;
|
||||||
|
}
|
||||||
Variant( Drawable* drawable, bool ownDrawable = false ) : mType( Type::Drawable ) {
|
Variant( Drawable* drawable, bool ownDrawable = false ) : mType( Type::Drawable ) {
|
||||||
mValue.asDrawable = drawable;
|
mValue.asDrawable = drawable;
|
||||||
mOwnsObject = ownDrawable;
|
mOwnsObject = ownDrawable;
|
||||||
@@ -54,7 +56,7 @@ class EE_API Variant {
|
|||||||
Variant( bool val ) : mType( Type::Bool ) { mValue.asBool = val; }
|
Variant( bool val ) : mType( Type::Bool ) { mValue.asBool = val; }
|
||||||
Variant( const Float& val ) : mType( Type::Float ) { mValue.asFloat = val; }
|
Variant( const Float& val ) : mType( Type::Float ) { mValue.asFloat = val; }
|
||||||
Variant( const int& val ) : mType( Type::Int ) { mValue.asInt = val; }
|
Variant( const int& val ) : mType( Type::Int ) { mValue.asInt = val; }
|
||||||
Variant( const unsigned int& val ) : mType( Type::Int ) { mValue.asUint = val; }
|
Variant( const unsigned int& val ) : mType( Type::Uint ) { mValue.asUint = val; }
|
||||||
Variant( const Int64& val ) : mType( Type::Int64 ) { mValue.asInt64 = val; }
|
Variant( const Int64& val ) : mType( Type::Int64 ) { mValue.asInt64 = val; }
|
||||||
Variant( const Uint64& val ) : mType( Type::Uint64 ) { mValue.asUint64 = val; }
|
Variant( const Uint64& val ) : mType( Type::Uint64 ) { mValue.asUint64 = val; }
|
||||||
explicit Variant( const char* data ) : mType( Type::cstr ) { mValue.asCStr = data; }
|
explicit Variant( const char* data ) : mType( Type::cstr ) { mValue.asCStr = data; }
|
||||||
|
|||||||
@@ -23,20 +23,8 @@ void operator delete( void* p ) throw() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef EE_MEMORY_MANAGER
|
|
||||||
static bool sHasInit = false;
|
|
||||||
#else
|
|
||||||
static bool sHasInit = true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace EE {
|
namespace EE {
|
||||||
|
|
||||||
static AllocatedPointerMap sMapPointers;
|
|
||||||
static size_t sTotalMemoryUsage = 0;
|
|
||||||
static size_t sPeakMemoryUsage = 0;
|
|
||||||
static AllocatedPointer sBiggestAllocation = AllocatedPointer( NULL, "", 0, 0 );
|
|
||||||
static Mutex sAllocMutex;
|
|
||||||
|
|
||||||
AllocatedPointer::AllocatedPointer( void* data, const std::string& file, int line, size_t memory,
|
AllocatedPointer::AllocatedPointer( void* data, const std::string& file, int line, size_t memory,
|
||||||
bool track ) {
|
bool track ) {
|
||||||
mData = data;
|
mData = data;
|
||||||
@@ -46,6 +34,26 @@ AllocatedPointer::AllocatedPointer( void* data, const std::string& file, int lin
|
|||||||
mTrack = track;
|
mTrack = track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct MemoryManagerState {
|
||||||
|
AllocatedPointerMap pointers;
|
||||||
|
size_t totalMemoryUsage{ 0 };
|
||||||
|
size_t peakMemoryUsage{ 0 };
|
||||||
|
AllocatedPointer biggestAllocation{ NULL, "", 0, 0 };
|
||||||
|
Mutex allocationMutex;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryManagerState& getMemoryManagerState() {
|
||||||
|
// The tracker must remain valid through process-static destruction. Function-local
|
||||||
|
// initialization makes its first concurrent use safe; intentionally retaining the state avoids
|
||||||
|
// reintroducing a static-destruction-order dependency for late eeDelete() calls.
|
||||||
|
static MemoryManagerState* state = new MemoryManagerState;
|
||||||
|
return *state;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void* MemoryManager::allocate( size_t size ) {
|
void* MemoryManager::allocate( size_t size ) {
|
||||||
return malloc( size );
|
return malloc( size );
|
||||||
}
|
}
|
||||||
@@ -55,9 +63,11 @@ void* MemoryManager::reallocate( void* ptr, size_t size ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void* MemoryManager::addPointerInPlace( void* place, const AllocatedPointer& aAllocatedPointer ) {
|
void* MemoryManager::addPointerInPlace( void* place, const AllocatedPointer& aAllocatedPointer ) {
|
||||||
AllocatedPointerMapIt it = sMapPointers.find( place );
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
AllocatedPointerMapIt it = state.pointers.find( place );
|
||||||
|
|
||||||
if ( it != sMapPointers.end() ) {
|
if ( it != state.pointers.end() ) {
|
||||||
removePointer( place, aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
|
removePointer( place, aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,42 +75,40 @@ void* MemoryManager::addPointerInPlace( void* place, const AllocatedPointer& aAl
|
|||||||
}
|
}
|
||||||
|
|
||||||
void* MemoryManager::addPointer( const AllocatedPointer& aAllocatedPointer ) {
|
void* MemoryManager::addPointer( const AllocatedPointer& aAllocatedPointer ) {
|
||||||
ConditionalLock l( sHasInit, &sAllocMutex );
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
|
||||||
sMapPointers.insert(
|
state.pointers.insert(
|
||||||
AllocatedPointerMap::value_type( aAllocatedPointer.mData, aAllocatedPointer ) );
|
AllocatedPointerMap::value_type( aAllocatedPointer.mData, aAllocatedPointer ) );
|
||||||
|
|
||||||
sTotalMemoryUsage += aAllocatedPointer.mMemory;
|
state.totalMemoryUsage += aAllocatedPointer.mMemory;
|
||||||
|
|
||||||
if ( sPeakMemoryUsage < sTotalMemoryUsage ) {
|
if ( state.peakMemoryUsage < state.totalMemoryUsage ) {
|
||||||
sPeakMemoryUsage = sTotalMemoryUsage;
|
state.peakMemoryUsage = state.totalMemoryUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( aAllocatedPointer.mMemory > sBiggestAllocation.mMemory ) {
|
if ( aAllocatedPointer.mMemory > state.biggestAllocation.mMemory ) {
|
||||||
sBiggestAllocation = aAllocatedPointer;
|
state.biggestAllocation = aAllocatedPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( aAllocatedPointer.mTrack )
|
if ( aAllocatedPointer.mTrack )
|
||||||
eePRINTL( "Allocating pointer %p at '%s' %d", aAllocatedPointer.mData,
|
eePRINTL( "Allocating pointer %p at '%s' %d", aAllocatedPointer.mData,
|
||||||
aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
|
aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
|
||||||
|
|
||||||
#ifdef EE_MEMORY_MANAGER
|
|
||||||
sHasInit = true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return aAllocatedPointer.mData;
|
return aAllocatedPointer.mData;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* MemoryManager::reallocPointer( void* data, const AllocatedPointer& aAllocatedPointer ) {
|
void* MemoryManager::reallocPointer( void* data, const AllocatedPointer& aAllocatedPointer ) {
|
||||||
Lock l( sAllocMutex );
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
|
||||||
AllocatedPointerMapIt it = sMapPointers.find( data );
|
AllocatedPointerMapIt it = state.pointers.find( data );
|
||||||
|
|
||||||
if ( it != sMapPointers.end() && it->second.mTrack )
|
if ( it != state.pointers.end() && it->second.mTrack )
|
||||||
eePRINTL( "Realloc pointer %p at '%s' %d", data, aAllocatedPointer.mFile.c_str(),
|
eePRINTL( "Realloc pointer %p at '%s' %d", data, aAllocatedPointer.mFile.c_str(),
|
||||||
aAllocatedPointer.mLine );
|
aAllocatedPointer.mLine );
|
||||||
|
|
||||||
if ( it == sMapPointers.end() )
|
if ( it == state.pointers.end() )
|
||||||
return addPointer( aAllocatedPointer );
|
return addPointer( aAllocatedPointer );
|
||||||
|
|
||||||
if ( aAllocatedPointer.mTrack )
|
if ( aAllocatedPointer.mTrack )
|
||||||
@@ -111,20 +119,20 @@ void* MemoryManager::reallocPointer( void* data, const AllocatedPointer& aAlloca
|
|||||||
removePointer( data, aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
|
removePointer( data, aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
|
||||||
addPointer( aAllocatedPointer );
|
addPointer( aAllocatedPointer );
|
||||||
} else {
|
} else {
|
||||||
sTotalMemoryUsage -= it->second.mMemory;
|
state.totalMemoryUsage -= it->second.mMemory;
|
||||||
it->second.mMemory = aAllocatedPointer.mMemory;
|
it->second.mMemory = aAllocatedPointer.mMemory;
|
||||||
it->second.mFile = aAllocatedPointer.mFile;
|
it->second.mFile = aAllocatedPointer.mFile;
|
||||||
it->second.mLine = aAllocatedPointer.mLine;
|
it->second.mLine = aAllocatedPointer.mLine;
|
||||||
it->second.mTrack = aAllocatedPointer.mTrack;
|
it->second.mTrack = aAllocatedPointer.mTrack;
|
||||||
|
|
||||||
sTotalMemoryUsage += aAllocatedPointer.mMemory;
|
state.totalMemoryUsage += aAllocatedPointer.mMemory;
|
||||||
|
|
||||||
if ( sPeakMemoryUsage < sTotalMemoryUsage ) {
|
if ( state.peakMemoryUsage < state.totalMemoryUsage ) {
|
||||||
sPeakMemoryUsage = sTotalMemoryUsage;
|
state.peakMemoryUsage = state.totalMemoryUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( aAllocatedPointer.mMemory > sBiggestAllocation.mMemory ) {
|
if ( aAllocatedPointer.mMemory > state.biggestAllocation.mMemory ) {
|
||||||
sBiggestAllocation = aAllocatedPointer;
|
state.biggestAllocation = aAllocatedPointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +140,12 @@ void* MemoryManager::reallocPointer( void* data, const AllocatedPointer& aAlloca
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MemoryManager::removePointer( void* data, const char* file, const size_t& line ) {
|
bool MemoryManager::removePointer( void* data, const char* file, const size_t& line ) {
|
||||||
Lock l( sAllocMutex );
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
|
||||||
AllocatedPointerMapIt it = sMapPointers.find( data );
|
AllocatedPointerMapIt it = state.pointers.find( data );
|
||||||
|
|
||||||
if ( it == sMapPointers.end() ) {
|
if ( it == state.pointers.end() ) {
|
||||||
eePRINTL( "Trying to delete pointer %p created that does not exist!", data );
|
eePRINTL( "Trying to delete pointer %p created that does not exist!", data );
|
||||||
eeASSERT( false );
|
eeASSERT( false );
|
||||||
return false;
|
return false;
|
||||||
@@ -145,23 +154,29 @@ bool MemoryManager::removePointer( void* data, const char* file, const size_t& l
|
|||||||
if ( it->second.mTrack )
|
if ( it->second.mTrack )
|
||||||
eePRINTL( "Deleting pointer %p at '%s' %d", data, file, line );
|
eePRINTL( "Deleting pointer %p at '%s' %d", data, file, line );
|
||||||
|
|
||||||
sTotalMemoryUsage -= it->second.mMemory;
|
state.totalMemoryUsage -= it->second.mMemory;
|
||||||
|
|
||||||
sMapPointers.erase( it );
|
state.pointers.erase( it );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t MemoryManager::getPeakMemoryUsage() {
|
size_t MemoryManager::getPeakMemoryUsage() {
|
||||||
return sPeakMemoryUsage;
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
return state.peakMemoryUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t MemoryManager::getTotalMemoryUsage() {
|
size_t MemoryManager::getTotalMemoryUsage() {
|
||||||
return sTotalMemoryUsage;
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
return state.totalMemoryUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AllocatedPointer& MemoryManager::getBiggestAllocation() {
|
AllocatedPointer MemoryManager::getBiggestAllocation() {
|
||||||
return sBiggestAllocation;
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
return state.biggestAllocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MemoryManager::showResults() {
|
void MemoryManager::showResults() {
|
||||||
@@ -173,11 +188,13 @@ void MemoryManager::showResults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
|
auto& state = getMemoryManagerState();
|
||||||
|
Lock lock( state.allocationMutex );
|
||||||
|
|
||||||
eePRINTL( "\n|--Memory Manager Report-------------------------------------|" );
|
eePRINTL( "\n|--Memory Manager Report-------------------------------------|" );
|
||||||
eePRINTL( "|" );
|
eePRINTL( "|" );
|
||||||
|
|
||||||
if ( sMapPointers.empty() ) {
|
if ( state.pointers.empty() ) {
|
||||||
eePRINTL( "| No memory leaks detected." );
|
eePRINTL( "| No memory leaks detected." );
|
||||||
} else {
|
} else {
|
||||||
eePRINTL( "| Memory leaks detected: " );
|
eePRINTL( "| Memory leaks detected: " );
|
||||||
@@ -186,9 +203,9 @@ void MemoryManager::showResults() {
|
|||||||
|
|
||||||
// Get max length of file name
|
// Get max length of file name
|
||||||
int lMax = 0;
|
int lMax = 0;
|
||||||
AllocatedPointerMapIt it = sMapPointers.begin();
|
AllocatedPointerMapIt it = state.pointers.begin();
|
||||||
|
|
||||||
for ( ; it != sMapPointers.end(); ++it ) {
|
for ( ; it != state.pointers.end(); ++it ) {
|
||||||
AllocatedPointer& ap = it->second;
|
AllocatedPointer& ap = it->second;
|
||||||
|
|
||||||
if ( (int)ap.mFile.length() > lMax )
|
if ( (int)ap.mFile.length() > lMax )
|
||||||
@@ -204,9 +221,9 @@ void MemoryManager::showResults() {
|
|||||||
|
|
||||||
eePRINTL( "|-----------------------------------------------------------|" );
|
eePRINTL( "|-----------------------------------------------------------|" );
|
||||||
|
|
||||||
it = sMapPointers.begin();
|
it = state.pointers.begin();
|
||||||
|
|
||||||
for ( ; it != sMapPointers.end(); ++it ) {
|
for ( ; it != state.pointers.end(); ++it ) {
|
||||||
AllocatedPointer& ap = it->second;
|
AllocatedPointer& ap = it->second;
|
||||||
|
|
||||||
eePRINT( "| %p\t %s", ap.mData, ap.mFile.c_str() );
|
eePRINT( "| %p\t %s", ap.mData, ap.mFile.c_str() );
|
||||||
@@ -220,12 +237,13 @@ void MemoryManager::showResults() {
|
|||||||
|
|
||||||
eePRINTL( "|" );
|
eePRINTL( "|" );
|
||||||
eePRINTL( "| Memory left: %s",
|
eePRINTL( "| Memory left: %s",
|
||||||
FileSystem::sizeToString( static_cast<Int64>( sTotalMemoryUsage ) ).c_str() );
|
FileSystem::sizeToString( static_cast<Int64>( state.totalMemoryUsage ) ).c_str() );
|
||||||
eePRINTL( "| Biggest allocation:" );
|
eePRINTL( "| Biggest allocation:" );
|
||||||
eePRINTL( "| %s in file: %s at line: %d",
|
eePRINTL( "| %s in file: %s at line: %d",
|
||||||
FileSystem::sizeToString( sBiggestAllocation.mMemory ).c_str(),
|
FileSystem::sizeToString( state.biggestAllocation.mMemory ).c_str(),
|
||||||
sBiggestAllocation.mFile.c_str(), sBiggestAllocation.mLine );
|
state.biggestAllocation.mFile.c_str(), state.biggestAllocation.mLine );
|
||||||
eePRINTL( "| Peak Memory Usage: %s", FileSystem::sizeToString( sPeakMemoryUsage ).c_str() );
|
eePRINTL( "| Peak Memory Usage: %s",
|
||||||
|
FileSystem::sizeToString( state.peakMemoryUsage ).c_str() );
|
||||||
eePRINTL( "|------------------------------------------------------------|\n" );
|
eePRINTL( "|------------------------------------------------------------|\n" );
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -41,24 +41,6 @@ Texture::Texture() :
|
|||||||
mFilter( Filter::Linear ),
|
mFilter( Filter::Linear ),
|
||||||
mCoordinateType( CoordinateType::Normalized ) {}
|
mCoordinateType( CoordinateType::Normalized ) {}
|
||||||
|
|
||||||
Texture::Texture( const Texture& Copy ) :
|
|
||||||
DrawableResource( Drawable::TEXTURE, Copy.mName ),
|
|
||||||
Image(),
|
|
||||||
mFilepath( Copy.mFilepath ),
|
|
||||||
mTexture( Copy.mTexture ),
|
|
||||||
mImgWidth( Copy.mImgWidth ),
|
|
||||||
mImgHeight( Copy.mImgHeight ),
|
|
||||||
mFlags( Copy.mFlags ),
|
|
||||||
mClampMode( Copy.mClampMode ),
|
|
||||||
mFilter( Copy.mFilter ) {
|
|
||||||
mWidth = Copy.mWidth;
|
|
||||||
mHeight = Copy.mHeight;
|
|
||||||
mChannels = Copy.mChannels;
|
|
||||||
mSize = Copy.mSize;
|
|
||||||
|
|
||||||
setPixels( reinterpret_cast<const Uint8*>( &Copy.mPixels[0] ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
Texture::Texture( const Uint32& texture, const unsigned int& width, const unsigned int& height,
|
Texture::Texture( const Uint32& texture, const unsigned int& width, const unsigned int& height,
|
||||||
const unsigned int& imgwidth, const unsigned int& imgheight,
|
const unsigned int& imgwidth, const unsigned int& imgheight,
|
||||||
const bool& UseMipmap, const unsigned int& Channels, const std::string& filepath,
|
const bool& UseMipmap, const unsigned int& Channels, const std::string& filepath,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ TextureAtlasLoader::TextureAtlasLoader( IOStream& IOS, const bool& Threaded,
|
|||||||
loadFromStream( IOS );
|
loadFromStream( IOS );
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureAtlasLoader::~TextureAtlasLoader() {}
|
TextureAtlasLoader::~TextureAtlasLoader() = default;
|
||||||
|
|
||||||
void TextureAtlasLoader::setLoadCallback( GLLoadCallback LoadCallback ) {
|
void TextureAtlasLoader::setLoadCallback( GLLoadCallback LoadCallback ) {
|
||||||
mLoadCallback = LoadCallback;
|
mLoadCallback = LoadCallback;
|
||||||
@@ -115,12 +115,12 @@ sTextureAtlasHdr TextureAtlasLoader::getTextureAtlasHeader() {
|
|||||||
void TextureAtlasLoader::setTextureFilter( const Texture::Filter& textureFilter ) {
|
void TextureAtlasLoader::setTextureFilter( const Texture::Filter& textureFilter ) {
|
||||||
mTexGrHdr.TextureFilter = (char)textureFilter;
|
mTexGrHdr.TextureFilter = (char)textureFilter;
|
||||||
|
|
||||||
size_t count = getTextureAtlas()->getTexturesCount() == 0;
|
if ( NULL == mTextureAtlas )
|
||||||
|
return;
|
||||||
|
|
||||||
if ( count > 0 ) {
|
const size_t count = mTextureAtlas->getTexturesCount();
|
||||||
for ( size_t i = 0; i < count; i++ )
|
for ( size_t i = 0; i < count; i++ )
|
||||||
getTextureAtlas()->getTexture( i )->setFilter( textureFilter );
|
mTextureAtlas->getTexture( i )->setFilter( textureFilter );
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureAtlasLoader::loadFromStream( IOStream& IOS ) {
|
void TextureAtlasLoader::loadFromStream( IOStream& IOS ) {
|
||||||
@@ -319,12 +319,12 @@ void TextureAtlasLoader::setThreaded( const bool& threaded ) {
|
|||||||
mThreaded = threaded;
|
mThreaded = threaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool& TextureAtlasLoader::isLoaded() const {
|
bool TextureAtlasLoader::isLoaded() const {
|
||||||
return mLoaded;
|
return mLoaded.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool& TextureAtlasLoader::isLoading() const {
|
bool TextureAtlasLoader::isLoading() const {
|
||||||
return mIsLoading;
|
return mIsLoading.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture* TextureAtlasLoader::getTexture( const Uint32& texnum ) const {
|
Texture* TextureAtlasLoader::getTexture( const Uint32& texnum ) const {
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ ResourceLoader::ResourceLoader( const Uint32& maxThreads ) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
ResourceLoader::~ResourceLoader() {
|
ResourceLoader::~ResourceLoader() {
|
||||||
clear();
|
|
||||||
|
|
||||||
mThread.wait();
|
mThread.wait();
|
||||||
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::setThreads() {
|
void ResourceLoader::setThreads() {
|
||||||
@@ -31,31 +30,35 @@ void ResourceLoader::setThreads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ResourceLoader::isThreaded() const {
|
bool ResourceLoader::isThreaded() const {
|
||||||
return mThreaded;
|
return mThreaded.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 ResourceLoader::getCount() const {
|
Uint32 ResourceLoader::getCount() const {
|
||||||
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
return mTasks.size();
|
return mTasks.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::setThreaded( const bool& threaded ) {
|
void ResourceLoader::setThreaded( const bool& threaded ) {
|
||||||
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
if ( !mLoading ) {
|
if ( !mLoading ) {
|
||||||
mThreaded = threaded;
|
mThreaded = threaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::add( const ObjectLoaderTask& objectLoaderTask ) {
|
void ResourceLoader::add( const ObjectLoaderTask& objectLoaderTask ) {
|
||||||
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
if ( !mLoading ) {
|
if ( !mLoading ) {
|
||||||
mTasks.emplace_back( objectLoaderTask );
|
mTasks.emplace_back( objectLoaderTask );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResourceLoader::clear() {
|
bool ResourceLoader::clear() {
|
||||||
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
if ( !mLoading ) {
|
if ( !mLoading ) {
|
||||||
mLoaded = false;
|
mLoaded = false;
|
||||||
mLoading = false;
|
|
||||||
mTotalLoaded = 0;
|
mTotalLoaded = 0;
|
||||||
mTasks.clear();
|
mTasks.clear();
|
||||||
|
mLoadCbs.clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,75 +66,88 @@ bool ResourceLoader::clear() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::load( const ResLoadCallback& callback ) {
|
void ResourceLoader::load( const ResLoadCallback& callback ) {
|
||||||
if ( callback )
|
{
|
||||||
mLoadCbs.push_back( callback );
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
|
if ( callback )
|
||||||
|
mLoadCbs.push_back( callback );
|
||||||
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::load() {
|
void ResourceLoader::load() {
|
||||||
if ( mLoaded )
|
bool serialized = false;
|
||||||
return;
|
{
|
||||||
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
|
if ( mLoaded || mLoading )
|
||||||
|
return;
|
||||||
|
|
||||||
if ( mThreaded ) {
|
mLoading = true;
|
||||||
if ( !mLoading ) {
|
if ( mThreaded ) {
|
||||||
mLoading = true;
|
|
||||||
mThread.launch();
|
mThread.launch();
|
||||||
|
} else {
|
||||||
|
serialized = true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
serializedLoad();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( serialized )
|
||||||
|
serializedLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResourceLoader::isLoaded() {
|
bool ResourceLoader::isLoaded() {
|
||||||
return mLoaded;
|
return mLoaded.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResourceLoader::isLoading() {
|
bool ResourceLoader::isLoading() {
|
||||||
return mLoading;
|
return mLoading.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::setLoaded() {
|
void ResourceLoader::setLoaded() {
|
||||||
mLoaded = true;
|
mLoaded = true;
|
||||||
mLoading = false;
|
mLoading = false;
|
||||||
|
|
||||||
if ( mLoadCbs.size() ) {
|
std::vector<ResLoadCallback> callbacks;
|
||||||
for ( auto it = mLoadCbs.begin(); it != mLoadCbs.end(); ++it ) {
|
{
|
||||||
( *it )( this );
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
}
|
callbacks.swap( mLoadCbs );
|
||||||
|
|
||||||
mLoadCbs.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for ( auto& callback : callbacks )
|
||||||
|
callback( this );
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::taskRunner() {
|
void ResourceLoader::taskRunner() {
|
||||||
{
|
{
|
||||||
auto pool = ThreadPool::createUnique( eemin( mThreads, (Uint32)mTasks.size() ) );
|
auto pool = ThreadPool::createUnique( eemin( mThreads, (Uint32)mTasks.size() ) );
|
||||||
|
|
||||||
for ( auto& task : mTasks ) {
|
for ( auto& task : mTasks )
|
||||||
pool->run( task, [this]( const auto& ) { mTotalLoaded++; } );
|
pool->run( task, [this]( const auto& ) { mTotalLoaded++; } );
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mLoading = false;
|
|
||||||
setLoaded();
|
setLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceLoader::serializedLoad() {
|
void ResourceLoader::serializedLoad() {
|
||||||
mLoading = true;
|
|
||||||
|
|
||||||
for ( auto& task : mTasks ) {
|
for ( auto& task : mTasks ) {
|
||||||
task();
|
task();
|
||||||
|
|
||||||
mTotalLoaded++;
|
mTotalLoaded++;
|
||||||
}
|
}
|
||||||
|
|
||||||
mLoading = false;
|
|
||||||
setLoaded();
|
setLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
Float ResourceLoader::getProgress() {
|
Float ResourceLoader::getProgress() {
|
||||||
return mTotalLoaded / (float)mTasks.size() * 100.f;
|
Uint32 taskCount;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock( mMutex );
|
||||||
|
taskCount = mTasks.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( taskCount == 0 )
|
||||||
|
return mLoaded ? 100.f : 0.f;
|
||||||
|
|
||||||
|
return mTotalLoaded / (float)taskCount * 100.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
}} // namespace EE::System
|
}} // namespace EE::System
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include "utest.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <eepp/graphics/textureatlas.hpp>
|
||||||
|
#include <eepp/graphics/textureatlasloader.hpp>
|
||||||
|
#include <eepp/graphics/texturefactory.hpp>
|
||||||
|
#include <eepp/system/resourceloader.hpp>
|
||||||
|
#include <eepp/ui/models/variant.hpp>
|
||||||
|
#include <eepp/window/engine.hpp>
|
||||||
|
#include <eepp/window/window.hpp>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
using namespace EE;
|
||||||
|
using namespace EE::Graphics;
|
||||||
|
using namespace EE::UI::Models;
|
||||||
|
using namespace EE::Window;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct LoaderGate {
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable condition;
|
||||||
|
bool started{ false };
|
||||||
|
bool release{ false };
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
std::unique_lock<std::mutex> lock( mutex );
|
||||||
|
started = true;
|
||||||
|
condition.notify_all();
|
||||||
|
condition.wait( lock, [this] { return release; } );
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitUntilStarted() {
|
||||||
|
std::unique_lock<std::mutex> lock( mutex );
|
||||||
|
condition.wait( lock, [this] { return started; } );
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish() {
|
||||||
|
std::unique_lock<std::mutex> lock( mutex );
|
||||||
|
release = true;
|
||||||
|
condition.notify_all();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestTextureAtlas : public TextureAtlas {
|
||||||
|
public:
|
||||||
|
using TextureAtlas::setTextures;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestTextureAtlasLoader : public TextureAtlasLoader {
|
||||||
|
public:
|
||||||
|
void setTextureAtlas( TextureAtlas* textureAtlas ) { mTextureAtlas = textureAtlas; }
|
||||||
|
|
||||||
|
void loadDelayed( const std::shared_ptr<LoaderGate>& gate,
|
||||||
|
const std::shared_ptr<std::atomic<bool>>& callbackCompleted ) {
|
||||||
|
mRL.setThreaded( true );
|
||||||
|
mRL.add( [gate] { gate->run(); } );
|
||||||
|
mRL.add( [] {} );
|
||||||
|
mRL.load( [this, callbackCompleted]( ResourceLoader* ) {
|
||||||
|
mTempAtlass.emplace_back();
|
||||||
|
*callbackCompleted = true;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static_assert( !std::is_copy_constructible<Texture>::value, "Texture must not be copyable" );
|
||||||
|
static_assert( !std::is_copy_assignable<Texture>::value, "Texture must not be copy-assignable" );
|
||||||
|
|
||||||
|
UTEST( ResourcePrerequisites, textureAtlasLoaderAppliesFilterToEveryTexture ) {
|
||||||
|
Engine::instance()->createWindow( WindowSettings( 64, 64, "TextureAtlasLoader filter test",
|
||||||
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
|
||||||
|
{
|
||||||
|
Texture* first = TextureFactory::instance()->createEmptyTexture( 1, 1 );
|
||||||
|
Texture* second = TextureFactory::instance()->createEmptyTexture( 1, 1 );
|
||||||
|
ASSERT_TRUE( first != NULL );
|
||||||
|
ASSERT_TRUE( second != NULL );
|
||||||
|
|
||||||
|
TestTextureAtlas atlas;
|
||||||
|
atlas.setTextures( { first, second } );
|
||||||
|
|
||||||
|
TestTextureAtlasLoader loader;
|
||||||
|
loader.setTextureAtlas( &atlas );
|
||||||
|
loader.setTextureFilter( Texture::Filter::Nearest );
|
||||||
|
|
||||||
|
EXPECT_EQ( first->getFilter(), Texture::Filter::Nearest );
|
||||||
|
EXPECT_EQ( second->getFilter(), Texture::Filter::Nearest );
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( ResourcePrerequisites, textureAtlasLoaderAcceptsFilterBeforeAtlasExists ) {
|
||||||
|
TestTextureAtlasLoader loader;
|
||||||
|
loader.setTextureFilter( Texture::Filter::Nearest );
|
||||||
|
EXPECT_EQ( static_cast<Texture::Filter>( loader.getTextureAtlasHeader().TextureFilter ),
|
||||||
|
Texture::Filter::Nearest );
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( ResourcePrerequisites, unsignedVariantPreservesTypeAndValue ) {
|
||||||
|
const unsigned int value = std::numeric_limits<unsigned int>::max();
|
||||||
|
Variant original( value );
|
||||||
|
|
||||||
|
ASSERT_TRUE( original.is( Variant::Type::Uint ) );
|
||||||
|
EXPECT_EQ( original.asUint(), value );
|
||||||
|
EXPECT_TRUE( original.toString() == std::to_string( value ) );
|
||||||
|
|
||||||
|
Variant copied( original );
|
||||||
|
EXPECT_TRUE( copied.is( Variant::Type::Uint ) );
|
||||||
|
EXPECT_EQ( copied.asUint(), value );
|
||||||
|
|
||||||
|
Variant moved( std::move( copied ) );
|
||||||
|
EXPECT_TRUE( moved.is( Variant::Type::Uint ) );
|
||||||
|
EXPECT_EQ( moved.asUint(), value );
|
||||||
|
EXPECT_TRUE( !copied.isValid() );
|
||||||
|
|
||||||
|
Variant copyAssigned;
|
||||||
|
copyAssigned = original;
|
||||||
|
EXPECT_TRUE( copyAssigned.is( Variant::Type::Uint ) );
|
||||||
|
EXPECT_EQ( copyAssigned.asUint(), value );
|
||||||
|
|
||||||
|
Variant moveAssigned;
|
||||||
|
moveAssigned = std::move( copyAssigned );
|
||||||
|
EXPECT_TRUE( moveAssigned.is( Variant::Type::Uint ) );
|
||||||
|
EXPECT_EQ( moveAssigned.asUint(), value );
|
||||||
|
EXPECT_TRUE( !copyAssigned.isValid() );
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( ResourcePrerequisites, emptyResourceLoaderHasDefinedProgress ) {
|
||||||
|
ResourceLoader loader;
|
||||||
|
loader.setThreaded( false );
|
||||||
|
|
||||||
|
EXPECT_EQ( loader.getProgress(), 0.f );
|
||||||
|
|
||||||
|
loader.load();
|
||||||
|
|
||||||
|
EXPECT_TRUE( loader.isLoaded() );
|
||||||
|
EXPECT_EQ( loader.getProgress(), 100.f );
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( ResourcePrerequisites, resourceLoaderReportsPartialAndCompleteProgress ) {
|
||||||
|
ResourceLoader loader;
|
||||||
|
loader.setThreaded( false );
|
||||||
|
Float progressDuringSecondTask = 0.f;
|
||||||
|
|
||||||
|
loader.add( [] {} );
|
||||||
|
loader.add( [&] { progressDuringSecondTask = loader.getProgress(); } );
|
||||||
|
loader.load();
|
||||||
|
|
||||||
|
EXPECT_EQ( progressDuringSecondTask, 50.f );
|
||||||
|
EXPECT_EQ( loader.getProgress(), 100.f );
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( ResourcePrerequisites, textureAtlasLoaderWaitsBeforeDestroyingCallbackState ) {
|
||||||
|
auto gate = std::make_shared<LoaderGate>();
|
||||||
|
auto callbackCompleted = std::make_shared<std::atomic<bool>>( false );
|
||||||
|
auto loader = std::make_unique<TestTextureAtlasLoader>();
|
||||||
|
loader->loadDelayed( gate, callbackCompleted );
|
||||||
|
gate->waitUntilStarted();
|
||||||
|
|
||||||
|
std::thread destroyThread( [loader = std::move( loader )]() mutable { loader.reset(); } );
|
||||||
|
gate->finish();
|
||||||
|
destroyThread.join();
|
||||||
|
|
||||||
|
EXPECT_TRUE( *callbackCompleted );
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user