Ownership and shutdown
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. Order and ownership were read from Game.hpp, Game.cpp, GraphicsDeviceManager.cpp, GraphicsDevice.cpp, GraphicsResource.cpp and CurrentPlatform.cpp at this snapshot; the named tests exist and were not executed. Backend-specific native destruction paths, mobile and browser hosts and the C API's own teardown are outside what was checked here.
The dangerous boundary in CNA is not simply “call Dispose”. A game has explicit disposal, C++ member destruction, borrowed service pointers, a device resource registry, a process-wide ambient platform and, for browsers, a loop that may still be running. Their order is visible in the code and differs depending on how the game ends. This page traces each path from the declaration order in Game.hpp to the last member destructor, names what each step guarantees, and records the bugs that order exists to prevent. It is for anyone touching a destructor, Dispose, a service registration or a device teardown step.
Ownership tree and borrowed references
Declaration order is construction order and its reverse is destruction order, so the diagram below is also the teardown script read from the bottom up (Game.hpp, GraphicsDevice.hpp).
Game object the caller owns it: stack, heap, or a C API handle slot ├─ Activated, Deactivated, Disposed, Exiting, RunApplication public members, declared first ├─ platform_ unique_ptr<IPlatform> physical lifetime encloses every later member ├─ platformInstallation_ unique_ptr<PlatformInstallation> undoes the ambient install if construction throws ├─ platformCapabilities_ value, cached once ├─ eventBatch_ unique_ptr<PlatformEventBatch> reused vector for PollEvents ├─ Components_ GameComponentCollection: raw items_ + owned_ shared_ptrs (shared overload only) ├─ GraphicsDevice_ value member │ ├─ platform_ borrowed IPlatform* (the Game's, or the ambient lazy default for a bare device) │ ├─ platformWindow_ unique_ptr<IPlatformWindow>: owns a CNA-created window, borrows an adopted one │ ├─ surfacePresenter_ unique_ptr, declared before renderer_ so it outlives it │ ├─ renderer_ unique_ptr<IGraphicsRenderer> │ ├─ resourceDeviceLifetime_ shared_ptr<void> token that resources hold weakly │ ├─ state objects, texture and sampler collections value members │ └─ resources_ vector<GraphicsResource*>: tracking only, deletes nothing ├─ Content_ ContentManager value: borrowed device, loadedAssets_ cache ├─ Window_ GameWindow value: borrows the device's IPlatformWindow ├─ LaunchParameters_ ├─ Services_ type_index -> borrowed void* │ └─ GraphicsDeviceManager* caller-owned, usually a member of the derived game ├─ clock, flags, four ordered raw-pointer component vectors, componentListsMutex_, order-token maps └─ graphicsDeviceService_, graphicsDeviceManager_, currentAdapter_ cached raw pointers
Member order to remember. Components_ is declared before GraphicsDevice_, not after the window and content manager. Reverse order therefore destroys the device first and the component collection after it, and any component held through the shared_ptr overload of Add is destroyed after the device has gone. The header comment calls platform_ the first member; strictly it is the first private data member, with the four events and RunApplication ahead of it.
The device's resource vector records which objects must be disposed while the native device still exists. It does not mean GraphicsDevice allocated every GraphicsResource or may delete one. A resource keeps a raw device pointer plus a std::weak_ptr<void> copy of resourceDeviceLifetime_ (GraphicsResource.cpp). Dispose(bool), moves and move-assignments consult that token (!graphicsDeviceLifetime_.expired()) before calling RemoveResourceReference or TransferResourceReference, so a resource whose C++ destructor runs after its device does not dereference a dead one. ~GraphicsDevice resets the token in its body, after Dispose() and before the device's own members are destroyed, so even those members see it expired. A texture or vertex buffer additionally owns a renderer-side allocation object (ITextureRenderer, IVertexBufferRenderer) that its own Dispose releases; whether every backend's allocation destructor tolerates the renderer being gone is a per-backend question these pages do not verify.
State wrappers (BlendState, DepthStencilState, RasterizerState, SamplerState, VertexDeclaration) are copied and assigned freely; copy assignment (and VertexDeclaration's copy constructor) then makes the target an alias of one SharedIdentity by ShareResourceIdentityWith, while the other four states' copy constructors produce an independent copy; that block holds the device pointer, the weak token, name, tag, the disposed flag and an alias list, and disposing any alias disposes the identity and raises Disposing on every alias. This complicates the ordinary one-object-one-registration assumption, so read GraphicsResource.cpp before changing copy, move or disposal behaviour.
Exit versus dispose versus destructor
| Action | What it does at this snapshot |
|---|---|
Game::Exit() | Sets RunApplication false and suppressDraw_ true. Nothing is disposed. |
Game::Run() returns | After the loop: OnExiting, EndRun, AfterLoop. The caller still owns the Game; nothing is disposed. |
Game::Dispose() | Calls Dispose(true), then raises Disposed — on every call, including repeated ones. Dispose(true) (first call only) disposes each component that is IDisposable, then Content_.Dispose(), then the registered graphics device service if it is IDisposable, then sets isDisposed_. Afterwards Run, DoInitialize and a following Update throw std::runtime_error. |
~Game() | On Emscripten, first stops the browser loop if this game is driving it (with an error log line). Then Dispose(false), which only marks the game disposed (no component, content or service disposal), then UninstallPlatform. Members are destroyed after the body, in reverse declaration order. |
What actually disposes the device. Game::Dispose(true) does not call GraphicsDevice_.Dispose() for the usual Game-owned value member. A registered manager's Dispose(true) unregisters its two services, releases its frame lease, unsubscribes its device events, raises its own DeviceDisposing and, because ownsGraphicsDevice_ is false for every Game-attached manager, neither disposes nor deletes the device before clearing its pointer. The device is disposed by GraphicsDevice::~GraphicsDevice(), or earlier if game code disposes it. (The gate matters: the raise of DeviceDisposing is deliberately not conditional on ownership, because gating it left the event permanently dead for the only configuration the code base constructs.)
What that means for UnloadContent. Game::Initialize subscribes UnloadContent to the graphics device service's DeviceDisposing event, and only when such a service is registered. So UnloadContent runs when (a) game code or a host disposes explicitly — Game::Dispose(), or the registered service's own Dispose() — and (b) a GraphicsDeviceManager (or another service) is registered. It runs after Content_.Dispose() has cleared the asset cache, so it is a place to release resources the game owns, not to touch content. It never runs from ~Game, from Run() returning, or in a game that has no manager, such as the demo_2d executable. GameTest.DisposingDeviceInvokesUnloadContent and RepeatedDisposeDoesNotReinvokeUnloadContent pin the explicit path; the second holds because the manager's disposed_ guard makes DeviceDisposing a one-time event even though Game::Dispose() re-raises Disposed.
Controllers are not released here: Game no longer acquires that subsystem (the startup page records why), so the platform releases whatever it acquired for the gamepad or joystick services when it is destroyed, which is the last member destructor.
Teardown order, step by step
Two paths reach teardown, and a change must be checked against both.
Explicit: game.Dispose() → Game::Dispose(true), then the Disposed event
1. every IDisposable component → Dispose()
2. Content_.Dispose() → Unload(): cached assets released
3. the registered graphics device service (a GraphicsDeviceManager):
unregister both services; drop the frame's renderer-context lease;
unsubscribe device events; raise DeviceDisposing (Game::UnloadContent runs here);
never delete or dispose the Game-owned device
4. isDisposed_ = true (GraphicsDevice_ is NOT disposed here)
Destruction (delete game, or scope exit):
0. the derived class first: its ~Derived body, then its members in reverse order
(a GraphicsDeviceManager member, textures, SpriteBatch ...) - all before any Game member
1. ~Game body: [Emscripten: stop the loop]; Dispose(false); UninstallPlatform(platform_)
2. Game members, reverse declaration order:
cached pointers, order-token maps, vectors, mutex, clock state
Services_ (drops borrowed pointers), LaunchParameters_, Window_ (borrows only)
Content_ (asset cache released; the device is still alive)
GraphicsDevice_ (~GraphicsDevice: Dispose() = Dispose(true) → Disposing raised,
registry drained while the renderer lives, renderer → presenter → window,
video reference released, lifetime token reset, then its own members)
Components_ (owned_ shared components destroyed - after the device)
eventBatch_, platformCapabilities_, platformInstallation_ (disarmed), platform_
finally the public events and RunApplication
Four consequences are easy to miss. First, the ambient platform is uninstalled in the ~Game body, before any member is destroyed: during device teardown GetCurrentPlatform() names the successor game's platform, or, when none is installed, would lazily create a brand-new default platform (see the next section). The device's own borrowed platform_ is still valid, which is why teardown code must use the stored dependency. Second, a GraphicsDeviceManager that is a member of the derived game is destroyed before every Game member: if the game was not explicitly disposed, its Dispose(false) unregisters the services (the Game and its container are still alive) and unsubscribes from the device, so the device's later Disposing event does not reach it, and DeviceDisposing is not raised at all. Third, shared-owned components outlive everything declared after Components_ — the device, content manager, window and services — so a component destructor must not call back into any of them through its Game reference; its GraphicsResource members are safe only because of the weak token. Fourth, ~GameComponentCollection is defaulted: it releases the owning references without raising ComponentRemoved and disposes nothing, so components (raw entries) are the caller's to destroy or, for IDisposable, to have disposed by an explicit Game::Dispose().
This is a composite of several lifecycles, not a claim that every application calls Dispose(). The same map with the thread side is in the Ownership and lifetime master map and the Thread and callback map; the user-level hooks are on the Game loop guide.
Inside GraphicsDevice disposal
GraphicsDevice.cpp's Dispose(bool) does, in this order:
- Returns if already disposed, otherwise sets
isDisposed_first, before any callback or owned-resource work, so a re-entrantDisposeis a no-op and aDisposinghandler cannot issue work against objects that are being torn down (the source ties this to FNA's lifecycle boundary). - Raises
Disposingwhendisposingis true. XNA's finalizer would pass false; C++ has no separate finalizer, so the destructor is the only other caller and it passes true because every member is still alive in a destructor body. The device'sDisposingevent is therefore raised during destruction as well. The manager's subscription uses it to release a still-active frame context lease soEndDrawcannot present on a destroyed renderer. - Moves
resources_into a local vector and clears the member, so a resource's re-entrantRemoveResourceReferencefinds an empty registry instead of mutating the iteration, then callsDispose()on each tracked resource whilerenderer_still exists. destroyNativeResources(): invalidates the cached capability profile, resetsrenderer_, thensurfacePresenter_, clears the sharedTextInputEXTandMousewindow handles if they still name this device's window (for owned and adopted windows alike), and resetsplatformWindow_— which destroys a CNA-created window and merely lets go of an adopted one.setVideoSubsystemAcquired(false)releases this device's single video-subsystem reference. The source states why the window goes first: the video subsystem is what backs it. The platform object itself is destroyed later, byGame.
The constructor has a mirror rollback: if resolving a renderer or applying the initial state throws, its catch calls destroyNativeResources(), balances the video reference and restores the caller's GL binding before rethrowing (see the startup page's failure table). This matters for testing fallback candidates and partial initialization. For a fallback chain, discardOwnedWindow() is the per-candidate variant: it drops the presenter, clears the shared input handles when they name the window, and resets the wrapper and the stored window handle; the attempt loop calls it only for a window this device owns, and resetting a borrowed wrapper could not destroy a caller's window in any case.
The video reference is a bool, not a counter. This is a worked historical case, fixed in the current code. Two independent campaigns each added an AcquireSubsystem(Video) (one where a window is created, one per fallback candidate) while a single release stayed in Dispose, so every windowed device took two references and returned one and the platform's video subsystem stayed up for the rest of the process; nothing observable failed. The repair is ownership rather than another release call: the device's count is videoSubsystemAcquired_, changed only by setVideoSubsystemAcquired, which acquires before setting the flag (an acquire can throw when there is no display server) and clears the flag before releasing, so every path — success, creation failure, fallback A to B to C, window failure, Dispose, the constructor's catch — balances by construction. GraphicsDeviceSubsystemLifecycleTests.cpp counts the acquisitions on a decorated real platform instead of faking one, because a fake that answers every call would have passed before the fix as well.
Ambient platform and out-of-order games
Some static XNA APIs (Keyboard::GetState, StorageDevice, TitleContainer) cannot be handed a Game, so CurrentPlatform.cpp keeps an ambient pointer, a lazily created owned default and a list of subsystem pins; Game installs its own platform for those APIs. A process-wide stack in Game.cpp records the live games' platforms, in the order they installed, and the back entry is the one the accessor aims at. A per-game “the platform I displaced” pointer looks simpler and is wrong, because games are not guaranteed to be destroyed in reverse construction order: a game that saved a predecessor destroyed first would restore a dangling pointer on its way out.
UninstallPlatform removes the departing platform wherever it sits in the stack (searching from the back, so the middle case works), takes the new back as the successor, and re-aims the accessor only if it currently points at the departing platform — a test may have installed its own, and a game going away is no reason to take that over. SetCurrentPlatform then moves the ambient pins: for each pin held on the departing platform it acquires the subsystem on the replacement and releases it on the old one; if there is no replacement, or the acquire fails, the pin is released and dropped. The pin in practice is the GraphicsAdapter video pin taken by the first adapter enumeration (the startup page's device section); when it could not be transferred, the next use of the adapter cache re-raises it. When nothing is left installed, GetCurrentPlatform() would create a default platform on demand, so code that must not do that uses HasCurrentPlatform() (this is why Game.cpp wraps it in InstalledPlatformOrNull, and why DestroyingAGameUninstallsItsPlatform can assert that the accessor is empty afterwards).
The stack, its mutex and every CurrentPlatform holder that has a destructor are deliberately immortal: allocated once, never destroyed, so unregistering is valid at any point of process teardown. The case that proved it is the C API. Its handle registry owns each C-created Game through a shared_ptr in a function-local static that is constructed on the first handle-creating call — necessarily before this file's statics, which are constructed inside that same call by the Game it is creating. Reverse-order destruction therefore destroys the stack first and the registry that still holds the Game second, and ~Game read a freed vector at exit; the source records that AddressSanitizer reported a heap-use-after-free at UninstallPlatform for every C API smoke test that left a game handle alive. It is a fixed, always-wrong ordering, not a race, and reordering cannot fix it because the registry has to exist before the object it is about to store.
~Game unregisters the ambient pointer before the platform member is destroyed, so the accessor is never left aimed at a dead platform, while the platform object's physical lifetime still encloses the device and window members because it was declared first. Code added to a later member destructor should use its stored platform dependency and never assume the ambient accessor still names this game.
Bugs this order prevents
- Video reference leak — two acquires and one release kept a native subsystem alive after the device (above).
setVideoSubsystemAcquiredcentralizes a single owned reference. - Use-after-free at exit — an immortal C API handle registry can destroy a
Gameafter ordinary statics, so the ambient platform stack and its mutex must still exist. - Dangling service —
Services_stores borrowed providers; the manager's destructor unregisters its entries before the game next queries them. The registration is not symmetric, see the maintainer note below. - Dead renderer during resource disposal — the registry is drained before
renderer_is reset, so a resource'sDisposeruns while the backend that created its allocation still exists; reversing the two would leave resources unable to release native handles. - Invalid in-flight present — disposing the device from inside
Drawneeds the manager'sDisposinglistener to release the frame lease while the renderer's context still exists. - Failed-constructor leak — a throwing
GraphicsDevice_constructor never reaches~Game; the installation guard must undo the ambient pointer before the platform argument is destroyed.AFailedConstructionLeavesNothingInstalledasserts the accessor is empty afterwards. - Resource outliving its device — the weak lifetime token keeps a late C++ destructor from calling into a destroyed device, including the device's own state members.
- Re-entrant disposal —
isDisposed_is set first and the registry is moved out before iteration.GraphicsDeviceLifecycleTest.DisposalIsReentrantAndReleasesABoundRenderTargetdisposes from inside aDisposinghandler and expects exactly one event and a released bound render target.
Maintainer note — shapes the order does not protect. Read from source, not observed: (1) Game caches graphicsDeviceService_ and graphicsDeviceManager_ as raw pointers that unregisterServices does not clear, and the manager's ClientSizeChanged subscription is never removed, so a manager destroyed while its game keeps running leaves both dangling; (2) ~GraphicsDeviceManager runs unregisterServices, which dereferences its Game, so a manager that outlives an undisposed game reads a destroyed object; (3) UnloadContent runs after Content_.Dispose() on the explicit path and never on the destructor path. The supported shape is a manager nested inside its game's lifetime, typically a member of the derived class. No test covers the two unsupported shapes.
Validation and source reading order
These tests exist at this snapshot; none was executed for this page. Each pins a narrow contract in its own configuration, and none proves every renderer's native destruction path.
| Suite | Pins |
|---|---|
GamePlatformOwnershipTests.cpp (13) | Install before members (ExplicitPlatformIsOwnedAndInstalledBeforeGameMembers), a failed construction leaves nothing installed, null rejected, install and uninstall, AnInnerGameRestoresTheOuterOnesPlatformWhenItGoesAway, AnOuterGameOutlivingAnInnerOneDoesNotOverwriteTheInstallation, OutOfOrderDestructionLeavesNoDanglingInstallation, one platform per game, repeated construct and destroy. The nested cases are guarded by a constant that is true at this snapshot. |
GameTests.cpp | DisposingDeviceInvokesUnloadContent, RepeatedDisposeDoesNotReinvokeUnloadContent, UnloadContentWorksAcrossRepeatedGameInstancesInOneProcess. |
GraphicsDeviceManagerTests.cpp | RepeatedDisposeDoesNotReraiseDeviceDisposing, reset-event forwarding. |
GameComponentCollectionTests.cpp | Shared ownership: an added shared component outlives the caller's reference, removal releases it after the event, Clear releases all, a rejected add leaves nothing behind, and the collection releases what it still owns when it goes away. |
modules/graphics/tests | GraphicsDeviceSubsystemLifecycleTests.cpp (7 cases: video-reference balance across normal disposal, failed construction, fallback, initialization failure then success, repeated lifetimes and double disposal); GraphicsDeviceLifecycleTest.RendererFacingOperationsRejectUseAfterDeviceDisposal and DisposalIsReentrantAndReleasesABoundRenderTarget; GraphicsResourceTest.CrossDeviceMoveAssignmentTransfersTrackingAndDetachesOldBindings. |
For a resource-specific change also inspect the graphics resource lifetime tests of that resource type. Audio callback barriers, sensor lifetimes and the C API's own teardown rules are mapped on the neighbouring pages; the debugging procedure is in Debug shutdown and lifetime behavior.
Game.hpp— declaration order and the borrowed manager and service members.Game.cpp—InstallPlatform, thePlatformInstallationguard,Dispose, the destructor and the uninstall stack.GraphicsDeviceManager.cpp— service unregister,DeviceDisposingand frame-lease cleanup.GraphicsDevice.cpp—Dispose(bool), registry drain,destroyNativeResources,discardOwnedWindow,setVideoSubsystemAcquired.GraphicsResource.cpp— registration, the weak lifetime token and shared-identity semantics.CurrentPlatform.cpp— the installed pointer, lazy default and pin transfer.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Exit, Exiting, Dispose and destruction — What each way of ending a CNA game runs: Exit versus Exiting, explicit Dispose versus destruction, the XNA 4.0 disposal order, repeated, re-entrant and throwing disposal, and component lifetimes at shutdown.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-029: Copying a GraphicsResource bypasses GraphicsDevice resource tracking (copies of Texture2D and TextureCube escape device disposal) — A copy-constructed Texture2D or TextureCube is never registered with its GraphicsDevice, so device disposal neither disposes it nor releases the renderer texture it shares, and a cross-device copy assignment leaves a sta
- CNA-BUG-082: GraphicsDeviceManager leaves dangling references when its lifetime is not nested inside its Game — Game never clears its cached manager and service pointers, the manager never removes its ClientSizeChanged handler, and ~GraphicsDeviceManager dereferences its Game, so a manager that dies before or after its game is a u
- CNA-BUG-087: Game::Dispose(true) walks Components by live index, so a component that removes itself or another during Dispose makes the loop skip one — XNA 4.0 copies the collection to an array before disposing; CNA re-reads the count and indexes the live collection, so a removal during a component's Dispose shifts later entries and the next component is never disposed
- CNA-BUG-091: GameWindow keeps a raw pointer to the platform window owned by the Game's GraphicsDevice, and nothing clears it when the game disposes that device — Game's constructor hands GraphicsDevice_.GetPlatformWindowInternal() to Window_.setWindowInternal; GraphicsDevice::Dispose destroys the platform window (destroyNativeResources resets platformWindow_), after which GameWin
- CNA-BUG-220: cna_game_destroy's comment says Shutdown has disposed the canonical graphics device; it has not — A comment in cna_game_destroy claims CGame::Shutdown already disposed the game's canonical GraphicsDevice so C subscribers saw its Disposing event, but Game::Dispose(true) never disposes the Game-owned device.
- CNA-VGAP-055: Process exit with live EasyGL-family resources whose creating thread has already ended is untested; a binding recorded SIGABRT on an earlier CNA revision — cna-java recorded that a process exiting with a live vertex buffer whose creating thread had ended aborts on the EasyGL renderer identities; CNA's own process-exit teardown test runs on one thread, so this case has no ev
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- Architecture
- Runtime lifecycle
- Internals
- Runtime module internals · Startup source trace · One frame source trace · GraphicsDevice internals · Textures and render targets
- Maintainer workflow
- Ownership and lifetime master map · Thread and callback map · Case study: Game component lifetime · Debug shutdown and lifetime behavior
- Tests and validation
- Test architecture
- Reference
- Test target index