CNA-GAP-071: GraphicsDevice is single-threaded: its resource registry has no lock and only EasyGL and OpenGL4 lease a context, so resource creation from a loading thread is supported only for ContentManager loads on those two
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. Nothing on this page was executed unless the Evidence section says so.
GraphicsDevice has no lock: GraphicsResource construction and disposal update its resource registry, and only EasyGL and OpenGL4 return a context lease (taken by frames and ContentManager loads), while XNA 4.0 creates its device D3DCREATE_MULTITHREADED.
- Identifier
CNA-GAP-071- Category
- Functional gap
- Subsystem
- Graphics & renderers
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- Microsoft::Xna::Framework::Graphics::GraphicsDevice and GraphicsResource: creating or disposing resources from a thread other than the game thread
Expected behaviour
XNA 4.0 creates its Direct3D 9 device with D3DCREATE_MULTITHREADED (decompiled GraphicsDevice.CreateDevice sets creation flags 38, which is 0x26 and includes 0x4), so a loading thread can create textures and buffers while the game thread runs. IGraphicsRenderer.hpp says renderer families that return no lease 'support concurrent resource creation directly or own a different synchronization boundary'.
Actual behaviour at TARGET
GraphicsDevice.cpp contains no mutex. AddResourceReference and RemoveResourceReference push to and swap-pop a plain std::vector, and GraphicsResource's device constructor and Dispose(bool) call them, outside any renderer call. The context lease is implemented only by EasyGLRenderer and OpenGL4Renderer and is taken by GraphicsDeviceManager::BeginDraw, by ContentReader and by GraphicsDevice's Clear and Present, not by resource constructors; every other renderer returns no lease. Creating or disposing a resource on a second thread while the game thread creates or disposes another therefore races on the registry, and a background ContentManager load on a renderer without a lease has no CNA-level exclusion against frames (the Vulkan, Direct3D, SDL_GPU, WebGPU, SDL_Renderer and Software sources hold no lock for it). CNA's own Vulkan lease test records the consequence: the renderer 'contains no synchronization at all', and 'A game that drives it from two threads races regardless of the lease'. GraphicsDevice.hpp states no threading rule.
Source locations
modules/graphics/src/Xna/GraphicsDevice.cpp— AddResourceReference, RemoveResourceReference, AcquireRendererThreadContextLease, Clear, Presentmodules/graphics/src/Xna/GraphicsResource.cpp— GraphicsResource constructor and Dispose(bool) registry callsmodules/graphics/include/CNA/Internal/Renderers/Common/IGraphicsRenderer.hpp— IRendererThreadContextLease documentation and AcquireThreadContextLeaseEXT defaultmodules/content/src/Xna/ContentReader.cpp— ContentReader constructor takes the leasemodules/runtime/src/GraphicsDeviceManager.cpp— BeginDraw takes the frame leasemodules/renderers/easygl/src/EasyGLRenderer.cpp— AcquireThreadContextLeaseEXT, the first of two implementersmodules/renderers/opengl4/src/OpenGL4Renderer.cpp— AcquireThreadContextLeaseEXT, the second implementermodules/renderers/vulkan/examples/vulkan_thread_context_lease_test.cpp— header: a null lease is not a thread-safety guarantee, the renderer has no synchronization
Evidence
Read at 009d40f5; nothing executed. Read GraphicsDevice.cpp and GraphicsResource.cpp for locking and registry use, searched the modules for every acquirer and implementer of the lease and the non-GL renderers for locks (only unrelated debug-queue and shader-session mutexes), read the lease contract in IGraphicsRenderer.hpp, the header of the Vulkan lease test and XNA's decompiled GraphicsDevice.CreateDevice for its creation flags. The race is established by reading and was not reproduced.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
The thread-context lease is tested per renderer (EasyGL, OpenGL4, and a null-lease test for Vulkan whose header says the renderer has no synchronization); no test creates or disposes graphics resources from two threads at once.
Regression test
Create and dispose textures and buffers from two threads at once on each renderer under ThreadSanitizer; or document that resource creation is game-thread-only and put a lock around GraphicsDevice::AddResourceReference and RemoveResourceReference.
Blast radius
Games that create or dispose graphics resources on a loading thread, and background ContentManager loads on renderers that return no context lease. Games that touch graphics only from the game thread are unaffected.
Workaround
Create and dispose graphics resources on the game thread, or marshal loads to it; background ContentManager loads are covered only on the renderers that lease a context (the EasyGL family and OPENGL4).
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Deep dives
- Renderer contract
- Known issues
- Functional gap index