Is there a way to host a DirectX12 application inside a WPF window?
Asked Answered
S

2

7

I know the terminology of this question must be all wrong, but please bear with me and try to see things from my layman's point of view (I have no formation in computer technology, I'm a self taught enthusiast. The closest I get from a formal education in programming language is my school's robotics club).

What I want is to be able to use managed DirectX 12 as the "background" of my application, with a game loop and all. And, if possible, to be able to have WPF controls like a ribbon or a toolbox or a menu around the actual directX game. I've been looking all over the internet and all I find is very old stuff for Windows and DirectX 9.0; i'm hoping there's something new these days.

I tryed the Windows Form approach, which is basically this:

using System;
using System.Windows;
using System.Windows.Interop;
using Microsoft.DirectX.Direct3D;
using DColor = System.Drawing.Color;

public partial class MainWindow : Window
{
    Device device;
    public MainWindow()
    {
        InitializeComponent();
        initDevice();
    }

    private void initDevice()
    {
        try
        {
            PresentParameters parameters = new PresentParameters();
            parameters.Windowed = true;
            parameters.SwapEffect = SwapEffect.Discard;
            IntPtr windowHandle = new WindowInteropHelper(this).Handle;

            device = new Device(0, DeviceType.Hardware, windowHandle, CreateFlags.HardwareVertexProcessing, parameters);
        }
        catch(Exception e)
        {
            MessageBox.Show("initDevice threw an Exception\n" + e.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    private void render()
    {
        device.Clear(ClearFlags.Target, DColor.LightGreen, 0f, 1);
        device.Present();
    }
}

No exception is thrown, the window is never rendered at all. The application runs, but the window doesn't show up. I didn't think this would work, because there's no game loop and render doesn't get invoked from anywhere, but I didn't expect the window not even being displayed. If I comment out the line that invokes initDevice(), WPF's blank window is shown normally

Then I that discovered the CompositionTarget.Rendering event gets called once every frame (or tick?), so the handler for this event must be used as the game loop.

and so I tried this:

using System;
using System.Drawing;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Forms.Integration;
using Microsoft.DirectX.Direct3D;
using DColor = System.Drawing.Color;
using System.Windows.Forms;

public partial class MainWindow : Window
{
    Device device = null;
    MemoryStream stream;
    PictureBox display;
    WindowsFormsHost host;

    public MainWindow()
    {
        InitializeComponent();
        initDevice();
        CompositionTarget.Rendering += CompositionTarget_Rendering;
    }

    private void CompositionTarget_Rendering(object sender, EventArgs e)
    {
        render();
    }

    private void initDevice()
    {
        try
        {
            PresentParameters parameters = new PresentParameters();
            parameters.Windowed = true;
            parameters.SwapEffect = SwapEffect.Discard;

            device = new Device(0, DeviceType.Hardware, display, CreateFlags.HardwareVertexProcessing, parameters);
            stream = new MemoryStream();
            device.SetRenderTarget(0, new Surface(device, stream, Pool.Managed));
        }
        catch(Exception e)
        {
            System.Windows.MessageBox.Show("initDevice threw an Exception\n" + e.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    private void render()
    {
        device.Clear(ClearFlags.Target, DColor.LightGreen, 0f, 1);
        device.Present();
        display.Image = Image.FromStream(stream);
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        host = new WindowsFormsHost();
        display = new PictureBox();
        host.Child = display;
        mainGrid.Children.Add(host);
    }
}

Still no window is shown, even though the application is running and not crashing.

Finally I tried the same thing but without handling CompositionTarget.Rendering, but using a DispatcherTimer instead, and called render from inside its Tick event handler. Same result: no Window.

Can anyone point me to the right direction?

Shorthanded answered 6/6, 2016 at 21:57 Comment(0)
T
5

I know it's an old post but for those who search a solution, there is the one I found. The solution is based on D3D11Image from the project mentioned by Chuck.

1. On Window_Loaded_Event :

    private void Window_Loaded(object sender, RoutedEventArgs e) {
        InitDx12();
        CreateDx11Stuff();

        DxImage.SetPixelSize(1280, 720);
        DxImage.WindowOwner = (new System.Windows.Interop.WindowInteropHelper(this)).Handle;
        DxImage.OnRender += Render;
        CompositionTarget.Rendering += CompositionTarget_Rendering;
    }

2. Create Dx11 Stuff :

private void CreateDx11Stuff() {
        D3D11Device = SharpDX.Direct3D11.Device.CreateFromDirect3D12(D3D12Device, SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport | SharpDX.Direct3D11.DeviceCreationFlags.Debug, new[] { SharpDX.Direct3D.FeatureLevel.Level_12_1 }, Adatper, CommandQueue);

        D3D11On12 = ComObject.QueryInterfaceOrNull<SharpDX.Direct3D11.Device11On12>(D3D11Device.NativePointer);                       

        for(int idx = 0; idx < BackBufferCount; idx++) {
            D3D11On12.CreateWrappedResource(BackBuffers[idx], new D3D11ResourceFlags { BindFlags = (int)BindFlags.RenderTarget, CPUAccessFlags = 0, MiscFlags = (int)0x2L, StructureByteStride = 0 }, (int)ResourceStates.RenderTarget, (int)ResourceStates.Present, typeof(Texture2D).GUID, out D3D11BackBuffers[idx]);
        }
    }

3. CompositionTarget Rendering : is quite simple

private void CompositionTarget_Rendering(object sender, EventArgs e) {
        DxImage.RequestRender();
    }

4. The render function :

private void Render(IntPtr surface, bool newSurface) {
        DoDx12Rendering();

        var unk = new ComObject(surface);
        var dxgiRes = unk.QueryInterface<SharpDX.DXGI.Resource>();

        var tempRes = D3D11Device.OpenSharedResource<SharpDX.Direct3D11.Resource>(dxgiRes.SharedHandle);
        var backBuffer = tempRes.QueryInterface<Texture2D>();
        var d3d11BackBuffer = D3D11BackBuffers[CurrentFrame];

        D3D11On12.AcquireWrappedResources(new[] { d3d11BackBuffer }, 1);
        D3D11Device.ImmediateContext.CopyResource(d3d11BackBuffer, backBuffer);
        D3D11Device.ImmediateContext.Flush();
        D3D11On12.ReleaseWrappedResources(new[] { d3d11BackBuffer }, 1);
    }

Bonus

You can also do you rendering without the composition target event. For this, in the Render callback --> void Render(IntPtr surface, bool newSurface), just store the handle of the surface.

Call DxImage.RequestRender() for this.

Do you render in your render loop and add the D3D11on12 to D3D11 copy at the end.

Note

If you handle the resize event, think to resize the DxImage with DxImage.SetPixelSize then recreate your wrapped resources.

More Explanations

I create the Device like this :

_D3D9Device = new DeviceEx(new Direct3DEx(), 0, DeviceType.Hardware, handle, CreateFlags.HardwareVertexProcessing | CreateFlags.Multithreaded | CreateFlags.FpuPreserve, new SharpDX.Direct3D9.PresentParameters(1, 1) {
            Windowed = true,
            SwapEffect = SharpDX.Direct3D9.SwapEffect.Discard,
            DeviceWindowHandle = handle,
            PresentationInterval = PresentInterval.Immediate
        });


_D3D11Device = SharpDX.Direct3D11.Device.CreateFromDirect3D12(Device, DeviceCreationFlags.BgraSupport, new[] { SharpDX.Direct3D.FeatureLevel.Level_12_0 }, null, RenderCommandQueue);

And I create the Dx11 and Dx9 FBOs like that :

private void CreateWPFInteropFBO()
    {
        var desc = new Texture2DDescription {
            ArraySize = 1,
            BindFlags = BindFlags.RenderTarget,
            Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
            Height = RenderTargetSize.Height,
            Width = RenderTargetSize.Width,
            MipLevels = 1,
            OptionFlags = ResourceOptionFlags.Shared,
            SampleDescription = new SharpDX.DXGI.SampleDescription(1, 0),
            Usage = ResourceUsage.Default
        };

        Dx11Texture?.Dispose();

        Dx11Texture = new Texture2D(_D3D11Device, desc);

        var ptr = Dx11Texture.NativePointer;
        var comobj = new ComObject(ptr);
        using (var dxgiRes = comobj.QueryInterface<SharpDX.DXGI.Resource>()) {
            var sharedHandle = dxgiRes.SharedHandle;

            var texture = new Texture(_D3D9Device, desc.Width, desc.Height, 1, SharpDX.Direct3D9.Usage.RenderTarget, SharpDX.Direct3D9.Format.A8R8G8B8, Pool.Default, ref sharedHandle);

            Dx9Surface?.Dispose();
            Dx9Surface = texture.GetSurfaceLevel(0);
        }
    }

In fact they are the sames. Then, after rendering I copy my Dx12 RenderTarget to my Dx11 RenderTarget.

        var ptr = GetDx12ResourceFromHandle(Resources.Dx11Texture.NativePointer);
        commandList.CopyResource(ptr, Resources.RenderTarget);

In my RenderLoop I update the BackBuffer like this :

private async void UpdateDx9Image()
    {
        if (Application.Current == null) return;

        await Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() =>
        {
            if (DxImage.TryLock(new Duration(new TimeSpan(0, 0, 0, 0, 16))))
            {
                DxImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _Renderer.Resources.Dx9Surface.NativePointer, false);
                DxImage.AddDirtyRect(new Int32Rect(0, 0, _Renderer.Resources.Dx9Surface.Description.Width, _Renderer.Resources.Dx9Surface.Description.Height));
            }

            DxImage.Unlock();
        }));
    }
Thug answered 25/5, 2018 at 7:20 Comment(2)
I am a bit late but I tried your method and I end up with DX12 complaining with OpenSharedResource because DX11 resource has not been created with Shared flag. In fact D3DImage11 don't seem to even create a D3D11 resource (it's a D3D10 one). Have you got that problem too ?Smail
Nope. I have no problem.Thug
P
2

This project should help. It currently only supports Direct3D 11, but the principles are the same with DirectX 12.

That said, why do you need DirectX 12 instead of just sticking with DirectX 11? The answer should be something more technical than "12 is bigger than 11."

Partlet answered 8/6, 2016 at 4:0 Comment(4)
I have a very powerful GPU and it is said that DirectX 12 has the best performance ever from a graphics library and I wanted to see how good it really is. Thank you very much for the link, looks promissing.Shorthanded
DirectX 12 has the potential for better CPU-side performance, but it requires a significant engineering effort to get there. The GPU features are identical for both DX11 and DX12 if you require Direct3D Hardware Feature Level 11.0 or later (DX12 doesn't have any drivers for FL 9.x or 10.x devices). In general you should already be pushing DX11 to the breaking point before it makes sense to move to DX12 unless you have a large team of graphics engineers who need the extra control the DX12 API provides. For indies or small teams, keeping DX 12 happy is a ton of work.Partlet
The only reasoun i would like to have analogue functionality for DX12 is that i want to learn it, not like i want to wirte game with it right now. So, that would be good to have such control for DX12 as well.Jovian
Have you been able to port the project to support D3D12 ?Thug

© 2022 - 2024 — McMap. All rights reserved.