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.
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
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.
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
.
<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.
CompositionTarget.Rendering += CompositionTarget_Rendering;
...
void CompositionTarget_Rendering(object? sender, EventArgs e)
{
RenderingEventArgs args = (RenderingEventArgs)e;
if (this._lastRender != args.RenderingTime)
{
InteropImage.RequestRender();
this._lastRender = args.RenderingTime;
}
}
Now, we will associate an action (DoRender
) to the Render
loop:
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.
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;
InteropImage.OnRender = this.DoRender;
}
}
Remark the _graphics
object (the SharpDX
development), instantiated from IGraphics
and initialized via the WpfSharpDXInterop
.
public MainWindow()
{
InitializeComponent();
...
_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.
using Resource d3dResource = (Resource)resourcePointer;
IntPtr renderTextureHandle = d3dResource.QueryInterface<Resource>().SharedHandle;
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.
RenderTargetViewDescription targetDescription = new()
{
Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
Dimension = RenderTargetViewDimension.Texture2D
};
targetDescription.Texture2D.MipSlice = 0;
RenderTargetView = new RenderTargetView(_device, texture2D, targetDescription);
_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).
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.
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:
D3D.BeginScene(0f, 0f, 0f, 1f);
with:
RawColor4 clearColor = new(0.00f, 0.00f, 0.0f, 0.0f);
_deviceContext?.ClearRenderTargetView(renderTargetView, clearColor)
-
Replace:
D3D.EndScene();
with:
_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