本文告诉大家如何通过 SharpDx 进行异步渲染,但是因为在 WPF 是需要使用 D3DImage 画出来,所以渲染只是画出图片,最后的显示还是需要 WPF 在他自己的主线程渲染。
本文是一个系列,希望大家从第一篇开始看
- WPF 使用 Direct2D1 画图入门
- WPF 使用 Direct2D1 画图 绘制基本图形
- WPF 使用 SharpDX
- WPF 使用 SharpDX 在 D3DImage 显示
- WPF 使用封装的 SharpDx 控件
- WPF 使用 SharpDx 异步渲染
更多请看 WPF 使用 SharpDx 渲染博客导航
虽然上一篇告诉大家如何使用封装的 SharpDx 控件,但是大家也看到了核心是使用CompositionTarget
告诉刷新的。
这个方法适合不停变化的控件,如果是很少刷新的控件使用这个方法会降低 WPF 的性能。
因为 CompositionTarget 刷新数太快了,而且每次都需要重复刷新一个图片,显示的性能比不过自带的控件。
使用方法
因为使用 SharpDx 在 WPF 除了使用 D3DImage 还可以使用 D3D11Image 但是这个需要分开 x86 和 x64 。现在使用的方法是把 D3DImage 作为图片画出来,如果使用 D3D11Image 也没有什么性能提升。
所以本文就和WPF 使用封装的 SharpDx 控件使用的基类不同,原来的基类是 Image 现在的基类是 FrameworkElement
。但是如果使用 Image 而且每次刷新的都是比较小的,性能会比使用 FrameworkElement 画出高一些。
这里因为封装没有告诉需要刷新的大小,所以只能每次都全部刷新,这样的性能使用 FrameworkElement 不会降低。
下面创建一个类,继承 SharpDxMaynumaSejair ,这个 SharpDxMaynumaSejair 是继承 FrameworkElement 而不是图片,这个类的代码放在文章最后,使用这个类可以异步渲染。
代码语言:javascript复制 public abstract class SharpDxMaynumaSejair : FrameworkElement
请随意写一个类继承 SharpDxMaynumaSejair 并且添加重写的 OnRender 函数。
下面是 SharpDxMaynumaSejair 类的 OnRender 方法,通过继承他就可以使用 SharpDx 画出来。
代码语言:javascript复制protected abstract void OnRender(SharpDX.Direct2D1.RenderTarget renderTarget);
其他的代码和WPF 使用封装的 SharpDx 控件使用的差不多
直接通过 OnRender 就可以进行渲染,但是 OnRender 是被触发的,触发的方法是调用基类 Rendering
函数,调用了这个函数会进入异步的 SharpDx 渲染,渲染完成再通过 WPF 渲染画出来。
因为不需要使用 CompositionTarget.Rendering 渲染,所以可以提高 WPF 刷新速度。
这个类可以在执行渲染计算复杂使用,假如需要渲染出 10000 个椭圆,而且有很多重叠,而且不需要立刻渲染。那么就可以使用本文的这个类,这个类可以在调用时异步渲染,不会卡 UI 线程,在 SharpDx 渲染完成再通过 WPF 渲染,这时 WPF 渲染也就是画出图片,性能比画出 10000 个椭圆快很多。通过这个方法可以提高渲染性能,提高软件打开的性能。
但是通过这个方法建议软件是 x64 因为需要很多内存。
下面来告诉大家本文这个类的原理。
绑定
如果需要使用 SharpDx 需要把 SharpDX.Direct3D11 和 D3DImage 绑定,调用时不能在这个控件的 Load 前,不然无法拿到大小。
下面这个方法和WPF 使用封装的 SharpDx 控件使用相同,所以我就直接写代码不解释了。
代码语言:javascript复制 private void CreateAndBindTargets(int actualWidth, int actualHeight)
{
var width = Math.Max(actualWidth, 100);
var height = Math.Max(actualHeight, 100);
var renderDesc = new SharpDX.Direct3D11.Texture2DDescription
{
BindFlags = SharpDX.Direct3D11.BindFlags.RenderTarget | SharpDX.Direct3D11.BindFlags.ShaderResource,
Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
Width = width,
Height = height,
MipLevels = 1,
SampleDescription = new SharpDX.DXGI.SampleDescription(1, 0),
Usage = SharpDX.Direct3D11.ResourceUsage.Default,
OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.Shared,
CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None,
ArraySize = 1
};
var device = new SharpDX.Direct3D11.Device(DriverType.Hardware,
SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport);
_device = device;
var renderTarget = new SharpDX.Direct3D11.Texture2D(device, renderDesc);
var surface = renderTarget.QueryInterface<SharpDX.DXGI.Surface>();
var d2DFactory = new SharpDX.Direct2D1.Factory();
var renderTargetProperties =
new SharpDX.Direct2D1.RenderTargetProperties(
new SharpDX.Direct2D1.PixelFormat(SharpDX.DXGI.Format.Unknown,
SharpDX.Direct2D1.AlphaMode.Premultiplied));
_d2DRenderTarget = new SharpDX.Direct2D1.RenderTarget(d2DFactory, surface, renderTargetProperties);
SetRenderTarget(renderTarget);
device.ImmediateContext.Rasterizer.SetViewport(0, 0, width, height);
CreationProperties creationProperties = new CreationProperties
{
DebugLevel = DebugLevel.Error,
Options = DeviceContextOptions.EnableMultithreadedOptimizations,
ThreadingMode = ThreadingMode.MultiThreaded,
};
_d2dContext = new DeviceContext(surface, creationProperties);
_d2DRenderTarget = _d2dContext;
InvalidateVisual();
}
如果大家有对比两个函数,会发现有一个属性不相同,原来是 Direct3D11.Device 被我拿出一个字段,这个字段在下面会使用到。
虽然已经写好 D3DImage 但是如何显示?
继承 FrameworkElement 可以重写 OnRender 。通过 OnRender 可以画出图片,而 D3Dimage 就是 ImageSource,虽然可以看到我自己定义的也是 OnRender, 这个函数和自己定义的不相同,虽然我把自己定义的函数也是和他使用相同的命名。看到下面代码也许就知道我说的是什么。
需要注意,如果因为_d3D.PixelWidth
为0抛出异常,那么就可能是绑定的时候在 Load 之前,需要修改一下代码。
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawImage(_d3D, new Rect(new Size(_d3D.PixelWidth, _d3D.PixelHeight)));
}
渲染为什么空白
现在已经完成了修改继承类,但是原来使用的渲染还是没有修改。如果大家尝试在一个按钮按下时,进行刷新。代码大概是在 按钮按下时调用Rendering
函数,这个函数只执行一次
public void Rendering()
{
_d2dContext.BeginDraw();
OnRender(_d2dContext);
_d2dContext.EndDraw();
_d3D.Lock();
_d3D.AddDirtyRect(new Int32Rect(0, 0, _d3D.PixelWidth, _d3D.PixelHeight));
_d3D.Unlock();
base.InvalidateVisual();
}
如果大家运行了代码,会发现现在的界面虽然有执行OnRender
但是显示和希望的不一样。
原因是没有等待 SharpDx 画完,虽然调用了EndDraw
但是只是把渲染命令发给显卡。
那么如何等待 SharpDx 画完
等待画完
如果刚才看到 CreateAndBindTargets 会看到把 Direct3D11.Device 放在字段,因为在 Rendering 就需要使用这个字段等待显卡刷新。
请看下面的代码
代码语言:javascript复制 public void Rendering()
{
_d2dContext.BeginDraw();
OnRender(_d2dContext);
_d2dContext.EndDraw();
_device.ImmediateContext.Flush();
_d3D.Lock();
_d3D.AddDirtyRect(new Int32Rect(0, 0, _d3D.PixelWidth, _d3D.PixelHeight));
_d3D.Unlock();
base.InvalidateVisual();
}
尝试修改代码运行一下,可以如果调用了一次函数就可以刷新一次。
如果是在按钮按下是刷新,但是渲染很多内容,那么主线程会在 Flush 占用很多资源。
在 WPF 的渲染,是把主线程和渲染线程分开,经常说的主线程是没有做渲染的,在 DrawingContext 实际上不是调用了显示,而且通过 Channel 发送到Dx渲染,也就是调用函数只是告诉显卡如何渲染。
在这里也是需要做相同的方法。
异步渲染
大家也可以看到,只需要使用一个新的线程去等待渲染就可以,使用新线程的方法是 Task ,但是不能把 d3dImage 放在另一个线程,他必须在主线程。
修改一下代码
代码语言:javascript复制 public async void Rendering()
{
await Task.Run(() =>
{
_d2dContext.BeginDraw();
OnRender(_d2dContext);
_d2dContext.EndDraw();
_device.ImmediateContext.Flush();
});
_d3D.Lock();
_d3D.AddDirtyRect(new Int32Rect(0, 0, _d3D.PixelWidth, _d3D.PixelHeight));
_d3D.Unlock();
base.InvalidateVisual();
}
现在就可以在另一个线程告诉 SharpDx 如何画,然后在另一个线程等待 SharpDx 画出来。这样可以做到异步渲染。
需要告诉大家,异步渲染不是多线程渲染,原因是渲染还是需要显卡来做,如果显卡的资源有限,那么渲染需要的时间不会降低。
如果需要设置多线程渲染,可以通过在CreationProperties
使用ThreadingMode.MultiThreaded
现在上面的代码已经这样使用。
不过大家不要直接使用这个类,因为上面代码使用Task.Run
,如果在线程池没有资源,那么这个代码可能会等很久,这样的性能比较差。
这个控件可以用在不需要立刻渲染的资源,但是渲染很慢,可以在用户做其他的输入进行渲染。因为默认的渲染都会让用户感觉软件速度有些慢,不过和这个做法相同的是使用 RenderTargetBitmap ,在另一个线程渲染,然后在主线程显示。
和 RenderTargetBitmap 不同的,本文的方法可以在显卡渲染,渲染性能比 RenderTargetBitmap 高。
多线程渲染
下面告诉大家如何使用 RenderTargetBitmap 多线程渲染
首先创建一个字段,在这个字段为空就需要调用函数创建
代码语言:javascript复制 private RenderTargetBitmap _stouFa;
这个方法适合只有进行很少的渲染,而且很慢的应用。实际上 RenderTargetBitmap 可以 Freeze ,所以在另一个线程渲染是可以。
代码语言:javascript复制 protected override void OnRender(DrawingContext drawingContext)
{
if (_stouFa == null)
{
TesuFudresel();
}
else
{
drawingContext.DrawImage(_stouFa, new Rect(new Size(ActualWidth, ActualHeight)));
}
}
在方法 TesuFudresel 就是创建渲染
代码语言:javascript复制 private void TesuFudresel()
{
Task.Run(() =>
{
var renderTargetBitmap = new RenderTargetBitmap(100, 100, 96, 96, new PixelFormat());
var drawingVisual = new DrawingVisual();
var drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawRectangle(Brushes.Chartreuse, new Pen(Brushes.Chartreuse, 2),
new Rect(new Point(10, 10), new Size(100, 100)));
drawingContext.Close();
renderTargetBitmap.Render(drawingVisual);
renderTargetBitmap.Freeze();
_stouFa = renderTargetBitmap;
});
}
代码主要就是使用 drawingVisual 画出来,然后让对象可以跨线程。
本文就告诉大家如何使用 SharpDx 异步渲染,还告诉大家如何使用 WPF 自带的类进行多线程渲染,下面就是本文这个控件的代码
建议大家自己写一个线程调度而不是使用 Task ,因为最近在写 Avalon 所以暂时我也没有把下面的类写的可以在产品使用。请大家参考代码而不是在项目使用这个代码,因为存在 Task 需要等很久和代码没有优化。
代码语言:javascript复制 public abstract class SharpDxMaynumaSejair : FrameworkElement
{
private D3DImage _d3D = new D3DImage();
/// <inheritdoc />
protected SharpDxMaynumaSejair()
{
Loaded = SharpDxMaynumaSejair_Loaded;
}
private void SharpDxMaynumaSejair_Loaded(object sender, RoutedEventArgs e)
{
CreateAndBindTargets((int) ActualWidth, (int) ActualHeight);
}
private void CreateAndBindTargets(int actualWidth, int actualHeight)
{
var width = Math.Max(actualWidth, 100);
var height = Math.Max(actualHeight, 100);
var renderDesc = new SharpDX.Direct3D11.Texture2DDescription
{
BindFlags = SharpDX.Direct3D11.BindFlags.RenderTarget | SharpDX.Direct3D11.BindFlags.ShaderResource,
Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
Width = width,
Height = height,
MipLevels = 1,
SampleDescription = new SharpDX.DXGI.SampleDescription(1, 0),
Usage = SharpDX.Direct3D11.ResourceUsage.Default,
OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.Shared,
CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None,
ArraySize = 1
};
var device = new SharpDX.Direct3D11.Device(DriverType.Hardware,
SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport);
_device = device;
var renderTarget = new SharpDX.Direct3D11.Texture2D(device, renderDesc);
var surface = renderTarget.QueryInterface<SharpDX.DXGI.Surface>();
var d2DFactory = new SharpDX.Direct2D1.Factory();
var renderTargetProperties =
new SharpDX.Direct2D1.RenderTargetProperties(
new SharpDX.Direct2D1.PixelFormat(SharpDX.DXGI.Format.Unknown,
SharpDX.Direct2D1.AlphaMode.Premultiplied));
_d2DRenderTarget = new SharpDX.Direct2D1.RenderTarget(d2DFactory, surface, renderTargetProperties);
SetRenderTarget(renderTarget);
device.ImmediateContext.Rasterizer.SetViewport(0, 0, width, height);
CreationProperties creationProperties = new CreationProperties
{
DebugLevel = DebugLevel.Error,
Options = DeviceContextOptions.EnableMultithreadedOptimizations,
ThreadingMode = ThreadingMode.MultiThreaded,
};
_d2dContext = new DeviceContext(surface, creationProperties);
_d2DRenderTarget = _d2dContext;
InvalidateVisual();
}
public new void InvalidateVisual()
{
Rendering();
}
/// <inheritdoc />
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawImage(_d3D, new Rect(new Size(_d3D.PixelWidth, _d3D.PixelHeight)));
}
protected abstract void OnRender(SharpDX.Direct2D1.RenderTarget renderTarget);
private SharpDX.Direct3D9.Texture _renderTarget;
private SharpDX.Direct2D1.RenderTarget _d2DRenderTarget;
private DeviceContext _d2dContext;
private SharpDX.Direct3D11.Device _device;
private async void Rendering()
{
await Task.Run(() =>
{
_d2dContext.BeginDraw();
OnRender(_d2dContext);
_d2dContext.EndDraw();
_device.ImmediateContext.Flush();
});
_d3D.Lock();
_d3D.AddDirtyRect(new Int32Rect(0, 0, _d3D.PixelWidth, _d3D.PixelHeight));
_d3D.Unlock();
base.InvalidateVisual();
}
private void SetRenderTarget(SharpDX.Direct3D11.Texture2D target)
{
var format = TranslateFormat(target);
var handle = GetSharedHandle(target);
var presentParams = GetPresentParameters();
var createFlags = SharpDX.Direct3D9.CreateFlags.HardwareVertexProcessing |
SharpDX.Direct3D9.CreateFlags.Multithreaded |
SharpDX.Direct3D9.CreateFlags.FpuPreserve;
var d3DContext = new SharpDX.Direct3D9.Direct3DEx();
var d3DDevice = new SharpDX.Direct3D9.DeviceEx(d3DContext, 0, SharpDX.Direct3D9.DeviceType.Hardware,
IntPtr.Zero, createFlags,
presentParams);
_renderTarget = new SharpDX.Direct3D9.Texture(d3DDevice, target.Description.Width,
target.Description.Height, 1,
SharpDX.Direct3D9.Usage.RenderTarget, format, SharpDX.Direct3D9.Pool.Default, ref handle);
using (var surface = _renderTarget.GetSurfaceLevel(0))
{
_d3D.Lock();
_d3D.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface.NativePointer);
_d3D.Unlock();
}
}
private static SharpDX.Direct3D9.PresentParameters GetPresentParameters()
{
var presentParams = new SharpDX.Direct3D9.PresentParameters();
presentParams.Windowed = true;
presentParams.SwapEffect = SharpDX.Direct3D9.SwapEffect.Discard;
presentParams.DeviceWindowHandle = NativeMethods.GetDesktopWindow();
presentParams.PresentationInterval = SharpDX.Direct3D9.PresentInterval.Default;
return presentParams;
}
private IntPtr GetSharedHandle(SharpDX.Direct3D11.Texture2D texture)
{
using (var resource = texture.QueryInterface<SharpDX.DXGI.Resource>())
{
return resource.SharedHandle;
}
}
private static SharpDX.Direct3D9.Format TranslateFormat(SharpDX.Direct3D11.Texture2D texture)
{
switch (texture.Description.Format)
{
case SharpDX.DXGI.Format.R10G10B10A2_UNorm:
return SharpDX.Direct3D9.Format.A2B10G10R10;
case SharpDX.DXGI.Format.R16G16B16A16_Float:
return SharpDX.Direct3D9.Format.A16B16G16R16F;
case SharpDX.DXGI.Format.B8G8R8A8_UNorm:
return SharpDX.Direct3D9.Format.A8R8G8B8;
default:
return SharpDX.Direct3D9.Format.Unknown;
}
}
private static class NativeMethods
{
[DllImport("user32.dll", SetLastError = false)]
public static extern IntPtr GetDesktopWindow();
}
}
更多渲染博客请看 WPF 底层渲染
特别感谢
Direct2D - 随笔分类 - 万仓一黍 - 博客园
Direct2D - 随笔分类 - 万一 - 博客园
Direct2D - 随笔分类 - zdd - 博客园