Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

WPF Hosting SharpDX

5.00/5 (10 votes)
27 Jul 2023MIT8 min read 7.9K   347  
Examples of WPF applications using DirectX11 managed code (SharpDX and C#) based on Microsoft interop solution
Microsoft provides WPF DirectX Extensions that allows to easily host DirectX 10 and DirectX 11 C++ content in WPF applications. This article presents how to use this package with managed code (SharpDX and C#). It also give an example and a few indications for the porting of existing WinForms / SharpDx applications into a WPF application.

Sample Image - maximum width is 600 pixels

Introduction

Some time ago, I did a few searches in Google in order to find how to host SharpDX Direct11 developments in a WPF app. I could find many opinions on this topic, varying from "it is not possible" to "it might be possible" and to "it may be possible" and even "My brother in law did it, so, it is possible". But, so far, I could not find any working example of code showing how to do it. My first thought was that somebody must absolutely do this. My second thought was that, perhaps, this somebody was me. So, here we go.

I know that SharpDX is no longer maintained but it still works pretty well and many of us have written tons of lines of code that use it. So I think this little work is worth being shared.

Microsoft is currently diffusing the package WPF DirectX Extensions that allows to easily host DirectX 10 and DirectX 11 content in WPF applications.

The Visual Studio solution that you can download with the present article contains a small library allowing to host a SharpDX content into a WPF application using Microsoft WPF DirectX Extensions.

The solution also contains an example of the porting of a SharpDX-Rastertek-Tutorials sample from WinForms to WPF. This is to illustrate how the library can be used to convert existing WinForms projects.

Background

Microsoft WPF DirectX Extensions can be downloaded from Github at https://github.com/microsoft/WPFDXInterop. It contains an unmanaged DLL named Microsoft.Wpf.Interop.DirectX.dll that you have to reference in your WPF app. Note that it comes in two flavors: x86 and x64. The x64 DLL is already included in the projects attached to this article, so you don't have to install it to run the examples.

In MS example included in WPF DirectX Extensions, the D3D11 code is packed in a C++ DLL. This sample of DLL is named D3D11Visualization. The start of this article was the rewriting, using C# and SharpDX, of this sample. This will be our first example.

The general principle of Microsoft WPF DirectX Extensions is that it allows us to incorporate D3DImage into your WPF window. Basically, a D3DImage is a Direct3D9 image. This image used a Texture2D memory structure to store its data. It happens that this memory structure is the same as the D3D11 Texture2D where we can render a SharpDX scene. So if we are able to create a D3D11 Texture2D that uses the same resource (a memory slice) as our D3DImage, we are done.

Another resource that we will use here is SharpDX-Rastertek-Tutorials-master. For those how don't know it, it is a package composed of tutorials about the use of SharpDX to build graphics applications under WinForm. You can download SharpDX-Rastertek-Tutorials-master from Github here. We will use RasterTek as a source for porting WinForm SharpDX application to WPF.

The Framework Structure

Sample Image - maximum width is 600 pixels

The framework is based on three layers:

  • The WPF app. It is the native WPF code with the declaration of a D3DImage and some code to have it rendered as a DirectX scene.
  • DSharpDxInterop: This is a bridge that allows the exchanges between the WPF app and the SharpDX content. It is contained in the WpfSharpDXLib. The C# code of this library is included in the solution repository, so you may change it to your taste if needed.
  • A graphics class, this is where your SharpDX content is. It must implement the IGraphics interface, also a member of WpfSharpDXLib.

Vertically, the diagram shows two distinct phases: Initialization and Render.

  • The initialization phase: During the phase, we will set a render loop on our D3DImage in the WPF app.
  • The Render phase: It is during this phase that our bridge class will cast the D3DImage to a D3D11 Texture2D. This Texture2D will be used to create a RenderTargetView. This way, the scene that we draw into the RenderTargetView will be displayed in the WPF D3DImage.

In the following, we will study the detail of implementation of each of these layers.

Walking through the Code: WpfSharpDXInterop_Master

We will analyze the first project, WpfSharpDXInterop_Master. It displays a simple rotating colored cube in a WPF application.

The WPF Application: MainWindow.xaml

This part is a basic WPF app. We have to add a namespace and a reference to the dxExtensions that corresponds to Microsoft.Wpf.Interop.DirectX.dll.

C#
xmlns:dxExtensions="clr-namespace:Microsoft.Wpf.Interop.DirectX;
                    assembly=Microsoft.Wpf.Interop.DirectX"

In our design, we must provide a panel to host our scene. Here, we use a Grid panel named Host that contains an image. The source of this image, InteropImage, will be provided using MS dxExtensions.

XML
<Grid Name="Host" Background="DimGray">
    <Image Stretch="Fill" Name="ImageHost" Margin="0">
        <Image.Source>
            <dxExtensions:D3D11Image x:Name="InteropImage" />
        </Image.Source>
    </Image>
</Grid>

The WPF Application: MainWindow.xaml.cs

The interesting point here is how to set a render loop on InteropImage. The CompositionTarget object provides the ability to create custom animations based on a per-frame callback. We will use it to render a DirectX scene.

C#
CompositionTarget.Rendering += CompositionTarget_Rendering;
            ...
void CompositionTarget_Rendering(object? sender, EventArgs e)
{
    RenderingEventArgs args = (RenderingEventArgs)e;
    // It's possible for Rendering to call back twice in the same frame 
    // so only render when we haven't already rendered in this frame.
    if (this._lastRender != args.RenderingTime)
    {
        InteropImage.RequestRender();
        this._lastRender = args.RenderingTime;
    }
}

Now, we will associate an action (DoRender) to the Render loop:

C#
private void InitializeRendering()
      {
          ...
          InteropImage.OnRender = DoRender;
          ...
      }

private void DoRender(IntPtr surface, bool isNewSurface)
      {
          if (Render(surface, isNewSurface)) return;
          ...
      }

We don't have to worry about synchronization, front and back buffers and so on, because the mechanism of CompositionTarget takes care of all this for us. The only point that we have to take into account is to stop the rendering when the front buffer is not available. This may occur when the computer is in sleep state or when the login screen is displayed, for instance.

C#
public MainWindow(bool lastVisible)
{
    InitializeComponent();
    ...
    InteropImage.IsFrontBufferAvailableChanged += FrontBufferAvailableChanged;
    ...
}

private void FrontBufferAvailableChanged
                (object sender, DependencyPropertyChangedEventArgs e)
{
    bool isFrontBufferAvailable = (bool)e.NewValue;
    if (!isFrontBufferAvailable)
    {
        InteropImage.OnRender = null;
    }
    else
    {
        _doRefresh = true; // force the creation of a renderTargetView 
                           // with the new buffer.
        InteropImage.OnRender = this.DoRender;
    }
}

Remark the _graphics object (the SharpDX development), instantiated from IGraphics and initialized via the WpfSharpDXInterop.

C#
public MainWindow()
{
    InitializeComponent();
    // Instantiate the graphics class. 
    ...
    _graphics = new DGraphics();
    InitSharpDxInterop();
}

WpfSharpDXInterop Class

This managed class is a bridge between WPF and SharpDX graphics application. It is a member of the WpfSharpDXLib provided within the solution repository.

Initialization

The initialization method is called by the WPF app. It receives a reference to the graphics instance. The method create the D3D11 Device and passes them to the graphics instance.

Render

The part of interest is the Render method. The first thing done here is to check if the RenderTargetView needs to be created. This happens the first time when we start the program. It may also occur with some events:

  • When the window is resized
  • If the login screen is called (Ctrl+Alt+Del)
  • If the computer was in sleep state and is restarted ...

The creation of a new RenderTargetView starts with the casting of the WPF D3DImage with a D3D11 Texture2D.

  • First, we get a handle on the DirectX9 Texture2D.
  • Then, we use this handle to create a D3D11 Texture2D linked to the same resource.
C#
// Get a handle to the DirectX9 texture2D
using Resource d3dResource = (Resource)resourcePointer;
IntPtr renderTextureHandle = d3dResource.QueryInterface<Resource>().SharedHandle;

// use this handle to create a D3D11 Texture2D linked to the same resource.
using SharpDX.Direct3D11.Resource d3d11Resource1 = 
  _device.OpenSharedResource<SharpDX.Direct3D11.Resource>(renderTextureHandle);
var texture2D = (Texture2D)d3d11Resource1.NativePointer;

Now, we dispose of D3D11 Texture2D where the SharpDX process can render the scene. Considering that the same resource is equally the WPF image source, we have built our bridge between WPF and SharpDX. We can build the RenderTargetView based.

C#
// Now, we can create the RenderTargetView
RenderTargetViewDescription targetDescription = new()
{
    Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
    Dimension = RenderTargetViewDimension.Texture2D
};
targetDescription.Texture2D.MipSlice = 0;

RenderTargetView = new RenderTargetView(_device, texture2D, targetDescription);

// Set render target view to interOpImage
_deviceContext.OutputMerger.SetRenderTargets(renderTargetView: RenderTargetView);

Utilities

The class also provides a couple of utility classes:

  • UpdateViewPort method allows to change the angle of view and clipping distances. Useful for zooming effects.
  • GetPixelSize method that computes the size in pixel of the image to fill the host. Needed by the WPF app.

The SharpDX Graphics Application

This is the class instantiated by the WPF app and initialized by WpfSharpDXInterop.Initialize(...) This is where we put D3D11 content such as models, textures, shaders, animations... A constraint is that this class must implement the interface IGraphics, equally as included in the WpfSharpDXLib. Another constraint is that the class must do its final rendering into the renderTargetView provided by WpfSharpDXInterop (see Render method below).

C#
public interface IGraphics
   {
       public bool Initialize(Device device, DeviceContext deviceContext);
       public bool Render(RenderTargetView renderTargetView,
              Matrix projectionMatrix, int surfaceWidth, int surfaceHeight);
   }

Porting a WinForm SharpDx to WPF: WpfSharpDXInterop_Tutorial7

The second project of the solution, WpfSharpDXInterop_Tutorial7, is a WPF version of the SharpDX Rastertek Tutorials #7. Actually, it is very similar to the previous example but the cube is textured and lighted.

Sample Image - maximum width is 600 pixels

If you want to try the porting by yourself, first you have to download the SharpDX-Rastertek-Tutorials-master from Github here.

Here is a step by step conduct line for the porting.

  • Copy the project WpfSharpDXInterop_Master into a new WPF solution.
  • Suppress the class DGraphics - We will replace it with code from RasterTek.
  • From the original SharpDX_RasterTek project, copy the files contained in the Graphics folder into your WPF project Graphics folder.
  • Delete the DDX11Class. It is replaced by our DSharpDxInterop class.

Open the class DGraphics:

  • Lets it implement the IGraphics interface. You will need to implement Device and DeviceContext objects and initialize them from the Initialize method.
  • D3D doesn't exist anymore => suppress anything related to the creation and initialization of this class.
  • Suppress Timer and other stuff that you don't need.
  • DSystemConfiguration doesn't exist anymore => create a new class to host necessary constants (access paths...).
  • windowsHandle is specific to the WindForm environment: suppress it from all classes in your project.
  • Replace:

    C#
    D3D.BeginScene(0f, 0f, 0f, 1f);

    with:

    C#
    //Clear the render buffer
    RawColor4 clearColor = new(0.00f, 0.00f, 0.0f, 0.0f);
    _deviceContext?.ClearRenderTargetView(renderTargetView, clearColor)			
  • Replace:

    C#
    D3D.EndScene();

    with:

    C#
    _deviceContext.Flush();

Finally, generate and ... fix the compilation errors. Don't panic, do it top down.

Before execution, make sure that all external files (shaders, models, textures ...) are copied where they must be according to the access paths.

Conclusion and Interest

I think that we can learn three things out of this article:

  • The first is how to cast a D3DImage into a D3D11 Texture2D with only four lines of C#.
  • The second is how to use MS WPF DirectX Extensions and our small library WpfSharpDXLib in order to host SharpDX managed code into a WPF application.
  • The last one, is how easy it is.

Regarding the migration of SharpDX code from WinForm to WPF. The important word is plan. Before merging tons of lines of code, study carefully how the different parts will interconnect. Write a plan of actions.

The next step for me will be to integrate this solution into my game project with a complex graphics structure.

History

  • 27th July, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License