dotnet/wpf · error · InvalidOperationException
SR.Image_MustBeLocked
Error message
SR.Image_MustBeLocked
What it means
D3DImage.SetBackBuffer requires the image to be in a locked state: a lock must have been acquired via Lock/TryLock and not yet released with Unlock. If _lockCount is 0, calling SetBackBuffer throws InvalidOperationException(Image_MustBeLocked). The lock ensures the CPU and the composition thread agree on when the back-buffer pointer can be swapped safely.
Solutions
- Acquire the lock first: call Lock() (or TryLock with a timeout and check the bool result) before SetBackBuffer, then Unlock when done.
- Keep every SetBackBuffer/AddDirtyRect call inside the same Lock/Unlock bracket.
- Ensure the Lock, SetBackBuffer, AddDirtyRect, Unlock sequence runs on one thread (the D3DImage's UI thread affinity).
Example fix
// before
d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface); // throws: not locked
// after
d3dImage.Lock();
try
{
d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface);
}
finally
{
d3dImage.Unlock();
} Defensive patterns
Strategy: validation
Validate before calling
if (!isLocked) { d3dImage.Lock(); isLocked = true; }
d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surfacePtr); Try / catch
try { d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, ptr); }
catch (InvalidOperationException ex) when (ex.Message.Contains("lock"))
{
d3dImage.Lock();
d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, ptr);
} Prevention
- Always wrap SetBackBuffer inside Lock/try/finally-Unlock.
- Keep lock state on one thread (the UI thread).
- Assign the back buffer once during setup, under lock.
When it happens
Trigger: Calling SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface) without a preceding Lock() or successful TryLock(); calling it after Unlock already released the last lock; calling it from a different thread than the one that took the lock.
Common situations: D3D/Direct3D9 interop render loops where the initial setup path assigns the back buffer before entering the lock/render/unlock cycle; races where another thread unlocked between checking and calling; forgetting the lock in a one-time back-buffer assignment.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- SR.D3DImage_MustHaveBackBuffer
- SR.Image_LockCountLimit
- 0x80070057
- Animation_Invalid_DefaultValue
- ArgumentNullException (buffer/sourceBuffer was IntPtr.Zero)
AI-assisted analysis of dotnet/wpf@81131a70a4 (2026-09-14).
Data as JSON: /api/errors/a595f0ca893d7d0f.
Report an issue: GitHub.
Appendix: source
Thrown at src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/D3DImage.cs:129
///
/// For best performance by type:
/// IDirect3DSurface9
/// Vista WDDM: non-lockable, created on IDirect3DDevice9Ex with
/// D3DDEVCAPS2_CAN_STRETCHRECT_FROM_TEXTURES and
/// D3DCAPS2_CANSHARERESOURCE support.
/// Vista XDDM: Doesn't matter. Software copying is fastest.
/// non-Vista: Lockable with GetDC support for the pixel format and
/// D3DDEVCAPS2_CAN_STRETCHRECT_FROM_TEXTURES support.
///
/// </summary>
public void SetBackBuffer(D3DResourceType backBufferType, IntPtr backBuffer, bool enableSoftwareFallback)
{
WritePreamble();
if (_lockCount == 0)
{
throw new InvalidOperationException(SR.Image_MustBeLocked);
}
// In case the user passed in something like "(D3DResourceType)-1"
if (backBufferType != D3DResourceType.IDirect3DSurface9)
{
throw new ArgumentOutOfRangeException(nameof(backBufferType));
}
// Early-out if the current back buffer equals the new one. If the front buffer
// is not available and software fallback is not enabled, _pUserSurfaceUnsafe
// will be null and this check will fail. We don't want a null backBuffer to
// early-out when the front buffer isn't available.
if (backBuffer != IntPtr.Zero && backBuffer == _pUserSurfaceUnsafe)
{
return;
}
SafeMILHandle newBitmap = null;View on GitHub (pinned to 81131a70a4)