Graphics resource lifetime: tracking, copies, moves and disposal

CNA snapshot 009d40f5  ·  Deep Dives › The graphics machine  ·  source links pinned to 009d40f5

✓

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
Figure. A 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 GraphicsResource copy constructor copies the device pointer, name and tag, starts undisposed, and deliberately copies no Disposing handlers, but it does not add an entry and raises no ResourceCreated. 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 BlendState and its siblings are on State objects.

Copy and move by type

TypeCopyMoveWhat a copy means
Texture2DYes (CNAEXT, defaulted)YesShares the renderer object and CPU shadow; untracked; a full-level upload through the copy detaches it by building a new renderer object
TextureCubeYes (CNAEXT, defaulted)YesShares the cube renderer object and its per-face recovery copies
Texture3DNoYes (added for the XNB volume reader)n/a
RenderTarget2DNo: unique wrapper identityYes; move-assigning over a bound target throwsn/a
VertexBuffer, IndexBufferNoYes; moves transfer the device's vertex- and index-buffer bindingsn/a
BlendState, DepthStencilState, RasterizerState, SamplerStateCopy construction makes an independent, mutable value; copy assignment makes an aliasYesSee State objects
EffectProtected 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
GraphicsDeviceNoNon/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.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Maintainer workflow
Debug shutdown and lifetime
Tests and validation
Test architecture