CNA-GAP-071: Only EasyGL leases its context to other threads: on every other renderer, graphics resources created on a loading thread have no CNA-level exclusion against the frame
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 c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
GraphicsDevice's resource registry is now locked and EasyGL leases its context to resource creation on any thread, but every other renderer returns no lease and has no synchronization of its own, while XNA 4.0 creates its device D3DCREATE_MULTITHREADED.
- Identifier
CNA-GAP-071- Category
- Functional gap
- Subsystem
- Graphics & renderers
- Status
- Narrowed (partially fixed; describes only what survives)
- Verified against
- CNA
c1c316b9(c1c316b9c7a846ce8002809c151fcd1af14942c9) - 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, and the game thread may wait for that thread. RendererThreadContextLease.hpp says renderer families that return no lease 'support concurrent resource creation directly or own a different synchronization boundary'.
Actual behaviour at TARGET
GraphicsDevice's resource registry is locked: AddResourceReference, RemoveResourceReference and TransferResourceReference take resourcesMutex_ (GraphicsDevice.cpp, KF-1a). On EasyGL, resource construction, SetData, GetData and Dispose of textures, render targets, vertex and index buffers take the renderer's context lease on whatever thread they run (GraphicsResource.cpp, LeaseRendererContext, KF-1), ContentReader holds it for a whole asset, and the game thread holds it for Initialize, BeginRun, every Tick and EndRun (Game.cpp, AcquireGameThreadContextLease, KF-16), so a loading thread works between frames. Only EasyGLRenderer implements AcquireThreadContextLeaseEXT; VULKAN, the Direct3D renderers, SDL_GPU, WEBGPU, SDL_RENDERER and SOFTWARE return no lease, and their 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'. The lease contract nevertheless still claims that lease-less renderers 'support concurrent resource creation directly or own a different synchronization boundary'. On EasyGL itself, a game thread that waits inside LoadContent, Update or Draw for a thread that is creating graphics resources deadlocks, because it still holds the context (opengl33-renderer.md); XNA 4.0 allows that wait.
Fixed since first recorded: the registry lock (KF-1a) and the EasyGL lease on resource construction, data transfer and disposal (KF-1, 00b9c9898) and on the whole frame (KF-16, a033a9184); native device construction and teardown are serialized (BINDFIX-050, 5b4edd6cc).
Source locations
modules/graphics/src/Xna/GraphicsDevice.cpp— AddResourceReference, RemoveResourceReference and TransferResourceReference under resourcesMutex_; NativeDeviceLifecycleMutexmodules/graphics/src/Xna/GraphicsResource.cpp— GraphicsResource registry calls and LeaseRendererContextmodules/graphics/include/CNA/Internal/Renderers/Common/RendererThreadContextLease.hpp— the lease contract and its claim about lease-less renderersmodules/graphics/include/CNA/Internal/Renderers/Common/IGraphicsRenderer.hpp— AcquireThreadContextLeaseEXT default (no lease)modules/content/src/Xna/ContentReader.cpp— ContentReader constructor takes the leasemodules/runtime/src/Game.cpp— AcquireGameThreadContextLease around Initialize, BeginRun, each Tick and EndRunmodules/renderers/easygl/src/EasyGLRenderer.cpp— AcquireThreadContextLeaseEXT, the only implementermodules/renderers/easygl/examples/easygl_worker_thread_resources_test.cpp— two workers create resources while the game runs frames (EasyGL only)modules/renderers/vulkan/examples/vulkan_thread_context_lease_test.cpp— header: a null lease is not a thread-safety guarantee, the renderer has no synchronizationdocs/opengl33-renderer.md— Graphics resources on other threads: the game-thread wait that deadlocks
Evidence
Read at c1c316b9; 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.
Re-verified at b0e97bb1: read the registry lock and LeaseRendererContext callers in modules/graphics/src, AcquireGameThreadContextLease in Game.cpp, every implementer of AcquireThreadContextLeaseEXT (EasyGL only) and the threading section of opengl33-renderer.md; not executed. The entry is narrowed to what survives KF-1, KF-1a, KF-16 and BINDFIX-050.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
EasyGL_WorkerThreadResources (easygl_worker_thread_resources_test.cpp) creates textures, vertex buffers and render targets on two workers while the game runs frames (EasyGL only); the Vulkan lease test asserts a null lease; nothing creates resources off the game thread on any other renderer.
Regression test
Create and dispose resources on a worker while the game draws, on each renderer other than EasyGL, under ThreadSanitizer; or document that this is supported only on EasyGL and correct the lease contract's claim.
Blast radius
Games that create resources or load content on a loading thread on renderers other than EasyGL, and games that wait on the game thread for such a loader on EasyGL.
Workaround
On renderers other than the EasyGL family, create graphics resources on the game thread or marshal loads to it. On EasyGL, start the loading work, keep running frames and pick the results up when the loader reports that it is done.
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