dotnet/wpf · critical · InvalidOperationException

SR.Image_LockCountLimit

Error message

SR.Image_LockCountLimit

What it means

D3DImage.LockImpl reference-counts locks in a UInt32 (_lockCount). If the count has already reached UInt32.MaxValue, acquiring one more lock would overflow, so it throws InvalidOperationException(Image_LockCountLimit). In practice this means lock/unlock pairs are severely unbalanced — the count should return to 0 after each frame.

Solutions

  1. Audit all Lock/TryLock call sites and ensure every one has a matching Unlock in a finally block; fix the leak — the counter should oscillate between 0 and small values.
  2. Add instrumentation/logging around Lock/Unlock to detect a monotonically growing count during development.
  3. Restart/reset the rendering component if the counter already saturated; the state cannot be corrected incrementally.

Example fix

// before
d3dImage.Lock();
if (frameReady) { Render(); d3dImage.Unlock(); } // Unlock skipped when !frameReady -> leak

// after
d3dImage.Lock();
try { if (frameReady) Render(); }
finally { d3dImage.Unlock(); }
Defensive patterns

Strategy: try-catch

Validate before calling

// Track lock balance in debug builds
Debug.Assert(lockDepth == 0 || lockDepth < 100, "Lock count growing without unlocks — leak suspected");

Try / catch

try { d3dImage.Lock(); /* render */ }
catch (InvalidOperationException ex) when (ex.Message.Contains("limit"))
{
    // unrecoverable counter saturation: recreate the D3DImage
}

Prevention

When it happens

Trigger: Calling Lock() or TryLock() millions of times without matching Unlock() calls; render loops that lock every frame but unlock only under a rarely-taken branch; a leaked-lock bug where exceptions skip the Unlock until the 32-bit counter saturates.

Common situations: Long-running rendering applications with a try/finally-less lock path; refactors that moved Unlock behind a conditional; deadlock-recovery code that force-unlocks in one place while other paths keep locking.

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/fa228f79c6a3c0bd. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/D3DImage.cs:587

            {
                MediaContext mediaContext = MediaContext.From(Dispatcher);
                mediaContext.CommittingBatch -= _sendPresentDelegate;
                _isWaitingForPresent = false;
            }
        }

        /// <summary>
        ///     Lock implementation shared by Lock and TryLock
        /// </summary>
        private bool LockImpl(Duration timeout)
        {
            Debug.Assert(timeout != Duration.Automatic);
            
            bool lockObtained = false;

            if (_lockCount == UInt32.MaxValue)
            {
                throw new InvalidOperationException(SR.Image_LockCountLimit);
            }
            
            if (_lockCount == 0)
            {
                if (timeout == Duration.Forever)
                {
                    lockObtained = _canWriteEvent.WaitOne();
                }
                else
                {
                    lockObtained = _canWriteEvent.WaitOne(timeout.TimeSpan, false);
                }
                
                // Consider the situation: Lock(); AddDirtyRect(); Unlock(); Lock(); return;
                // The Unlock will have set us up to send a present packet but since
                // the user re-locked the buffer we shouldn't copy forward
                UnsubscribeFromCommittingBatch();
            }

View on GitHub (pinned to 81131a70a4)