Graphics resource lifetime: tracking, copies, moves and disposal
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read at 009d40f5; no test was run. The ResourceDestroyed observation for untracked copies follows from reading the code; no test asserts it.
XNA resources are garbage-collected objects that a GraphicsDevice can dispose on your behalf. CNA keeps that contract in C++, where objects are copied, moved and destroyed by scope. This page explains how the two meet: which resources the device tracks and what tracking does and does not mean, how copies and moves of each resource type behave, what disposal unbinds, and which resources can report lost content. It is for readers who store graphics resources in containers, pass them by value, or tear a device down while resources still exist. The device-side view of the same machinery is on GraphicsDevice internals.
The ownership model
+----------------------+
| GraphicsDevice |
+----------------------+
| owns | Dispose(): 1. mark disposed
v | 2. raise Disposing
+--------------------------+| 3. dispose every tracked resource
| resources_: raw pointers || 4. then renderer, presenter, window
| (a disposal list, not an |+
| owner) |
+--------------------------+
: non-owning, one entry per constructed resource
v
+--------------------------+ owns or shares +----------------------------+
| GraphicsResource object |-----------------> | renderer object |
| Texture2D, VertexBuffer, | | shared_ptr (Texture2D, |
| RenderTarget2D, ... | | TextureCube) or unique_ptr |
+--------------------------+ +----------------------------+
| | |
| copy | move v
v v native GPU or CPU storage
copy: shares moved-to object takes over the entry
the renderer (TransferResourceReference) and any
object, has NO texture, sampler or render-target
device entry bindings (TransferMovedTexture)
weak lifetime token: every resource can tell whether its device still exists
GraphicsDevice owns a list of raw pointers to the resources constructed on it; the list is used to dispose those resources, in step 3 of device disposal, while the renderer still exists, and it never owns or allocates them. Each resource owns (or, for Texture2D and TextureCube, shares; Texture3D also holds a shared_ptr but cannot be copied) a renderer object, which owns the native or CPU storage. A move hands the device entry and any bindings to the moved-to object; a copy of a shareable texture shares the renderer object but gets no device entry, so device disposal never reaches it. Every resource also holds a weak token that says whether its device still exists.Value-like syntax does not supply CLR lifetime semantics: the device can dispose only what it tracks, and it tracks addresses. The next sections state exactly which addresses those are.
What the device tracks
GraphicsResource(GraphicsDevice*) (GraphicsResource.cpp) appends this to the device's resources_, raises ResourceCreated, and copies a weak_ptr to the device's lifetime token. GraphicsResource::Dispose(bool) raises Disposing only on the explicit path, and, only if the token has not expired, raises ResourceDestroyed and removes the entry. The registry is a disposal-order mechanism; replacing it with owning pointers would break both public allocation and value semantics. The detailed rationale is on Textures and render targets internals: why registration is not ownership.
- Moves keep tracking correct: the moved-from address is replaced by the moved-to one (
TransferResourceReference), and a moved texture also transfers its texture-slot and render-target bindings (TransferMovedTexture). A texture move-assigned from another device first detaches the old address from the previous device's texture collections. - Copies of a resource are not registered. The
GraphicsResourcecopy constructor copies the device pointer, name and tag, starts undisposed, and deliberately copies noDisposinghandlers, but it does not add an entry and raises noResourceCreated. Device disposal therefore never disposes a copy. - State objects and vertex declarations use a shared identity instead: assignment makes the target an alias of one canonical resource whose device, name, tag and disposed flag all aliases share, and state objects are constructed without a device, so the device list never holds them. The consequences for
BlendStateand its siblings are on State objects.
Copy and move by type
| Type | Copy | Move | What a copy means |
|---|---|---|---|
Texture2D | Yes (CNAEXT, defaulted) | Yes | Shares the renderer object and CPU shadow; untracked; a full-level upload through the copy detaches it by building a new renderer object |
TextureCube | Yes (CNAEXT, defaulted) | Yes | Shares the cube renderer object and its per-face recovery copies |
Texture3D | No | Yes (added for the XNB volume reader) | n/a |
RenderTarget2D | No: unique wrapper identity | Yes; move-assigning over a bound target throws | n/a |
VertexBuffer, IndexBuffer | No | Yes; moves transfer the device's vertex- and index-buffer bindings | n/a |
BlendState, DepthStencilState, RasterizerState, SamplerState | Copy construction makes an independent, mutable value; copy assignment makes an alias | Yes | See State objects |
Effect | Protected cloning constructor only (as XNA's protected Effect(Effect cloneSource)); copy assignment deleted | — | Games clone through Effect::Clone(), which returns a raw owning pointer the caller must delete |
GraphicsDevice | No | No | n/a |
Two practical rules follow. Hold move-only resources in a std::unique_ptr or as members, never in containers that may copy. And do not rely on the device to clean up a Texture2D you copied: the copy releases its share of the renderer object when it is destroyed, but it was never on the device's list. One observable quirk of the same design: when an untracked copy is disposed (for example by its destructor), it still raises the device's ResourceDestroyed event, although no matching ResourceCreated was ever raised for it. A tool that pairs the two events should expect that.
When the device goes first
GraphicsDevice::Dispose marks itself disposed before any callback runs (so re-entrant disposal is a no-op), raises Disposing on the explicit path, moves its resource list into a local vector and clears it, calls Dispose() on every tracked resource, and only then destroys the renderer, the presenter and the window. Resources are therefore always disposed while their renderer still exists, and their re-entrant attempts to unregister are harmless. The destructor calls Dispose() and then resets the lifetime token, which is what lets any surviving resource or copy test graphicsDeviceLifetime_.expired() instead of dereferencing a dead device: texture disposal, for instance, skips its render-target detach and binding cleanup when the device is gone. The full device-side order, and what happens to renderer objects that outlive their family (Vulkan keeps a list of live textures and releases their native resources at renderer teardown), is on GraphicsDevice internals and Device reset and lifetime.
What disposing a texture unbinds
Texture::Dispose(bool) (Texture.cpp) removes the texture from every slot of GraphicsDevice.Textures and GraphicsDevice.VertexTextures before the base disposal, matching FNA's RemoveDisposedTexture, so no slot holds a dangling pointer. This applies to every texture kind, including Texture3D and TextureCube, which derive from Texture at this snapshot (earlier revisions did not, and a volume texture then skipped this unbinding). A disposed texture cannot be placed in a slot again (ObjectDisposedException).
Render targets
Disposing a render target that is still bound throws, for RenderTarget2D and RenderTargetCube alike; destroying one while bound takes the destructor path, which detaches it without pretending a valid back-buffer transition happened. The exact sequence is on Render targets: disposing and moving a bound target. Vertex and index buffers have the equivalent destructor hooks, DetachDestroyedVertexBuffer and DetachDestroyedIndexBuffer, which remove a destroyed buffer from the device's current bindings.
Lost content
Four resource types implement CNA's internal IContentLosable: RenderTarget2D, RenderTargetCube, DynamicVertexBuffer and DynamicIndexBuffer. When a renderer reports a real device reset, the device walks a snapshot of its tracked resources and calls NotifyContentLostEXT on each of them, which sets the flag and raises the resource's ContentLost event; the snapshot is taken because a subscriber may dispose the very resource it is told about. The flag clears when the caller takes ownership of the content again: a target when it is next bound, a dynamic buffer on its next upload. Ordinary textures and static buffers have no such flag; context-backed families restore them from CPU copies instead (see Texture data transfer: the CPU shadow). Only some families ever report a reset, so a flag that is never set is not evidence of anything; see Graphics architecture: device loss and reset.
Evidence and its limits
Checked by reading the CNA source at 009d40f5; not executed. Registration, copy and move behaviour, disposal order and content-lost notification are shared code; tests that pin them include GraphicsDeviceLifecycleTest (device disposal and use after disposal), Texture2DCacheReconstructionTests.cpp (shared renderer objects and detaching uploads), GraphicsDeviceDefaultStateTests.cpp (state-object aliases) and the shared fixture bound_target_lifetime_test.cpp. The ResourceDestroyed-without-ResourceCreated observation for copies follows from reading the constructors and Dispose(bool); no test was found that asserts either way.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Tutorial 71: Memory management
- Architecture
- Graphics architecture: resource lifetime
- Maintainer workflow
- Debug shutdown and lifetime
- Tests and validation
- Test architecture
- Reference
- Test target index
- Deep dives
- Device reset and lifetime · Render target semantics