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

CNA snapshot c1c316b9  ·  Known Issues › Functional gaps  ·  source links pinned to c1c316b9

✓

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

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.

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