dotnet/wpf · error · InvalidOperationException
SR.D3DImage_MustHaveBackBuffer
Error message
SR.D3DImage_MustHaveBackBuffer
What it means
After verifying the lock, D3DImage.AddDirtyRect requires that a back buffer has been assigned via SetBackBuffer; if _pInteropDeviceBitmap is null it throws InvalidOperationException(D3DImage_MustHaveBackBuffer). You cannot mark dirty regions on an image that has no back-buffer surface yet.
Solutions
- Call SetBackBuffer(D3DResourceType.IDirect3DSurface9, surfacePtr) with a valid surface before any AddDirtyRect call.
- Gate the render/dirty-rect code on 'back buffer assigned' state in your renderer; skip the frame otherwise.
- Do not call AddDirtyRect after SetBackBuffer(null); re-set the buffer before resuming rendering.
Example fix
// before
if (rendered)
d3dImage.AddDirtyRect(rect); // throws if no back buffer set yet
// after
if (_backBufferSet && rendered)
{
d3dImage.AddDirtyRect(rect);
}
// where _backBufferSet = true only after a successful SetBackBuffer with non-null pointer Defensive patterns
Strategy: validation
Validate before calling
if (backBufferPtr == IntPtr.Zero) return; // no back buffer; skip dirty rect d3dImage.AddDirtyRect(rect);
Type guard
bool HasBackBuffer(IntPtr ptr) => ptr != IntPtr.Zero;
Try / catch
try { d3dImage.AddDirtyRect(rect); }
catch (InvalidOperationException ex) when (ex.Message.Contains("back buffer"))
{
// buffer not yet set — set it under lock, then retry once
} Prevention
- Set the back buffer (non-null) before starting the render loop.
- Track back-buffer state and skip AddDirtyRect when it is unset or was set to null.
- During teardown, stop the render loop before calling SetBackBuffer(null).
When it happens
Trigger: Calling AddDirtyRect before the first successful SetBackBuffer call; calling it after SetBackBuffer(null) released the back buffer; calling it when TryLock succeeded but the front buffer is unavailable and software fallback left _pUserSurfaceUnsafe/_pInteropDeviceBitmap null; calling it during initialization before the D3D surface was created.
Common situations: Startup ordering bugs where the render loop runs before the back buffer is set; teardown races where the surface was released but the render callback still fires; cases where SetBackBuffer was passed null to detach but the dirty-rect path still executes.
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.Image_MustBeLocked
- 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/c90a10c6708e7b75.
Report an issue: GitHub.
Appendix: source
Thrown at src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/D3DImage.cs:298
/// the D3DImage. After you unlock, we will copy the dirty areas to the front buffer.
///
/// Can only be called while locked.
///
/// IMPORTANT: After five dirty rects, we will union them all together. This
/// means you must have valid data outside of the dirty regions.
/// </Summary>
public void AddDirtyRect(Int32Rect dirtyRect)
{
WritePreamble();
if (_lockCount == 0)
{
throw new InvalidOperationException(SR.Image_MustBeLocked);
}
if (_pInteropDeviceBitmap == null)
{
throw new InvalidOperationException(SR.D3DImage_MustHaveBackBuffer);
}
dirtyRect.ValidateForDirtyRect(nameof(dirtyRect), PixelWidth, PixelHeight);
if (dirtyRect.HasArea)
{
// Unmanaged code will make sure that the rect is well-formed
HRESULT.Check(UnsafeNativeMethods.InteropDeviceBitmap.AddDirtyRect(
dirtyRect.X,
dirtyRect.Y,
dirtyRect.Width,
dirtyRect.Height,
_pInteropDeviceBitmap
));
// We're now dirty, but we won't consider it a change until Unlock
_isDirty = true;
_isChangePending = true;
}View on GitHub (pinned to 81131a70a4)