Resource residency and maintenance
A resource domain ties native images, geometry and programs to one backend/context lifetime. A retained logical handle keeps its resource ownership, but cannot make that native lifetime valid after renderer shutdown. CPU release contexts are covered separately by CPU memory lifetime.
When a resource seems to remain resident, follow its owners in order:
Release the application’s handles and, when appropriate, clear or replace the owning cache entry. A cache is itself a strong owner.
Account for materials, scenes and published frames retaining the same generation. Frame recycling releases only the recycled frame’s owners.
Allow platform-owned
Renderer::maintain_resources()to collect native registrations whose final logical owner has retired. Dropping a handle on another thread does not call OpenGL there.
The sections below distinguish those logical owners from native collection and shutdown; cache size alone cannot tell whether a native object is still live.
Cache ownership
CE::Assets::Cache::Context is the shared provider/loading-owner guard used by all asset
caches. It allows one active provider domain and one loading owner at a time.
It protects publication and teardown, rather than implementing a cache eviction
policy. Each cache map retains a strong shared_ptr until explicit clear,
successful replacement, or provider teardown. A lookup copies another strong
handle under a shared lock. An unused cached asset remains resident; the cache
has no LRU, residency budget or automatic unused-asset eviction.
Provider teardown first marks the domain as releasing, then clears material, sprite/tile, font, indexed shader, program and image caches in that order. Final owners release outside cache locks. A reentrant deleter can inspect a cleared cache but cannot refill it during teardown. When release ends, a new provider can bind the global domain. Externally retained logical assets continue to exist independently; their native handles still belong to the old renderer/context lifetime and cannot be used after that lifetime closes.
Graphics-domain observation
Retain renderer.graphics_domain() or provider.graphics_domain() when a cache
must know whether its native resources remain usable. The portable
CE::Rendering::GraphicsDomain token retains only synchronized observation state;
it owns no renderer, window, context or native objects. An empty token has ID zero
and explicitly means unsupported. A supported domain has a checked permanent ID:
stateDiagram-v2
accTitle: Permanent graphics-domain lifetime
accDescr: A supported domain starts uninitialized, becomes available after native initialization, and becomes permanently unavailable after shutdown or abandonment. A replacement renderer creates a different domain.
Uninitialized --> Available: initialize / sequence 1
Available --> Unavailable: shutdown or abandon / sequence 2
Sequence zero means unavailable with reason NotInitialized; sequence one is
available with reason Initialized. Sequence two is permanently unavailable with
reason Shutdown after native deletion/deactivation, or Abandoned when native
cleanup could not run. A domain never reopens. Another renderer has another ID;
retaining or replacing a provider facade does not migrate old handles.
CE::Rendering::graphics_domain_state_changed_event carries an owned historical
current snapshot and a live domain token. Runtime configures renderer routing
and installs CE::Assets::Cache::Context::evict_graphics_domain on its Context
bus. Standalone hosts select a weak renderer route and may install that consumer
on their loading/graphics owner. It ignores foreign/stale records and clears the
matching bound caches after real invalidation, releasing their provider binding.
Native cleanup remains the backend’s responsibility even if observation fails.
Available publication waits until the complete renderer initialization operation finishes. A failure before deactivation leaves a live domain available; a later context-release failure after deactivation preserves the unavailable fact and the original direct error. Destruction can abandon a live domain without calling listeners; optional terminal owner delivery is best-effort. The retained token’s current truth remains readable after facade destruction.
Register and read the initial token snapshot on the publication owner when an
ordered seed is needed. A bus subscription has no replay. Token snapshots may be
read on any thread, but Available grants no foreign-thread/context access and
does not freeze availability. Per-key catalogue generations
carry this provenance separately from catalogue identity, without retaining GPU
owners. The contract reports native lifetime, not device-loss recovery, residency
budgets or automatic asset adoption.
Immutable resource publication
CE::Assets::Images::decode(path) and decode(encoded_bytes) are CPU-only
operations returning owned, tightly packed top-to-bottom RGBA pixels. They do
not publish native handles or retain an encoded byte span. Toolkit decoding uses
the same Engine implementation; each UI module does not embed another stb decoder.
CE::Rendering::Resources::Provider::create_image() copies owned RGBA pixels into a new backend handle.
Changed images or geometry use fresh immutable handles and replacement materials
or frames. Published packets retain their selected geometry, material, program and
image/sampler generations; replacing a cache entry releases only that cache’s old owner.
There is no in-place texture update or dynamic atlas-growth API. UI recordings use
owned CPU images and expanded triangles; the
Unicode service
publishes complete message-specific glyph-page generations after successful upload.
Neither requires a shared glyph-residency cache.
Colored geometry uploads use CE::Rendering::Geometry::Vertex2DColor and a Position3UV2Color4 pipeline.
A provider that does not implement that overload rejects it explicitly; CE::Rendering::Geometry::Vertex2D
retains its existing layout. The
render contract
defines layout and alpha validation. Generic asset batches follow the separate
partial-publication and retry contract.
Native retirement and maintenance
Logical release can happen on another thread; native collection remains on the graphics owner. Shutdown closes the native domain even when logical owners remain.
stateDiagram-v2
accTitle: Native registration retirement
accDescr: Final logical release marks a native registration pending. Graphics-owner maintenance collects it. Renderer shutdown sweeps remaining registrations and closes the domain, making later logical release harmless.
Live --> Pending: final logical owner releases
Pending --> Collected: graphics-owner maintenance
Live --> Closed: renderer shutdown sweep
Pending --> Closed: renderer shutdown sweep
Closed --> Closed: late logical release does no native work
Strong owners and their native retirement paths are below. Namespace names in
the table are relative to CE.
Resource |
Strong logical ownership |
Native retirement |
|---|---|---|
Image/texture |
|
|
Sampler |
Explicit image bindings, material FX policy and retained packets; provider/uploader reuse tables are weak |
|
Geometry |
|
|
Linked program |
|
|
Compiled stage |
Local shader-link guard and temporary program attachment |
Mark for deletion after compilation; detach every stage after linking so retained programs do not retain stages. Failure destroys the guarded program and its remaining attachments. |
Font atlas/glyphs |
|
Texture/vertex-array final-owner retirement; layout metadata has independent CPU ownership. |
Sprite/tileset/graphic |
Strong cache or application handle; const |
Composite release drops constituent owners; no second native deleter. |
Published frame |
Each packet retains geometry/material and copied image/sampler parameter handles; pass parameters retain their own bindings |
Platform recycling releases the frame’s owners; other owners can keep resources resident. |
Pipeline/material |
|
Native handles retire through the same program/image/sampler owners. Successful replacement preserves old frame owners; failed reload preserves the cache entry. |
Indexed shader catalogue |
|
Catalogue replacement/clear drops its owners; native resources retire through their underlying handles. |
UI uploaders retain their lazily created default material pair. OpenGL’s built-in program/material caches are weak; they do not add catalogue entries or prolong residency after uploader, scene and frame handles release those resources.
CE::Rendering::OpenGL::Handle registration is move-only. Destruction from any thread marks its
registration pending; it never calls GL. Duplicate pending retirement is ignored,
and collected registration slots can be reused only after deletion. Renderer
shutdown deletes all remaining tracked IDs, including resources with external
logical owners, and closes the lifetime. A later release does not delete again.
Using a closed handle fails before querying its borrowed context.
CE::Rendering::Renderer::maintain_resources() is the backend-neutral platform maintenance
operation. OpenGL validates the selected current context, unbinds its last program,
then runs the context-owned retirement collector. A deleted current program stays
alive until it is unbound; this also happens before the shutdown sweep while a
borrowed window/context may remain alive. Subsequent draws select their own program.
Backends without deferred resource release implement a no-op. Both runtime modes
invoke it through a required maintenance attachment
after platform/render/recycle work and before waiting, including when no
simulation update or first frame is available. It also runs while accepted CPU
and simulation work settle. After final frame/game releases, the final collection
phase is followed by the direct renderer maintenance barrier even if a phase
callback failed. Partial renderer
initialization does not count as ready for maintenance. Clear/render validate
their context but no longer duplicate collection.
Idle platform waits are capped at 10 ms. A resource retired just after collection will therefore receive another maintenance opportunity without a new frame, poll, or explicit retirement notifier. A full lockstep/finite polling backlog does not disable this bound. This bounds the wait, rather than promising a 10 ms deletion deadline: a blocked presentation, long sequential update, platform callback, or OS scheduling delay can still postpone owner-thread maintenance. Concurrent simulation stays on its own timing deadline; these platform wakeups do not synthesize early simulation updates or consume input themselves.
Tracking, use, collection, shutdown, and untracked failure cleanup require the native owner and its actual current context. A resource from another backend is rejected by pipeline/geometry/image domain checks before binding; another native context’s resources are rejected as well. One context being current does not make another context’s IDs valid. Native pipeline/geometry/image domain validation preserves these guards.
Failure and recovery
Native creation guards retain an untracked ID until registration succeeds.
Program linking then transfers ownership to a CE::Rendering::OpenGL::Handle before constructing
the logical CE::Rendering::OpenGL::Program, so later allocation failure has only one retirement
owner. Logical program adoption validates that this registration is a program,
preserving retirement when another native resource kind is rejected.
Texture, vertex-array and buffer failure guards and program/stage guards use
discard_untracked(): it checks owner/current context and cannot replace the
original failure. If the context is unavailable, it leaves native cleanup to
platform context destruction. Renderer destruction similarly uses abandon()
when context recovery fails, invalidating logical handles without GL calls.
Texture and buffer constructors reject pending native errors before generating IDs and check generation, storage upload, mipmap generation and vertex-layout setup before publishing a logical resource. Errors include their native code. Failed untracked creation uses guarded discard; tracked IDs retire during constructor unwinding. An atlas restores unpack alignment before reporting upload failure and skips dependent mipmap generation. This detects reported native failures without promising recovery from memory exhaustion or context loss. OpenGL leaves native state undefined after OUT_OF_MEMORY; real-driver acceptance remains separate.
Cache insertion retains a local candidate owner across fallible node construction and rehash. A failed or duplicate insertion can release its final candidate only after the cache write lock unwinds. Replacement exchanges an already established slot, and clear detaches the whole map before release. This includes material candidates and their retained program/image resources; reentrant deleters remain outside publication locks on all three paths.
Texture::unbind(unit) selects the supplied unit after checking the texture’s
owner/current/live context and unit limit. It does not depend on an inherited active
unit.
If shutdown cannot obtain the current context, pending/live registrations remain
available for owner recovery and an exactly-once sweep. If recovery fails during
renderer destruction, abandon() invalidates every registration without a native
delete or later query of the destroyed context. Late foreign-thread release is safe
for those invalidated handles.
Cache entry counts do not measure physical residency bytes. Shared images, staging pixels, mipmaps, programs and pending retirement have different owners and lifetimes; future budgets require the accounting plan before introducing eviction.
Validation procedures cover strong residency, cache-domain rebinding, idle maintenance, recording failure/recovery/abandonment, real selected-context restoration, retained frame pixels and native deletion before window destruction. Hardware context loss and reset recovery remain outside those executed scopes. See architecture-validation.md.