如何检测WPF控件何时被重绘?

6
我正在使用D3DImage来显示一系列在同一Direct3D Surface上渲染的帧。我的当前逻辑如下:
  • 显示最后渲染的帧(即 D3DImage.Lock()/AddDirtyRect()/Unlock())
  • 开始渲染下一帧
  • 等待下一帧准备就绪并且到了展示它的时间
  • 显示最后渲染的帧
  • ...
这种方法的问题在于,当我们在D3DImage上调用Unlock()时,图像实际上没有被复制,只是在下一个WPF渲染时计划复制。因此,在WPF有机会显示它之前,我们可能在Direct3D表面上渲染新帧。结果是我们在显示器上看到了错过的帧。
现在我正在尝试使用单独的Direct3D纹理进行渲染,并在显示之前执行复制到“显示纹理”,这样可以获得更好的结果,但会产生相当大的开销。最好能够知道D3DImage何时完成刷新,并在立即开始渲染下一帧。如果可能的话,如何实现?或者您还有更好的想法吗?
谢谢。

CompositionTarget.Rendering 有用吗? - Kent Boogaart
我的理解是,这个事件是在渲染之前调用的,而不是之后。如果我们必须等到下一个渲染事件才能知道上一个已经完成,那么开始渲染帧就太晚了。理想情况下,我们希望在 WPF 的渲染线程完成重新绘制 D3DImage 或其包含的 UIElement 后立即被调用。 - Asik
你能否提前完成所有渲染工作(急切地),然后使用 CompositionTarget.Rendering 作为触发器,在你的 D3DImage 上调用 Unlock()(如果你的渲染准备就绪)并安排另一个渲染?这意味着你只会以 WPF 的帧速率(但可能少于此速率)渲染和显示图像。 - Kent Boogaart
这会减少可能性,但无法消除在Unlock()之后调度另一个渲染造成竞态条件的可能性:帧可能在控件实际重绘之前完成渲染。 - Asik
我不明白。一旦调用了Unlock(),你就可以立即开始另一个对D3DImage的渲染。 调用Lock()会阻塞,直到WPF渲染线程将后备缓冲区复制到前缓冲区为止。完成渲染后,在下一个CompositionTarget.Rendering之前等待才调用Unlock()并重复该过程。如果在控件重新绘制之前完成渲染,这根本没有关系,因为它是从前缓冲区而不是你正在修改的后缓冲区重新绘制的。 - Kent Boogaart
显示剩余3条评论
2个回答

2
CompositionTarget.Rendering事件在WPF将要渲染时被调用,因此您应该在此时执行Lock()Unlock()。在Unlock()之后,您可以启动下一次渲染。

您还应该检查RenderingTime,因为该事件可能每帧触发多次。尝试使用以下代码:

private void HandleWpfCompositionTargetRendering(object sender, EventArgs e)
{
    RenderingEventArgs rea = e as RenderingEventArgs;

    // It's possible for Rendering to call back twice in the same frame
    // so only render when we haven't already rendered in this frame.
    if (this.lastRenderTime == rea.RenderingTime)
        return;

    if (this.renderIsFinished)
    {
        // Lock();
        // SetBackBuffer(...);
        // AddDirtyRect(...);
        // Unlock();

        this.renderIsFinished = false;
        // Fire event to start new render
        // the event needs to set this.renderIsFinished = true when the render is done

        // Remember last render time
        this.lastRenderTime = rea.RenderingTime;
    }
}

更新以回应评论

您确定存在竞态条件吗?此页面指出,在调用 Unlock() 时会复制后备缓冲区。

如果确实存在竞态条件,那么在渲染代码周围放置 Lock/Unlock 怎么样?此页面指出,Lock() 将阻塞,直到复制完成。


是的,正如文档所示(它被“标记为渲染”),存在竞争条件,我已经在反编译器中查看了代码。我还以大约4种不同的方式验证了每次调用Lock()/Unlock()时,我的d3d帧内容都是一个单独的新帧,因此这里的问题实际上是让D3DImage在我渲染下一个之前复制缓冲区。额外的Lock()/Unlock()调用没有帮助;只有当控件当前正在渲染时,Lock()才会阻塞。 - Asik
@Asik 我在回答中提到的 Lock() 链接表示“调用 Lock 方法会阻塞,直到渲染线程将后备缓冲区的内容复制到前置缓冲区为止”。 - shoelzer
@Asik 我不确定这个,但我认为“提交更改”发生在你解锁(Unlock())时。 - shoelzer
@Asik:“调用Lock方法会阻塞,直到渲染线程完成复制…” - shoelzer
@Asik,我想指出Lock()函数会关注渲染线程,这也是你在之前评论中所担心的。我认为,在获取锁之后,从后台缓冲区复制到前台缓冲区的操作已经完成,因此您可以对后台缓冲区进行任何想做的操作,而不受WPF渲染的影响。如果您已经测试过并且情况并非如此,那么很抱歉我无法再提供更多帮助了。 - shoelzer
显示剩余6条评论

0

看起来这样做的清晰方式,为了与UI并行渲染,是将其渲染到单独的D3D表面,并在Lock()和Unlock()之间将其复制到显示表面(即传递给SetBackBuffer的表面)。因此,算法变为:

  1. 复制并显示上一帧渲染结果,即
    • Lock()
    • 从渲染表面复制到显示表面
    • SetBackBuffer(displaySurface)
    • AddDirtyRect()
    • Unlock()
  2. 安排新的渲染到渲染表面
  3. 等待它完成并且时机合适以显示它
  4. 回到1

D3DImage的文档明确说明

在D3DImage未锁定时,请勿更新Direct3D表面。

这里的痛点在于复制,如果硬件繁忙,可能会很昂贵(即>2ms)。为了在D3DImage解锁时使用显示表面(避免在渲染时进行潜在昂贵的操作),人们不得不诉诸反汇编和反射来钩取D3DImage自身的渲染...

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