D3DImage and SharpDX flickering on slow hardware

2019-03-30 18:41发布

问题:

I am using the SharpDX.WPF project for the WPF abilities, it seems like an easy to understand low-overhead library, compared to the Toolkit that comes with SharpDX (which has the same issue!)

First: I fixed the SharpDX.WPF project for the latest SharpDX using the following: https://stackoverflow.com/a/19791534/442833

Then I made the following hacky adjustment to DXElement.cs, a solution that was also done here:

private Query queryForCompletion;
    public void Render()
    {
        if (Renderer == null || IsInDesignMode)
            return;

        var test = Renderer as D3D11;
        if (queryForCompletion == null)
        {

            queryForCompletion = new Query(test.Device,
                new QueryDescription {Type = QueryType.Event, Flags = QueryFlags.None});
        }

        Renderer.Render(GetDrawEventArgs());

        Surface.Lock();
        test.Device.ImmediateContext.End(queryForCompletion);
        // wait until drawing completes
        Bool completed;
        var counter = 0;
        while (!(test.Device.ImmediateContext.GetData(queryForCompletion, out completed)
                 && completed))
        {
            Console.WriteLine("Yielding..." + ++counter);
            Thread.Yield();
        }
        //Surface.Invalidate();
        Surface.AddDirtyRect(new Int32Rect(0, 0, Surface.PixelWidth, Surface.PixelHeight));
        Surface.Unlock();
    }

Then I render 8000 cubes in a cube pattern...

Yielding...

gets printed to the console quite often, but the flickering is still there. I am assuming that WPF is nice enough to show the image using a different thread before the rendering is done, not sure though... This same issue also happens when I use the Toolkit variant of WPF support with SharpDX.

Images to demonstate the issue:

  • Bad
  • Better
  • Almost
  • Intended

Note: It randomly switches between these old images, randomly. I am also using really old hardware which makes the flickering much more appearant (GeForce Quadro FX 1700)

A made a repo which contains the exact same source-code as I am using to get this issue: https://github.com/ManIkWeet/FlickeringIssue/

回答1:

I think you are not locking properly. As far as I understand the MSDN documentation you are supposed to lock during the entire rendering not just at the end of it:

While the D3DImage is locked, your application can also render to the Direct3D surface assigned to the back buffer.

The information you find on the net about D3DImage/SharpDX is somewhat confusing because the SharpDX guys don't really like the way D3DImage is implemented (can't blame them), so there are statements about this being a "bug" on Microsofts side when its actually just improper usage of the API.

Yes, locking during rendering has performance issues, but it is probably not possible to fix them without porting WPF to DirectX11 and implementing something like a SwapChainPanel which is available in UWP apps. (WPF itself still runs on DirectX9)

If the locking is a performance issue for you, one idea I had (but never tested) is that you could render to an offscreen surface and reduce the lock duration to copying that surface over to the D3DImage. No idea if that would help performance wise but its something to try.



回答2:

Related to D3DImage locking, note that the D3DImage.TryLock API has rather unconventional semantics which most developers would not expect:

Beware!
You must call Unlock even in the case where TryLock indicates failure (i.e., returns false)

Although perhaps more of an alarming design choice than a bug per se, misunderstanding this behavior will trivially result in D3DImage deadlocks and hangs, and thus might be responsible for much of the frustration people experience in attempting to get D3DImage working properly.

The following code is a correct WPF D3D render with no flicker in my app:

void WPF_D3D_render(IntPtr pSurface)
{
    if (TryLock(new Duration(default(TimeSpan))))
    {
        SetBackBuffer(D3DResourceType.IDirect3DSurface9, pSurface);
        AddDirtyRect(new Int32Rect(0, 0, PixelWidth, PixelHeight));
    }
    Unlock();    //  <--- !
}

Yes, this unintuitive code is actually correct; it is the case that that D3DImage.TryLock(0) leaks one internal D3D buffer lock every time it returns failure. You don't have to take my word for it, here's the CLR code from PresentationCore.dll v4.0.30319:

private bool LockImpl(Duration timeout)
{
    bool flag = false;

    if (_lockCount == uint.MaxValue)
        throw new InvalidOperationException();

    if (_lockCount == 0)
    {
        if (timeout == Duration.Forever)
            flag = _canWriteEvent.WaitOne();
        else
            flag = _canWriteEvent.WaitOne(timeout.TimeSpan, false);

        UnsubscribeFromCommittingBatch();
    }
    _lockCount++;
    return flag;
}

Notice that the internal _lockCount field is incremented regardless of whether the function returns success or failure. You have to call Unlock() yourself, as shown in the first code example above, if you want to avoid certain deadlock. Failing to do so creates is nasty to debug, too, because the component won't (potentially) deadlock until the next render pass, by which time the relevant evidence is long gone.

The unusual behavior does not seem to be mentioned at MSDN, but to be fair, that documentation doesn't note that you have to call Unlock() if the call is successful, either.



回答3:

The problem is not the Locking mechanism. Normally you use Present to draw to present the image. Present will wait until all drawing is ready. With D3DImage you are not using the Present() method. Instead of Presenting, you lock, adding a DirtyRect and unlock the D3DImage.

The rendering is done asynchrone so when you are unlocking, the draw actions might not be ready. This is causing the flicker effect. Sometimes you see items half drawn. A poor solution (i've tested with) is adding a small delay before unlocking. It helped a little, but it wasn't a neat solution. It was terrible!

Solution:

I continued with something else; I was expirimenting with MSAA (antialiasing) and the first problem I faced was; MSAA cannot be done on the dx11/dx9 shared texture, so i decided to render to a new texture (dx11) and create a copy to the dx9 shared texture. I slammed my head on the tabel, because now it was anti-aliased AND flicking-free!! Don't forget to call Flush() before adding a dirty rect.

So, creating a copy of the texture: DXDevice11.Device.ImmediateContext.ResolveSubresource(_dx11RenderTexture, 0, _dx11BackpageTexture, 0, ColorFormat); (_dx11BackpageTexture is shared texture) will wait until the rendering is ready and will create a copy.

This is how I got rid of the flickering....