Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Known issue: WPF will throw COM Exception when create RenderTargetBitmap too fast #3067

Open
lindexi opened this issue May 29, 2020 · 17 comments
Labels
Enhancement Requested Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@lindexi
Copy link
Contributor

lindexi commented May 29, 2020

  • .NET Core Version: 3.1.300
  • Windows version: 10.0.18362
  • Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes

Problem description:

When we use RenderTargetBitmap to take the control screenshots, if our do it too frequent and too fast, then we will find that the framework throws an exception.

Actual behavior:

 System.Runtime.InteropServices.COMException: MILERR_WIN32ERROR

System.Runtime.InteropServices.COMException (0x88980003): MILERR_WIN32ERROR (Exception from HRESULT:0x88980003)
   in System.Windows.Media.Imaging.RenderTargetBitmap.FinalizeCreation()
   in System.Windows.Media.Imaging.RenderTargetBitmap..ctor(Int32 pixelWidth, Int32 pixelHeight, Double dpiX, Double dpiY, PixelFormat pixelFormat)

Expected behavior:

No exception

Minimal repro:

https://github.com/dotnet-campus/wpf-issues/tree/master/RenderTargetBitmapThrowsCOMExceptionWhenCreatedTooFast

@elyoh
Copy link

elyoh commented May 29, 2020

My understanding of this issue is that one or more GDI handles are allocated by each RenderTargetBitmap and there is no reliable way to ensure they are cleaned up quickly once a RenderTargetBitmap goes out of scope.

According to the documentation there is a theoretical limit of 65,536 GDI handles per session and this is set to 10,000 per process in the registry.

I have hit the issue when doing lots of image tiling operations in a short time. I was able to work around it by forcing the garbage collector to clean up before there was a chance of hitting this limit. It is far from ideal as the call to GC.Collect() can incur a performance hit.

GC.Collect();
GC.WaitForPendingFinalizers();

@lindexi
Copy link
Contributor Author

lindexi commented May 30, 2020

@elyoh Thank you. The RenderTargetBitmap will allocate come GDI handle and clean up quickly. The RenderTargetBitmap will allocate come GDI handle in Render method and clean it in Render method.

public void Render(Visual visual)
{
BitmapVisualManager bmv = new BitmapVisualManager(this);
bmv.Render(visual); // Render indirectly calls RenderTargetContentsChanged();
}

I use TaskManager and found that there are less than a thousand GDI objects in this process.

@elyoh
Copy link

elyoh commented May 30, 2020

I modified your sample code by counting the GDI handles at the point when the exception is thrown:

[System.Runtime.InteropServices.DllImport("User32")]
private extern static int GetGuiResources(IntPtr hProcess, int uiFlags);

private static int GetGDIHandleCount()
{
     using (var process = System.Diagnostics.Process.GetCurrentProcess())
     {
          var gdiHandles = GetGuiResources(process.Handle, 0);
          return Convert.ToInt32(gdiHandles);
     }
}
// RenderTargetBitmap throws COM exception when created too fast: MILERR_WIN32ERROR (Exception from HRESULT: 0x88980003)
// The count is always equal to the per-process GDI handle limit in the following registry key:
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota
Console.WriteLine("GDI handles {0}: ", GetGDIHandleCount());
Console.WriteLine(e);

It always yields the following output when the exception is thrown:

GDI handles 10000: 
System.Runtime.InteropServices.COMException (0x88980003): MILERR_WIN32ERROR (Exception from HRESULT: 0x88980003)
   at System.Windows.Media.Imaging.RenderTargetBitmap.FinalizeCreation()

In other words, FinalizeCreation() only throws this exception when the GDI handle count reaches the per-process limit. The cause is that release of these GDI handles is not immediate, so there is no guarantee that there will be enough GDI handles left when a new RenderTargetBitmap object is created. Subsequent use of RenderTargetBitmap only succeeds once the GDI handle count has dropped below the per-process limit again.

The call to GC.WaitForPendingFinalizers(); in the original sample just reduces the probability of reaching the per-process limit before handles have been released. Increasing the delay in the task creation also helps. It is certainly hardware dependent. In fact, I had to remove the call to GC.WaitForPendingFinalizers(); and decrease the delay to get the exception to throw without running the sample for quite some time.

@miloush
Copy link
Contributor

miloush commented Jun 3, 2020

Yeah, from my experience if you are running a x64 process (not the default setting anymore) and have plenty of RAM, the finalizers do eventually start kicking in.

As for fix, the BitmapSource should be IDisposable and release the handle/IWICBitmap on disposal rather than in a finalizer.

@fabiant3 fabiant3 added the Enhancement Requested Product code improvement that does NOT require public API changes/additions label Jun 3, 2020
@fabiant3 fabiant3 added this to the Future milestone Jun 3, 2020
@lamposu
Copy link

lamposu commented Apr 2, 2021

Any news? help...

1 similar comment
@lamposu
Copy link

lamposu commented Apr 2, 2021

Any news? help...

@lindexi
Copy link
Contributor Author

lindexi commented Apr 7, 2021

Any news? help...

@lamposu
This problem hard to be fixed

@matthew-a-thomas
Copy link

Bump. I'm running into this issue when I use lots of RenderTargetBitmap instances in a parallel rendering scenario. GDI object count is hitting the upper limit of 10,000.

@matthew-a-thomas
Copy link

The GDI count increments by two every time this function here is invoked:
image

This is being invoked in a background dispatcher thread (a different thread than the main UI, but the thread is running a Dispatcher).

Here's my code:
image

@miloush
Copy link
Contributor

miloush commented Mar 10, 2022

@matthew-a-thomas The workaround is to invoke GC and wait for pending finalizers

@matthew-a-thomas
Copy link

matthew-a-thomas commented Mar 10, 2022

The workaround is to invoke GC and wait for pending finalizers

@miloush Thank you. Unfortunately in my case that won't do because it introduces noticeable lag and because it increases the size of the gen 2 heap.

Edit: I changed my mind. I just garbage collect when GDI handle is over a certain large number—you can P/Invoke this function to get GDI handle count—and then only every few seconds.

@lindexi
Copy link
Contributor Author

lindexi commented Mar 11, 2022

I think it is necessary to provide manual control, as @miloush says.

As for fix, the BitmapSource should be IDisposable and release the handle/IWICBitmap on disposal rather than in a finalizer.

@elyoh
Copy link

elyoh commented Mar 12, 2022

The only way I have found to reliably force the cleanup of the GDI handles and ensure memory use drops immediately is with the following hack:

// WPF Hack: Imaging - Force the RenderTargetBitmap to release memory handles immediately
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var fieldWicSourceHandle = typeof(RenderTargetBitmap).GetField("_wicSource", flags);
var fieldRenderTargetBitmap = typeof(RenderTargetBitmap).GetField("_renderTargetBitmap", flags);
var methodReleaseHandle = fieldRenderTargetBitmap.FieldType.GetMethod("ReleaseHandle", flags);
methodReleaseHandle.Invoke(fieldWicSourceHandle.GetValue(rtb), null);
methodReleaseHandle.Invoke(fieldRenderTargetBitmap.GetValue(rtb), null);
fieldWicSourceHandle.SetValue(rtb, null);
fieldRenderTargetBitmap.SetValue(rtb, null);
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

As @miloush and @lindexi note, BitmapSource should use IDisposable to release these resources and save developers this headache.

@JensNordenbro
Copy link

Please fix for NET6+ and NET48.

@lindexi
Copy link
Contributor Author

lindexi commented Apr 5, 2022

@JensNordenbro We are trying to find a solution, thank you.

@ulex
Copy link

ulex commented Oct 6, 2022

The workaround without forcing the GC is to manually release the handle in the RenderTargetBitmap using reflection:

(bitmap.GetType().GetField("_renderTargetBitmap", BindingFlags.Instance | BindingFlags.NonPublic)?
.GetValue(bitmap) as IDisposable)?.Dispose();

@pchaurasia14 pchaurasia14 changed the title Known issus: WPF will throw COM Exception when create RenderTargetBitmap too fast Known issue: WPF will throw COM Exception when create RenderTargetBitmap too fast Oct 6, 2022
@MauNguyenVan
Copy link
Contributor

MauNguyenVan commented Dec 17, 2023

in WPF 8.0. I still get this error when creating more than 5000 instances. Please inform me if you have new things about it

Err: System.Runtime.InteropServices.COMException (0x88980003): MILERR_WIN32ERROR (0x88980003) at System.Windows.Media.Imaging.RenderTargetBitmap.FinalizeCreation() at System.Windows.Media.Imaging.RenderTargetBitmap..ctor(Int32 pixelWidth, Int32 pixelHeight, Double dpiX, Double dpiY, PixelFormat pixelFormat)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement Requested Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

No branches or pull requests

9 participants