smourier/DirectN

ID3D11Device is not thread-safe

Closed this issue · 9 comments

Hi,
At first, thanks for creating DirectN and currently, I used DirectN for my project.
As I know ID3D11Device is thread-safe and ID3D11DeviceContext is not, but when I use ID3D11Device to create texture or any buffer in other thread, it meets the error:

'Unable to cast COM object of type 'System.__ComObject' to interface type 'DirectN.ID3D11Device'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{DB6F6DDB-AC77-4E88-8253-819DF9BBF140}' failed due to the following error: No such interface supported (0x80004002

I can work around by just using a single thread, but I'd like to use multithread in my case.
Am I missing something on this?

Thank you.

Hi,

Indeed, this is the typical marshaling error message when you want to pass an object that has no proxy/stub defined between thread.

Do you have some more reproducing code?

Hi,
Here're reproducing code:

    public sealed partial class MainWindow : Window
    {
        private IComObject<ID3D11Device> _d3dDevice;
        private IComObject<ID3D11DeviceContext> _d3dDeviceContext;
        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            myButton.Content = "Clicked";

            //Task.Run(() =>
            //{
                var flags = D3D11_CREATE_DEVICE_FLAG.D3D11_CREATE_DEVICE_BGRA_SUPPORT;
                var fac = DXGIFunctions.CreateDXGIFactory2();
                _d3dDevice = D3D11Functions.D3D11CreateDevice(null, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, flags, out _d3dDeviceContext);
                var mt = _d3dDevice.As<ID3D11Multithread>();
                mt?.SetMultithreadProtected(true);
            //});

            System.Threading.Thread.Sleep(2000);
            Task.Run(() =>
            {
                var access = _d3dDevice.Object;   // Error at here
            });
        }
    }

However, If the _d3dDevice is not created in UI thread, but in another thread instead (uncomment Task.Run above), it will work well. So, is it a expected behavior?

What framework are you using .NET Framework? .NET Core? 5, 6?

I used .Net6

Ok, I can reproduce, and here is what's happening.

First of all, this is not related to DirectN but only to .NET.

What happens is ID3D11Device doesn't declare how it behaves with regards to COM threading rules (it's more "nano" COM than full COM), so it's considered MTA by default by .NET. So, for .NET (for the RCW wrapper), it must be marshaled when used from one apartment to another, see here https://docs.microsoft.com/en-us/dotnet/framework/interop/interop-marshalling

The problem is ID3D11Device registers no way to be marshaled because... it doesn't need to be! Hence the .NET error (0x80004002)

In your reproducing code, since you're creating the device from the main WPF or Winforms thread, the thread that creates the D3D11 device is marked as STA. When you want to use this object from another thread, .NET will try to marshal the device object reference and fails.

One solution is to create the device in an MTA thread and keep using it in MTA threads:

  private void Button_Click(object sender, RoutedEventArgs e)
  {
      // we're running in an STA here
      Task.Run(() =>
      {
          // we're running in an MTA here
          var flags = D3D11_CREATE_DEVICE_FLAG.D3D11_CREATE_DEVICE_BGRA_SUPPORT;
          _d3dDevice = D3D11Functions.D3D11CreateDevice(null, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, flags, out _);
          Task.Run(() =>
          {
              // we're also running in an MTA here
              var access = _d3dDevice.Object; // works
          });
      });
  }

Note you won't be able to use the D3D11 device from an STA thread with .NET.

Thanks, I understood.
In my case, I also need to bind swapChainPanel UI element (which must be run on STA thread) with swapchain:

                var nativePanel = _swapChainPanel.As<ISwapChainPanelNative>();
                nativePanel.SetSwapChain(swapChain?.Object); //Error if I create swapchain on MTA threads

so it seems that I cannot use with MTA threads.

What you should be able to do in this case if it's just for passing objects around, is avoid using interface .NET objects, but use IntPtr variables which are opaque pointers to .NET.

So, convert your objects into IntPtr using Marshal.GetIUnknownForObject (or modify interface to get back IntPtr directly) and you should also modify interfaces you use in this case (like ISwapChainPanelNative) so they use IntPtr too.

Thanks, I will try with that

See also this for the reasons why it doesn't work (and will never) with COM interop interface : dotnet/runtime#69979