Direct2D in WPF: what am I doing wrong?

3 days ago 12
ARTICLE AD BOX

I am using Vortice to draw Direct2D graphics in a WPF window.

I have been able to make it work using Vortice.Direct2D1.ID2D1Factory8.CreateHwndRenderTarget() , but this works with a HWND, so it suffers from airspace problems.

So, now I am trying to somehow make use of System.Windows.Interop.D3DImage, which has no airspace problems.

Why am I doing all this: To prevent answers of the kind "oh, you should go about it in a totally different way" let me just say I am doing this as an exercise. Now, if you care to know the real reason, I need to create a WPF control that shows a scrolling chart while consuming near-zero CPU. For that, I cannot use built-in WPF means of drawing, because WPF has no concept of scrolling, so on each frame it has to re-render everything, which of course consumes quite a bit of CPU. So, I have to use some rendering mechanism that allows scrolling the graphics surface and adding only the graphics that are being scrolled in. Direct2D is such a mechanism.

Unfortunately, the SetBackBuffer() method of D3DImage will accept nothing but an IDirect3DSurface9, so I have to somehow do Direct2D drawing on a Direct3D9 surface, but there appears to be no straightforward way of accomplishing this.

After a lot of research and trying various things, I have found rumors and tangible indications that this can be accomplished with the use of Direct3D11, DXGI, and "share handles". I found some code out there and adapted it to suit my needs, transcribing it from C++ to C# on the way, which is intended to work as follows:

Create a "shared" Vortice.Direct3D11.ID3D11Texture2D. (It works, in the sense that I do get a non-null share-handle.) Query-interface-cast the ID3D11Texture2D as an Vortice.DXGI.IDXGISurface. (It works.) Create a Vortice.Direct2D1.ID2D1RenderTarget from the IDXGISurface using Vortice.Direct2D1.ID2D1Factory.CreateDxgiSurfaceRenderTarget(). (It works.) Open a Vortice.Direct3D9.IDirect3DTexture9 from the ID3D11Texture2D using Vortice.Direct3D9.IDirect3DDevice9Ex.CreateTexture() and passing it the non-null share-handle. (Here is where I ran into trouble.) Set the IDirect3DTexture9 as the back-buffer of the System.Windows.Interop.D3DImage. From that moment on, keep drawing 2D graphics into the ID2D1RenderTarget which actually draws into the ID3D11Texture2D which is seen as a IDirect3DTexture9 by the D3DImage.

Unfortunately, the call to Vortice.Direct3D9.IDirect3DDevice9Ex.CreateTexture() fails with SharpGen.Runtime.SharpGenException: 'HRESULT: [0x80070057], Module: [Vortice.Direct2D1], ApiCode: [WINCODEC_ERR_INVALIDPARAMETER/InvalidParameter], Message: [The parameter is incorrect.]'

If I set the share-handle to zero right before invoking the function, then it does not fail, but of course then I get nothing rendered.

Can anyone please tell me what I am doing wrong?

Here is my source code, which is pretty close to a minimal reproducible example:

Experiment.Direct3dImageEx.csproj:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net9.0-windows</TargetFramework> <LangVersion>13</LangVersion> <OutputType>WinExe</OutputType> <Configurations>Debug;Release</Configurations> <UseWPF>true</UseWPF> </PropertyGroup> <ItemGroup> <PackageReference Include="Vortice.Direct2D1" Version="3.8.3" /> <PackageReference Include="Vortice.Direct3D9" Version="3.8.3" /> <PackageReference Include="Vortice.Direct3D11" Version="3.8.3" /> </ItemGroup> </Project>

MainWindow.xaml:

<Window x:Class="Experiment.Direct3dImageEx.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wpfInterop="clr-namespace:System.Windows.Interop;assembly=PresentationCore" xmlns:local="clr-namespace:Experiment.Direct3dImageEx" Title="Experiment.Direct3dImageEx"> <Grid> <Button Width="200" Height="80" Content="WPF Button" VerticalAlignment="Top" Margin="0,200,0,0"/> <Image Stretch="Fill" Name="ImageHost"> <Image.Source> <wpfInterop:D3DImage x:Name="TheD3dImage"/> </Image.Source> </Image> <Button Width="200" Content="WPF Button" Height="80" VerticalAlignment="Bottom" Margin="0,0,0,200"/> </Grid> </Window>

MainWindow.xaml.cs:

namespace Experiment.Direct3dImageEx; using Sys = System; using Wpf = System.Windows; using WpfMedia = System.Windows.Media; using WpfInterop = System.Windows.Interop; using D2d = Vortice.Direct2D1; using D3d = Vortice.Direct3D; using D3d9 = Vortice.Direct3D9; using D3d11 = Vortice.Direct3D11; using DCommon = Vortice.DCommon; using Dxgi = Vortice.DXGI; public partial class MainWindow : Wpf.Window { static readonly D2d.ID2D1Factory8 d2dFactory = D2d.D2D1.D2D1CreateFactory<D2d.ID2D1Factory8>(); static readonly D3d9.IDirect3D9Ex d3d9 = D3d9.D3D9.Direct3DCreate9Ex(); static readonly D3d11.ID3D11Device d3d11Device = D3d11.D3D11.D3D11CreateDevice( D3d.DriverType.Hardware, D3d11.DeviceCreationFlags.BgraSupport ); D3d9.IDirect3DDevice9Ex? d3d9Device; D3d11.ID3D11Texture2D? d3d11Texture2d; D2d.ID2D1RenderTarget? d2dRenderTarget; float colorValue; public MainWindow() { InitializeComponent(); Loaded += onLoaded; } void onLoaded( object sender, Wpf.RoutedEventArgs e ) { nint deviceWindowHandle = new WpfInterop.WindowInteropHelper( this ).Handle; d3d9Device = createD3d9Device( deviceWindowHandle ); (uint deviceWidth, uint deviceHeight) = getDeviceSize( this ); d3d11Texture2d = createSharedD3d11Texture2d( Dxgi.Format.B8G8R8A8_UNorm, deviceWidth, deviceHeight ); setBackBuffer( d3d11Texture2d ); d2dRenderTarget = createD2dRenderTarget( d3d11Texture2d ); WpfMedia.CompositionTarget.Rendering += onCompositionTargetRendering; } void setBackBuffer( D3d11.ID3D11Texture2D d3d11Texture2d ) { D3d9.IDirect3DTexture9 d3d9Texture = getSharedD3d9Texture( d3d9Device!, d3d11Texture2d ); D3d9.IDirect3DSurface9 d3d9Surface = d3d9Texture.GetSurfaceLevel( 0 ); TheD3dImage.Lock(); TheD3dImage.SetBackBuffer( WpfInterop.D3DResourceType.IDirect3DSurface9, d3d9Surface.NativePointer ); TheD3dImage.Unlock(); } static D3d9.IDirect3DTexture9 getSharedD3d9Texture( D3d9.IDirect3DDevice9Ex d3d9Device, D3d11.ID3D11Texture2D d3d11Texture2d ) { D3d11.Texture2DDescription textureDescription = d3d11Texture2d.Description; uint width = textureDescription.Width; uint height = textureDescription.Height; D3d9.Format d3d9Format = d3d9FormatFromDxgi( textureDescription.Format ); nint sharedHandle = d3d11Texture2d.QueryInterface<Dxgi.IDXGIResource>().SharedHandle; assert( sharedHandle != 0 ); // sharedHandle = 0; // this prevents failure but of course nothing renders then. // This fails with 'The parameter is incorrect.' return d3d9Device.CreateTexture( width, height, 1, D3d9.Usage.RenderTarget, d3d9Format, D3d9.Pool.Default, ref sharedHandle ); } static D3d9.Format d3d9FormatFromDxgi( Dxgi.Format format ) { assert( format == Dxgi.Format.B8G8R8A8_UNorm ); return D3d9.Format.A8R8G8B8; } static (uint deviceWidth, uint deviceHeight) getDeviceSize( Wpf.FrameworkElement frameworkElement ) { Wpf.DpiScale dpiScale = WpfMedia.VisualTreeHelper.GetDpi( frameworkElement ); uint deviceWidth = (uint)(frameworkElement.ActualWidth * dpiScale.DpiScaleX); uint deviceHeight = (uint)(frameworkElement.ActualHeight * dpiScale.DpiScaleY); return (deviceWidth, deviceHeight); } static D2d.ID2D1RenderTarget createD2dRenderTarget( D3d11.ID3D11Texture2D d3d11Texture2d ) { Dxgi.IDXGISurface dxgiSurface = d3d11Texture2d.QueryInterface<Dxgi.IDXGISurface>(); DCommon.PixelFormat pixelFormat = new( d3d11Texture2d.Description.Format, DCommon.AlphaMode.Premultiplied ); D2d.RenderTargetProperties renderTargetProperties = new( D2d.RenderTargetType.Hardware, pixelFormat, 96.0f, 96.0f, D2d.RenderTargetUsage.None, D2d.FeatureLevel.Default ); return d2dFactory.CreateDxgiSurfaceRenderTarget( dxgiSurface, renderTargetProperties ); } static D3d11.ID3D11Texture2D createSharedD3d11Texture2d( Dxgi.Format dxgiFormat, uint deviceWidth, uint deviceHeight ) { D3d11.Texture2DDescription textureDescription; textureDescription.ArraySize = 1; textureDescription.BindFlags = D3d11.BindFlags.RenderTarget /* | D3d11.BindFlags.ShaderResource*/; //D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; textureDescription.CPUAccessFlags = D3d11.CpuAccessFlags.None; textureDescription.Format = dxgiFormat; textureDescription.Width = deviceWidth; textureDescription.Height = deviceHeight; textureDescription.MipLevels = 1; textureDescription.SampleDescription = Dxgi.SampleDescription.Default; textureDescription.Usage = D3d11.ResourceUsage.Default; textureDescription.MiscFlags = D3d11.ResourceOptionFlags.Shared; D3d11.ID3D11Texture2D d3d11Texture2d = d3d11Device.CreateTexture2D( textureDescription ); assert( d3d11Texture2d.QueryInterface<Dxgi.IDXGIResource>().SharedHandle != 0 ); return d3d11Texture2d; } void onCompositionTargetRendering( object? sender, Sys.EventArgs e ) { Vortice.Mathematics.Color color = new( colorValue += 0.005f ); TheD3dImage.Lock(); d2dRenderTarget!.BeginDraw(); d2dRenderTarget.Clear( color ); d2dRenderTarget.EndDraw().CheckError(); TheD3dImage.AddDirtyRect( new Wpf.Int32Rect( 0, 0, TheD3dImage.PixelWidth, TheD3dImage.PixelHeight ) ); TheD3dImage.Unlock(); } static D3d9.IDirect3DDevice9Ex createD3d9Device( nint deviceWindowHandle ) { D3d9.PresentParameters presentParameters = new(); presentParameters.Windowed = true; presentParameters.SwapEffect = D3d9.SwapEffect.Discard; presentParameters.DeviceWindowHandle = deviceWindowHandle; presentParameters.PresentationInterval = D3d9.PresentInterval.Immediate; D3d9.CreateFlags creationFlags = D3d9.CreateFlags.HardwareVertexProcessing /*| D3d9.CreateFlags.Multithreaded*/ | D3d9.CreateFlags.FpuPreserve; return d3d9.CreateDeviceEx( 0 /*D3DADAPTER_DEFAULT*/, D3d9.DeviceType.Hardware, deviceWindowHandle, creationFlags, presentParameters ); } static void assert( bool x ) { if( !x ) throw new Sys.Exception(); } }
Read Entire Article