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
- 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.
- Add instrumentation/logging around Lock/Unlock to detect a monotonically growing count during development.
- 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
- Always pair Lock/TryLock with Unlock in a finally block.
- Log lock acquisitions/releases in debug builds to catch monotonic growth early.
- Recreate the D3DImage if a lock leak is detected; the counter cannot be reset.
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
- SR.D3DImage_MustHaveBackBuffer
- SR.Image_MustBeLocked
- Animation_Invalid_DefaultValue
- args
- Cannot remove signature from read-only file.
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)