How to detect when a WPF control has been redrawn?

丶灬走出姿态 提交于 2021-02-19 03:33:09

问题


I am using D3DImage to display a sequence of frames that are rendered unto the same Direct3D Surface one after the other. My current logic is thus:

  • Display last rendered frame (i.e.D3DImage.Lock()/AddDirtyRect()/Unlock())
  • Start rendering next frame
  • Wait for next frame to be ready and that it's time to display it
  • Display last rendered frame
  • ...

The problem with this approach is that when we are done calling Unlock() on D3DImage, the image isn't actually copied, it's only scheduled to be copied on the next WPF render. It's therefore possible that we render a new frame on the Direct3D surface before WPF has had the chance to display it. The net result is that we see missed frames on the display.

Right now I'm experimenting with using a separate Direct3D texture for rendering and performing a copy to a "display texture" just before display, which is giving better results but incurs substantial overhead. It would be preferrable to just be able to know when D3DImage is done refreshing and start rendering the next frame immediately after. Is this possible, if so how? Or do you have a better idea altogether?

Thanks.


回答1:


The CompositionTarget.Rendering event is called when WPF is going to render, so that's when you should do your Lock() and Unlock(). After the Unlock(), you can kick off the next render.

You should also check the RenderingTime because the event may fire multiple times per frame. Try something like this:

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

Update to address comments

Are you sure that there's a race condition? This page says that the back buffer gets copied when you call Unlock().

And if there really is a race condition, how about putting Lock/Unlock around the render code? This page says that Lock() will block until the copy is finished.




回答2:


It looks like the clean way of doing this, in order to render in parallel with the UI, is to render into a separate D3D surface, and copy it to the display surface (i.e. the one passed to SetBackBuffer) between the calls to Lock() and Unlock(). So the algorithm becomes:

  1. Copy and display the last rendered frame, i.e.
    • Lock()
    • Copy from render to display surface
    • SetBackBuffer(displaySurface)
    • AddDirtyRect()
    • Unlock()
  2. Schedule a new render to the render surface
  3. Wait for it to complete and that the timing is ok to display it
  4. Goto 1

The documentation for D3DImage explicitely states:

Do not update the Direct3D surface while the D3DImage is unlocked.

The sore point here is the copy, which is potentially costly (i.e. >2ms if the hardware is busy). In order to use the display surface while the D3DImage is unlocked (avoiding a potentially costly operation at render time), one would have to resort to disassembly and reflection to hook into D3DImage's own rendering...



来源:https://stackoverflow.com/questions/16482841/how-to-detect-when-a-wpf-control-has-been-redrawn

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!