|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionDon't let the name fool you. The newest release of Windows Presentation Foundation (WPF), contained within Service Pack 1 of the Microsoft .NET Framework, version 3.5, is much more than just a service release. It contains several completely new and highly requested features, including things like hardware accelerated bitmap effects and a much better DirectX interop story. Most of the new features were described last May in Tim Sneath's original announcement for the beta release of SP1. The purpose of this article is to introduce one particular new feature called We will look at a very simple example of D3D interop in this article. Although our sample demonstrates a custom 3D scene, keep in mind that D3D is not just for rendering 3D content. Beginning with DirectX version 8, D3D has subsumed DirectDraw (the former 2D DirectX graphics technology). As such, D3D is now responsible for rendering both 2D and 3D graphics. Scope of this ArticleThe primary goal of this article is to present developers with the information that is needed to get started using In an effort to stay focused and keep the sample uncluttered, I have intentionally selected a very simple 3D scene. In fact, I have simply borrowed a scene from the sample in the DirectX SDK, entitled "Tutorial 3: Using Matrices". It contains nothing more than a multicolored triangle spinning about the Y axis. This was about the simplest D3D sample I could find, and it has been reduced even further for the purposes of this article. To be clear, this is not intended to be an example of quality D3D code. The unmanaged code is intentionally minimalistic. I have reduced it to a single code file. In this article, we will focus more on the usage of the What is an ImageSource?First things first... What, exactly, is an * "MIL object" refers to a managed object that is accessed directly by WPF's Media Integration Layer (MIL) via specific MIL interfaces (like The fact that we can create a This allows us to make a couple of brushes out of images like the following:
And, then do silly things like this:
XAML<Grid Background="{StaticResource IceBrush}">
<TextBlock Foreground="{StaticResource FireBrush}"
Text="Fire & Ice" />
</Grid>
Alright... not that compelling... I guess that's why I'll never be a designer. But in the hands of a talented designer, an D3DImage is an ImageSourceAs mentioned previously, Enter D3DImage... Exit Airspace RestrictionsThe Until now, if we needed to host an unmanaged D3D surface within our WPF application, we have been limited to a solution that involves hosting an These types of airspace restrictions represent a huge limitation in a framework, like WPF, where element composition is used to create very rich user experiences. With a Composing a Custom D3D Scene with the WPF SceneThe code sample included with this article demonstrates how to blend a custom D3D render target with the render target of a WPF application. Let's begin by running through the managed side of the sample (the WPF code) at a high level, and then, we'll take a closer look at some of the details. The WPF app begins by creating a All of the code to do this is contained within the public Window1()
{
// create a D3DImage to host the scene and
// monitor it for changes in front buffer availability
_di = new D3DImage();
_di.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged;
// make a brush of the scene available as a resource on the window
Resources["RotatingTriangleScene"] = new ImageBrush(_di);
// begin rendering the custom D3D scene into the D3DImage
BeginRenderingScene();
// parse the XAML
InitializeComponent();
}
We will examine the finer details of this routine momentarily. First, let's look at the markup for the WPF scene, which is contained within Window1.xaml and shown here: <Window x:Class="D3DImageSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="RootWindow" Title="Window1"
Height="300" Width="300">
<Window.Background>
<ImageBrush ImageSource="Forest.jpg" />
</Window.Background>
<Grid Background="{StaticResource RotatingTriangleScene}" />
</Window>
This XAML consists of nothing more than a When you build and run the app, you see something like the following (although these images do not do justice to the fidelity of the actual scene):
Thanks to WPF's support for layered windows, we could even take this a step further and get rid of the window chrome entirely. This would allow us to render the custom D3D content directly to the screen, as shown here:
Okay... okay... arguably, you would never opt to host a D3D scene in WPF unless you needed to blend it with WPF content... but, it's just cool that you can do this! Creating the D3DImageLet's take a closer look at the // create a D3DImage to host the scene and
// monitor it for changes in front buffer availability
_di = new D3DImage();
_di.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged;
// make a brush of the scene available as a resource on the window
Resources["RotatingTriangleScene"] = new ImageBrush(_di);
// begin rendering the custom D3D scene into the D3DImage
BeginRenderingScene();
After creating the object, we immediately establish a handler for the After creating the Knowing When to Update the SceneAs alluded to above, there are actually times when it makes sense to render our custom scene, and other times when it does not. Clearly, it does not make sense to update our scene more often than the WPF scene, itself, is updated. That would just be a waste of cycles. So at most, we should only update our scene during a render pass. Seasoned WPF developers know that the static private void BeginRenderingScene()
{
if (_di.IsFrontBufferAvailable)
{
// create a custom D3D scene and get a pointer to its surface
// (this is a call into our custom unmanaged library)
_scene = InitializeScene();
// set the back buffer using the new scene
_di.Lock();
_di.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _scene);
_di.Unlock();
// leverage the Rendering event of WPF's composition target to
// update the custom D3D scene
CompositionTarget.Rendering += OnRendering;
}
}
You probably noticed that this call is wrapped within a conditional But, if WPF loses its D3D device for any reason, then private void OnIsFrontBufferAvailableChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
// if the front buffer is available, then WPF has just created a new
// D3D device, so we need to start rendering our custom scene
if (_di.IsFrontBufferAvailable)
{
BeginRenderingScene();
}
else
{
// If the front buffer is no longer available, then WPF has lost
// its D3D device so there is no reason to waste cycles rendering
// our custom scene until a new device is created.
StopRenderingScene();
}
}
The routine to stop rendering the scene is fairly straightforward. It is basically the inverse of the earlier private void StopRenderingScene()
{
// This method is called when WPF loses its D3D device.
// In such a circumstance, it is very likely that we have lost
// our custom D3D device also, so we should just release the scene.
// We will create a new scene when a D3D device becomes
// available again.
CompositionTarget.Rendering -= OnRendering;
// release the scene
// (this is a call into our custom unmanaged library)
ReleaseScene();
_scene = IntPtr.Zero;
}
WPF will automatically create a new D3D device when the Operating System signals that it is once again possible. At that point, Side-note: If you're not a DirectX developer, you may be wondering why WPF would ever lose its D3D device. This is actually a pretty common occurrence, and can happen if the user performs a system operation that affects the video driver's resolution or color settings; or if the user presses Ctrl+Alt+Delete to invoke the WinLogon desktop; or if another application locks the screen; or if the user launches a full-screen D3D app; etc. Updating the Custom SceneIf our custom D3D scene involves animations, we will likely want to update it as part of a WPF render pass. Then, any changes made to our 3D surface can be integrated with WPF's surface. For this reason, we have established an private void OnRendering(object sender, EventArgs e)
{
// when WPF's composition target is about to render, we update our
// custom render target so that it can be blended with the WPF target
UpdateScene();
}
In our example, we will update the scene on every render pass. Note, however, that if you wanted to throttle the rate at which the custom scene is updated, you could certainly insert that logic into this To update our scene, we must perform the following four steps:
These steps can be seen in our private void UpdateScene()
{
if (_di.IsFrontBufferAvailable && _scene != IntPtr.Zero)
{
// lock the D3DImage
_di.Lock();
// update the scene
// (this is a call into our custom unmanaged library)
SIZE size = new SIZE();
RenderScene(size);
// invalidate the updated rect of the D3DImage (in this case, the
// whole image)
_di.AddDirtyRect(new Int32Rect(0, 0, size.Width, size.Height));
// unlock the D3DImage
_di.Unlock();
}
}
When the scene is rendered, WPF will check the Side-note: It should be noted that to support composition with the WPF scene, a flush of the D3D device is required. This will certainly have an impact on performance. The degree of that impact depends on both the scene being rendered and the video driver's cost for such a flush operation. As such, performance may vary on different hardware or even under different driver versions. A Brief Look at D3D Interop RequirementsWe have now covered the managed side of the equation. Hopefully, it all makes pretty good sense. Refer to Window1.xaml.cs for the full code file, including the interop (P/Invoke) declarations that are necessary to call into the unmanaged library. Before we move on to the unmanaged side of things, let's take a moment to look at some of the requirements for a well performing D3D interop scenario...
It should be noted up front that a general requirement, in case it is not immediately obvious, is that any application using Below are the specific requirements for a performant solution on a particular Operating System: Vista Requirements for a Performant D3DImage Solution
XP Requirements for a Performant D3DImage Solution
- OR -
On Vista, On either Operating System, the pixel format of the created surface must be either 32-bit RGB ( For hardware acceleration on XP, the surface must be created as lockable. On Vista, however, a non-lockable surface is preferable (although not required), as it is generally faster. On XP, the It is also worth noting that Keeping the above requirements in mind, let's now look at the native (unmanaged) code that is used to create our very simple D3D scene. Dynamically Creating a Simple D3D SceneYou may have noticed that our managed application calls into three routines within our unmanaged library: Only the Creating the DeviceOne of our requirements is that when running on Vista, we should create a D3D device that is capable of creating a shared surface. This can only be done using a D3D 9Ex device. So, the first order of business is to determine if the 9Ex functions are available. If so, we should initialize D3D using the Ex functions; otherwise, we can initialize D3D using the non-Ex functions. This is done in the // Vista requires the D3D "Ex" functions for optimal performance.
// The Ex functions are only supported with WDDM drivers, so they
// will not be available on XP. As such, we must use the D3D9Ex
// functions on Vista and the D3D9 functions on XP.
// Rather than query the OS version, we can simply check for the
// 9Ex functions, since this is ultimately what we care about.
HMODULE hD3D9 = LoadLibrary(TEXT("d3d9.dll"));
g_pfnCreate9Ex = (DIRECT3DCREATE9EX)GetProcAddress(hD3D9,
"Direct3DCreate9Ex");
g_is9Ex = (g_pfnCreate9Ex != NULL);
FreeLibrary(hD3D9);
if (g_is9Ex)
{
InitializeD3DEx(hWnd, d3dpp);
}
else
{
InitializeD3D(hWnd, d3dpp);
}
For simplicity, we have created two separate functions for initializing the D3D library and creating a device: It is worth noting that the function to create the 9Ex device is not even available within the D3D9 library on Windows XP. For this reason, the unmanaged code within our sample application does not contain a direct call to It should also be noted that in addition to obtaining the 9Ex interfaces, the What's up with the HWND?Getting back to the For completeness, it should be noted that there is a very small performance cost associated with this IntPtr hwnd = (PresentationSource.FromVisual(this) as HwndSource).Handle;
Of course, this approach requires that we wait for the Creating the Render Target SurfaceOnce we have the appropriate device, we are ready to create a render surface. Recall that our requirement for this surface is that it be lockable on XP, yet we prefer a non-lockable surface on Vista. Fortunately, the // create and set the render target surface
// it should be lockable on XP and nonlockable on Vista
if (FAILED(g_pd3dDevice->CreateRenderTarget(WIDTH, HEIGHT,
D3DFMT_A8R8G8B8, D3DMULTISAMPLE_NONE, 0,
!g_is9Ex, // lockable
&g_pd3dSurface, NULL)))
{
return NULL;
}
g_pd3dDevice->SetRenderTarget(0, g_pd3dSurface);
Note that in this sample, we are creating a 32-bit ARGB surface. On XP, this will require SP3 (or SP2 with the aforementioned layered windows patch). That pretty much does it for our initialization of the D3D surface. We now have a pointer to an interface for the D3D surface that can be returned from our The remainder of the Additional D3D ConsiderationsBelow are several additional things to keep in mind when implementing a D3D interop solution. Present Not Thy BufferAs mentioned already, the
For the latter purpose, the One very interesting thing to note is that for a WPF Maintains a Reference to the D3D SurfaceRecall that a D3D surface is supplied to WPF via a call to _di.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero);
Pool Considerations for the D3D 9Ex DeviceThe fact that a 9Ex device is required for a performant solution on Vista actually has a few other implications for our D3D code. The biggest one is that we cannot use managed D3D resources with a 9Ex device. Note that this caveat does not apply to the sample included with this article, since we are not creating any pool resources. But if we were, this restriction basically means when creating pool resources, we would need to use our D3DXLoadMeshFromXInMemory(meshSrcData, meshSrcDataSize,
g_is9Ex ? D3DPOOL_DEFAULT : D3DXMESH_MANAGED,
g_pd3dDevice, NULL, NULL, NULL, NULL, ppMesh);
A Note about Supporting Multiple Video AdaptersIn our example, we are creating a device for the default adapter by passing A truly dynamic approach would be to recreate the surface when the window is moved to a monitor that is driven by a different adapter. This would require additional logic within both the WPF application (to re-initialize the scene) and the unmanaged library (to ensure that the correct adapter is used to create the surface). One approach might be to have the WPF application supply the screen coordinates of the window in its Tips for Running the D3DImage SampleThe remainder of this article is dedicated to helping you build and/or run the provided sample. Please read through the following tips prior to contacting me with specific questions. I will, of course, do my best to address any outstanding questions that pertain directly to this article. If you encounter bugs or limitations in I have included a compiled version of the D3DImage sample as part of this article. If you simply want to run this application, then follow the recommendations in this section. However, if you intend to build the sample yourself, please skip to the section entitled "Tips for Building the D3DImage Sample". Install .NET 3.5 SP1Make sure that you have installed Service Pack 1 of the Microsoft .NET Framework, version 3.5. Install the DirectX Redistributable LibrariesYou will also need the latest DirectX Redistributable Libraries. Note that the prebuilt sample that I have included consists of 32-bit binaries that were built against the June 2008 release of the DirectX SDK. They are not binary compatible with earlier versions. If you need to run against the March 2008 release, for example, you will need to download and recompile the sample project yourself. Tips for Building the D3DImage SampleI have also included the source code for the D3DImage sample as part of this article. Below are some tips for compiling and running the sample: Install Visual Studio 2008 SP1 - Or - Install .NET 3.5 SP1This sample requires the new Install the DirectX SDKMake sure that you have installed the latest DirectX SDK. Configure Visual StudioIf Visual Studio is not configured properly, you may see one of the following messages: fatal error C1083: Cannot open include file: 'd3dx9.h': No such file or directory
fatal error LNK1104: Cannot open file 'd3dxof.lib'
To resolve this, make sure the paths to the DirectX header and library files are correctly set within your Visual Studio settings. To configure these paths, bring up the "Options..." dialog from the Tools menu. Then, expand "Projects and Solutions" in the tree on the left, and select "VC++ Directories". Next, select "Include files" from the "Show directories for:" drop down list and add an entry pointing to the DirectX header files. The entry should be "
You will also need to select "Library files" from the "Show directories for:" drop down list and add an entry pointing to the DirectX lib files. The entry should be "
Enable Unmanaged Code DebuggingNote that the D3DImageSample project is already configured for managed code debugging since the target application is a .NET app. However, since the sample project is also comprised of unmanaged code, you may wish to configure Visual Studio for unmanaged code debugging. This will allow you to step directly from managed code into unmanaged code. To enable unmanaged code debugging, you must first open the property pages for the Visual Studio project. There are several ways to do this, but one easy method is to select the project within Solution Explorer and then select "D3DImageSample Properties..." from the "Project" menu. When the property pages open, select the "Debug" page and then make sure the "Enable unmanaged code debugging" option is selected, as shown below:
Save and rebuild the project, and you should be all set for mixed-mode debugging. You can verify this by setting a breakpoint in an unmanaged function in main.cpp. Then, run under the debugger to verify that your breakpoint is hit. Credit Where Credit is DueI want to sincerely thank Jordan Parker (from the WPF 3D Team) and Dwayne Need at Microsoft for helping me get started and setting me straight on so many things while I was learning the new Go Ye Therefore and Create Coolness!Now, it's up to all the serious DirectX developers to create some real fire and ice! I certainly look forward to seeing all the coolness and hotness that folks are able to produce in WPF using the new | ||||||||||||||||||||||