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

  1. Call SetBackBuffer(D3DResourceType.IDirect3DSurface9, surfacePtr) with a valid surface before any AddDirtyRect call.
  2. Gate the render/dirty-rect code on 'back buffer assigned' state in your renderer; skip the frame otherwise.
  3. 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

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


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)