Есть ли способ разместить приложение DirectX12 в окне WPF?
Я знаю, что терминология в этом вопросе должна быть неправильной, но, пожалуйста, потерпите меня и постарайтесь взглянуть на вещи с точки зрения моего неспециалиста (у меня нет образования в области компьютерных технологий, я энтузиаст-самоучка. формальное образование на языке программирования - клуб робототехники моей школы).
Я хочу использовать управляемый DirectX 12 в качестве "фона" моего приложения с игровым циклом и всем остальным. И, если возможно, чтобы иметь возможность иметь элементы управления WPF, такие как лента, набор инструментов или меню в реальной игре DirectX. Я искал по всему интернету, и все, что я нашел, это очень старые вещи для Windows и DirectX 9.0; Я надеюсь, что есть что-то новое в эти дни.
Я попробовал подход Windows Form, который в основном таков:
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 отображается нормально
Тогда я обнаружил 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 ответа
Я знаю, что это старый пост, но для тех, кто ищет решение, есть тот, который я нашел. Решение основано на 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 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: довольно просто
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, bool newSurface) просто сохраните дескриптор поверхности.
Для этого вызовите DxImage.RequestRender().
Выполняете ли вы рендеринг в цикле рендеринга и добавляете копию D3D11on12 к D3D11 в конце.
Заметка
Если вы обрабатываете событие resize, подумайте о том, чтобы изменить размер DxImage с помощью DxImage.SetPixelSize, а затем воссоздать ваши обернутые ресурсы.
Больше объяснений
Я создаю устройство следующим образом:
_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);
И я создаю FBO Dx11 и Dx9 так:
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();
}));
}
Этот проект должен помочь. В настоящее время он поддерживает только Direct3D 11, но принципы аналогичны DirectX 12.
Тем не менее, зачем вам DirectX 12 вместо того, чтобы просто придерживаться DirectX 11? Ответ должен быть более техническим, чем "12 больше, чем 11".