我正在尝试在 WPF 应用程序中通过 SharpDX 使用 DirectX11 在窗口中平滑滚动一些图像。
一点背景:
图像是信号返回,随时间变化,已从文件加载并作为纹理数组加载到 D3D 中,图像本身被渲染为一系列带纹理的四边形(一系列相邻的矩形在一条很长的水平线上)。视口的设置使左右边缘代表时间偏移,并且在这些时间之间的时间段内显示信号。DirectX 的实现非常简单;四边形顶点生成一次,一个非常小的每帧缓冲区包含一个简单的 2D 世界视图变换,根据当前可见范围/缩放等更新比例和平移。据我所知,D3D 实现不是我遇到的问题的一部分-它确实是一个非常简单的设置,并且似乎渲染得非常快(应该如此),
问题:
为可见时间范围设置动画时,图像的滚动不流畅。图像在很多时候“抖动”,坦率地说看起来很糟糕(当它不抖动时,它看起来很棒)。
设置:
DirectX 被渲染为 D3DImage,在幕后进行了一些工作以使 DX11 工作 - 此代码取自https://sharpdxwpf.codeplex.com/
有多个 D3DImages(最多 4 个),排列在一个网格中(我一直在测试两个,两个都包含相同时间段的信号并且一起动画)。
这些 D3DImage 由 DrawingVisual(由自定义 FrameworkElement 托管)绘制,它们用作 ImageBrush 的源。frameworkelement 对象根据需要触发渲染,绘图视觉对象处理 D3D 渲染调用并绘制一个矩形以使用 D3DImage 画笔填充控件。
时间范围值使用 WPF DoubleAnimation 进行动画处理。当前显示的视觉效果通过 INotifyPropertyChanged 绑定到此值,并在每次更改时触发渲染(通过 InvalidateVisual)。
DrawingVisual 渲染代码,由“位置”值的变化触发(可见时间范围的开始):
// update scene per-frame buffer first, with world-view transform
using (DrawingContext dc = RenderOpen()
{
_scene.Renderer.Render(_draw_args);
_image.Invalidate();
dc.DrawRectangle(_brush, null, new Rect(viewer.RenderSize));
}
我试过的:
我已经尝试了很多事情(并进行了很多搜索)来尝试确定问题是否是由于渲染请求时间抖动,或者问题是否进一步进入渲染过程。
- 挂钩 CompositionTarget.Rendering 首先通过 CompositionTarget.Rendering 驱动位置值的更新,并让其余过程保持原样(即元素对该值的变化作出反应):
(不用说,这是非常“测试”的代码):
Stopwatch rsw;
long last_time = 0;
void play()
{
rsw = new Stopwatch();
last_time = 0;
rsw.Start();
CompositionTarget.Rendering += rendering;
}
void rendering(object sender, EventArgs e)
{
double update_distance = rsw.ElapsedMilliseconds - last_time;
Position += 10 * update_distance;
last_time = rsw.ElapsedMilliseconds;
}
结果 - 比简单的双重动画差。
- 使用 DispatcherTimer。与上面类似,使用计时器结合秒表来更好地判断经过的时间。更糟糕的结果再次出现,有趣的是 CPU 使用率从大约 7%(半个核心)下降到 0.7%。
最初的尝试以及变体 1 和 2 都尝试更新单个值,然后触发相关方的渲染。出于兴趣,我记录了渲染请求到达绘图视觉对象时的时间差异——尽管 DisptacherTimer 实际上在那里给出了最一致的结果(WPF 动画和 CompositionTarget 都丢弃了奇数帧),但 DisptacherTimer 也给出了最紧张的动画。
更新图像,但不使视觉无效。更新 ImageBrush 的源会更新显示的图像,而无需重新渲染 DrawingVisual。这种方法产生了非常紧张的结果。
框架元素本身中的 CompositionTarget.Rendering。这产生了该批次的最佳结果,但仍不完美。抖动会发生,然后消散,然后又回来了。在这种方法中,保存 DV 的框架元素与 CompositionTarget.Rendering 挂钩,并且视觉对象会查询当前位置,该位置正在独立进行动画处理。我几乎可以接受这种方法。
尝试等待直到渲染 D3D 场景,然后再使图像无效(没有明显的改进):
_scene.Renderer.Render(_draw_args);
_scene.Renderer.Device.ImmediateContext.End(q);while (!(_scene.Renderer.Device.ImmediateContext.IsDataAvailable(q))) Thread.Yield();
_image.Invalidate();
观察:
- 我真的不认为这是一个性能问题。我的开发机器有一个很好的显卡,8 个 i7 内核等,这是一个简单的渲染操作。
- 这看起来确实像是 D3D 和 WPF 渲染之间的某种同步问题,但我不知道如何开始研究这个问题。
- 如果我有两个并行动画的图像,则抖动在两个图像中的第一个上更加明显(通常)。
- 如果我在制作动画时主动调整窗口大小,动画会非常流畅(尽管正在进行大量额外工作,因为 D3D 上下文正在不断调整大小)。
编辑
我已经尽可能地收回事情以尝试隔离问题,并且该问题似乎是 WPF 更新和呈现 D3DImage 的方式的基础。
我已经修改了 SharpDX Samples 解决方案中的 WPFHost 示例,以便显示的简单三角形在屏幕上显示动画。此示例托管在由 CompositionTarget.Rendering 上的 DPFCanvas 呈现的 DX10ImageSource 中。
这个例子再简单不过了,在 WPF 中渲染 D3DImage 时,它几乎是“接近金属”的。一个三角形,通过渲染之间的时间差计算的值在屏幕上平移。口吃仍然存在,来来去去,好像某种同步问题。它让我感到困惑,但本质上使 SharpDX 在 WPF 中无法用于任何类型的流畅动画,这非常令人失望。
如果有人有兴趣重现此问题,请在此处获得 SharpDX 示例:https ://github.com/sharpdx/SharpDX-Samples
我对 WPFHost 示例进行了以下简单更改:
void IScene.Render()
{
...
EffectMatrixVariable wv = this.SimpleEffect.GetVariableBySemantic("WorldView").AsMatrix();
wv.SetMatrix(this.WorldViewMatrix);
...
}
void IScene.Update(TimeSpan sceneTime)
{
float x = (float)sceneTime.Milliseconds * 0.001f - 0.5f;
WorldViewMatrix = Matrix.Translation(x, 0, 0);
}
在着色器 Simple.fx 中:
float4x4 WorldViewTransform : WorldView;
PS_IN VS( VS_IN input )
{
PS_IN output = (PS_IN)0;
output.pos = mul(input.pos, WorldViewTransform);
output.col = input.col * Overlay;
return output;
}