有没有一种方法可以在WPF窗口中托管DirectX12应用程序?

6
我知道这个问题的术语可能都是错误的,但请您从我的外行人角度来看待,并尝试理解我(我没有计算机技术的背景,我是一个自学的爱好者。我最接近编程语言的正式教育是在学校机器人俱乐部)。
我想要的是能够使用托管DirectX 12作为我的应用程序的“背景”,带有游戏循环等所有内容。如果可能的话,还希望能够在实际的DirectX游戏周围拥有WPF控件,例如功能区、工具箱或菜单。我已经在互联网上寻找了很久,但发现的都是非常陈旧的Windows和DirectX 9.0的资料;我希望现在有一些新的东西。
我尝试了Windows窗体的方法,基本上是这样的:
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();
    }
}

没有任何异常被抛出,窗口根本没有被渲染。应用程序运行,但窗口没有显示出来。我原本认为这不会起作用,因为没有游戏循环,render也没有从任何地方调用,但我没想到窗口甚至都没有显示出来。如果我注释掉调用initDevice()的那一行,WPF的空白窗口就会正常显示。
接着我发现每帧(或者说每个tick)都会调用CompositionTarget.Rendering事件,所以必须使用该事件的处理程序作为游戏循环。
于是我尝试了这个方法:
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);
    }
}

尽管应用程序正在运行且未崩溃,但仍未显示任何窗口。

最终我尝试了相同的方法,但是没有处理CompositionTarget.Rendering,而是使用一个DispatcherTimer,并在其中调用render的Tick事件处理程序。结果相同:没有窗口。

有人能指导我走向正确的方向吗?

2个回答

4

我知道这是一篇旧文章,但对于那些正在寻找解决方案的人,这是我找到的解决方案。该解决方案基于Chuck提到的项目中的D3D11Image。

1. 在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. 创建 Dx11 相关内容:

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 : 相当简单

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

4. 渲染函数:

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);
    }

奖励

您还可以在不使用组合目标事件的情况下进行渲染。在Render回调函数--->void Render(IntPtr surface, bool newSurface)中,只需存储表面的句柄。

调用DxImage.RequestRender()即可。

在渲染循环中执行渲染并在最后添加D3D11on12到D3D11的复制。

注意

如果您处理了重置事件,请考虑使用DxImage.SetPixelSize调整DxImage大小,然后重新创建包装资源。

更多解释

我是这样创建设备的:

_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);
        }
    }

实际上它们是相同的。然后,在渲染后,我将我Dx12 RenderTarget复制到我的Dx11 RenderTarget。
        var ptr = GetDx12ResourceFromHandle(Resources.Dx11Texture.NativePointer);
        commandList.CopyResource(ptr, Resources.RenderTarget);

在我的RenderLoop中,我会像这样更新BackBuffer:
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();
        }));
    }

我有点晚了,但我尝试了你的方法,结果DX12抱怨OpenSharedResource,因为DX11资源没有使用Shared标志创建。实际上,D3DImage11似乎甚至没有创建D3D11资源(它是一个D3D10资源)。你也遇到这个问题了吗? - Karnalta
不,我没有问题。 - Mayhem50

2
这个项目应该会对你有所帮助。目前仅支持Direct3D 11,但在DirectX 12中的原理相同。
然而,为什么你需要使用DirectX 12而不是仅停留在DirectX 11上呢?答案应该比“12比11更大”这样的非技术性回答更加专业。

我有一块非常强大的GPU,据说DirectX 12是迄今为止图形库中性能最好的,我想看看它到底有多好。非常感谢您提供的链接,看起来很有前途。 - FinnTheHuman
3
DirectX 12有潜力提高CPU性能,但需要大量的工程努力才能达到这一目标。如果您需要Direct3D硬件特征级别11.0或更高版本(DX12没有针对FL 9.x或10.x设备的驱动程序),那么DX11和DX12的GPU功能是相同的。通常情况下,在将DX11推向极限之前,除非您拥有一个需要DX12 API提供的额外控制权的大型图形团队,否则转换到DX12可能并不明智。对于独立开发者或小团队来说,维护好DX12是一项艰巨的工作。 - Chuck Walbourn
我想要DX12的模拟功能只是为了学习,而不是现在就想用它来编写游戏。因此,如果DX12也有这样的控制,那将是很好的。 - Yola
你已经能够将项目移植以支持D3D12了吗? - Mayhem50

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接