Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 689
FinnTheHuman Avatar asked Jun 06 '16 21:06

FinnTheHuman


1 Answers

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();
        }));
    }
like image 77
Mayhem50 Avatar answered Oct 17 '22 04:10

Mayhem50