dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
6.84k stars 1.13k forks source link

One way to fix UCEERR_RENDERTHREADFAILURE #9042

Open vladimir-cheverdyuk-altium opened 3 weeks ago

vladimir-cheverdyuk-altium commented 3 weeks ago

I know that are many reasons for this error but I found that for my case it was related to using D3DImage.

Here is link to the project that demonstrates problem : https://github.com/vladimir-cheverdyuk-altium/dx9host Basically it is Microsoft code copied from here: https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/walkthrough-creating-direct3d9-content-for-hosting-in-wpf?view=netframeworkdesktop-4.8 https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/walkthrough-hosting-direct3d9-content-in-wpf?view=netframeworkdesktop-4.8

It has project for .NET Framework and .NET6.

Steps:

  1. You need 2 monitors with different screen resolution.
  2. Start app on main monitor and maximize it
  3. Power off this monitor or change display settings to display only on second monitor
  4. Windows will move the application to the second monitor and it will crash with UCEERR_RENDERTHREADFAILURE

Source of the problem It looks like IDirect3D9.GetAdapterMonitor will return 0 because monitor is powered off (or unavailable) and function pDisplaySet->GetDisplayIndexFromMonitor(hMon, uDisplayIndex) in CInteropDeviceBitmap::GetDisplayFromUserDevice will return E_INVALIDARG and this eventually will lead to crash.

So to me it looks like issue is in D3DImage that connected to adapter 0 while adapter 0 is disconnected from monitor.

**Fix*** To fix it I declared private EventHandler renderingEventHandler; in MainWindow.xaml.cs then set rendering like this:

            renderingEventHandler = new EventHandler(CompositionTarget_Rendering);
            CompositionTarget.Rendering += renderingEventHandler;

and add these 2 lines to the end of constructor:

            Microsoft.Win32.SystemEvents.DisplaySettingsChanging += DisplaySettingsChanging;
            Microsoft.Win32.SystemEvents.DisplaySettingsChanged += DisplaySettingsChanged;

and lastly I added implementation:

        private void DisplaySettingsChanging(object sender, EventArgs e)
        {
            CompositionTarget.Rendering -= renderingEventHandler;
            d3dimg.Lock();
            d3dimg.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero, true);
            d3dimg.Unlock();
        }

        private void DisplaySettingsChanged(object sender, EventArgs e)
        {
            POINT p = new POINT(imgelt.PointToScreen(new Point(0, 0)));

            HRESULT.Check(SetAdapter(p));

            CompositionTarget.Rendering += renderingEventHandler;
        }

and lastly I added 3rd parameter true to every call to SetBackBuffer and removed d3dimg.IsFrontBufferAvailable check in CompositionTarget_Rendering.

Does anybody know if this approach to detect monitor changes (using Microsoft.Win32.SystemEvents.DisplaySettingsChanging) is correct?

Just in case if somebody thing it is rare thing, we have 37933 crash reports from users for this issue. I'm not sure how many related to this particular reason but at least some are. Here is one of reports from our customers: "I also get this issue on my Lenovo laptop when connecting/disconnecting external monitors." Some customers state that it happened when computer is going to sleep or perhaps when monitors going to sleep but I couldn't able to reproduce it.

Const-me commented 2 days ago

Steps

Another test case which reproduces that crash.

  1. Use a laptop with connected HDMI external display, set display settings to “Extend”. DPI doesn’t matter.
  2. In the OS control panel, set “when I close the lid” = “Do nothing”.
  3. Run a .NET 4.8 WPF app which uses D3DImage on the internal monitor.
  4. Close the lid.

It looks like IDirect3D9.GetAdapterMonitor will return 0 because monitor is powered off (or unavailable)

I can confirm the source is correct.

Your workaround didn’t work for me because I don't want to render the 3D content at refresh rate of the monitor, but I was able to workaround using MinHook to replace IDirect3D9.GetAdapterMonitor API method for the current process, changing nullptr return value to the handle of the primary monitor.

Implementing the fix on the WPF side would be way simpler. Literally 2 lines of code in CInteropDeviceBitmap::GetDisplayFromUserDevice method, after the pID3DUserObject->GetAdapterMonitor line:

if( nullptr == hMon )
    hMon = ::MonitorFromWindow( nullptr, MONITOR_DEFAULTTOPRIMARY );