Invalid Call on ReleaseFrame() in DesktopDuplication API
Asked Answered
A

1

7

An user of my app is having some problem capturing the screen with the DesktopDuplication API.

Upon starting the capture, the app crashes because the app is unable to release the frame of the OutputDuplication.

User's PC details:

Windows 10.0.18362.0, 64 bits
Nvidia GeForce GTX 960, driver version 441.66

Error logs:

▬ Message - 
HRESULT: [0x887A0001], Module: [SharpDX.DXGI], ApiCode: [DXGI_ERROR_INVALID_CALL/InvalidCall], 
Message: The application made a call that is invalid. Either the parameters of the call or the state of some object was incorrect.
Enable the D3D debug layer in order to see details via debug messages.

○ Type - 
    SharpDX.SharpDXException
▲ Source - 
    SharpDX
▼ TargetSite - 
    Void CheckError()
♠ StackTrace - 
   at SharpDX.Result.CheckError()
   at SharpDX.DXGI.OutputDuplication.ReleaseFrame()
   at MyApp.Capture.DirectImageCapture.CaptureWithCursor(FrameInfo frame)

----------------------------------

▬ Message - 
    Object reference not set to an instance of an object.
○ Type - 
    System.NullReferenceException
▲ Source - 
    SharpDX.Direct3D11
▼ TargetSite - 
    Void GetDescription(SharpDX.Direct3D11.Texture2DDescription ByRef)
♠ StackTrace - 
   at SharpDX.Direct3D11.Texture2D.GetDescription(Texture2DDescription& descRef)
   at MyApp.Capture.DirectImageCapture.GetCursor(Texture2D screenTexture, OutputDuplicateFrameInformation info, FrameInfo frame)
   at MyApp.Capture.DirectImageCapture.CaptureWithCursor(FrameInfo frame)

The user is able to capture the screen when the mouse cursor is not being captured, so there must be something wrong with the capture method that also captures the cursor.

    var res = Result.Ok;

    try
    {
        //Try to get the duplicated output frame within given time.
        res = DuplicatedOutput.TryAcquireNextFrame(0, out var info, out var resource);

        //Checks how to proceed with the capture. It could have failed, or the screen, cursor or both could have been captured.
        if (res.Failure || resource == null || (info.AccumulatedFrames == 0 && info.LastMouseUpdateTime <= LastProcessTime))
        {
            //Somehow, it was not possible to retrieve the resource, frame or metadata.
            //frame.WasDropped = true;
            //BlockingCollection.Add(frame);

            resource?.Dispose();
            return FrameCount;
        }
        else if (info.AccumulatedFrames == 0 && info.LastMouseUpdateTime > LastProcessTime)
        {
            //Gets the cursor shape if the screen hasn't changed in between, so the cursor will be available for the next frame.
            GetCursor(null, info, frame);
            return FrameCount;
        }

        //Saves the most recent capture time.
        LastProcessTime = Math.Max(info.LastPresentTime, info.LastMouseUpdateTime);

        //Copy resource into memory that can be accessed by the CPU.
        using (var screenTexture = resource.QueryInterface<Texture2D>())
        {
            //Copies from the screen texture only the area which the user wants to capture.
            Device.ImmediateContext.CopySubresourceRegion(screenTexture, 0, new ResourceRegion(TrueLeft, TrueTop, 0, TrueRight, TrueBottom, 1), BackingTexture, 0);

            //Copy the captured desktop texture into a staging texture, in order to show the mouse cursor and not make the captured texture dirty with it.
            Device.ImmediateContext.CopyResource(BackingTexture, StagingTexture);

            //Gets the cursor image and merges with the staging texture.
            GetCursor(StagingTexture, info, frame);
        }

        //Get the desktop capture texture.
        var data = Device.ImmediateContext.MapSubresource(StagingTexture, 0, MapMode.Read, MapFlags.None);

        if (data.IsEmpty)
        {
            //frame.WasDropped = true;
            //BlockingCollection.Add(frame);

            Device.ImmediateContext.UnmapSubresource(StagingTexture, 0);
            resource.Dispose();
            return FrameCount;
        }

        #region Get image data

        var bitmap = new System.Drawing.Bitmap(Width, Height, PixelFormat.Format32bppArgb);
        var boundsRect = new System.Drawing.Rectangle(0, 0, Width, Height);

        //Copy pixels from screen capture Texture to the GDI bitmap.
        var mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat);
        var sourcePtr = data.DataPointer;
        var destPtr = mapDest.Scan0;

        for (var y = 0; y < Height; y++)
        {
            //Copy a single line.
            Utilities.CopyMemory(destPtr, sourcePtr, Width * 4);

            //Advance pointers.
            sourcePtr = IntPtr.Add(sourcePtr, data.RowPitch);
            destPtr = IntPtr.Add(destPtr, mapDest.Stride);
        }

        //Release source and dest locks.
        bitmap.UnlockBits(mapDest);

        //Set frame details.
        FrameCount++;
        frame.Path = $"{Project.FullPath}{FrameCount}.png";
        frame.Delay = FrameRate.GetMilliseconds(SnapDelay);
        frame.Image = bitmap;
        BlockingCollection.Add(frame);

        #endregion

        Device.ImmediateContext.UnmapSubresource(StagingTexture, 0);

        resource.Dispose();
        return FrameCount;
    }
    catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code)
    {
        return FrameCount;
    }
    catch (SharpDXException se) when (se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceRemoved.Result.Code || se.ResultCode.Code == SharpDX.DXGI.ResultCode.DeviceReset.Result.Code)
    {
        //When the device gets lost or reset, the resources should be instantiated again.
        DisposeInternal();
        Initialize();

        return FrameCount;
    }
    catch (Exception ex)
    {
        LogWriter.Log(ex, "It was not possible to finish capturing the frame with DirectX.");

        OnError.Invoke(ex);
        return FrameCount;
    }
    finally
    {
        try
        {
            //Only release the frame if there was a success in capturing it.
            if (res.Success)
                DuplicatedOutput.ReleaseFrame();
        }
        catch (Exception e)
        {
            LogWriter.Log(e, "It was not possible to release the frame.");

            //HERE
            //What should I do after the frame is not released properly?
            //Should I reset the whole capture?
            //DisposeInternal();
            //Initialize();
        }
    }

When the capture finishes for each frame, the DuplicatedOutput must release the frame.

But when the release fails with InvalidCall, what should I do?
Also, how can I debug this kind of error on a users PC (not on a developer's machine)?


EDIT:

This is what I'm trying to do:

With the Graphics Tools enabled on Windows Settings, I added this code to the capture initialization:

#if DEBUG
    Device = new Device(DriverType.Hardware, DeviceCreationFlags.VideoSupport | DeviceCreationFlags.Debug);

    var debug = InfoQueue.TryCreate();
    debug.SetBreakOnSeverity(DebugId.All, InformationQueueMessageSeverity.Corruption, true);
    debug.SetBreakOnSeverity(DebugId.All, InformationQueueMessageSeverity.Error, true);
    debug.SetBreakOnSeverity(DebugId.All, InformationQueueMessageSeverity.Warning, true);

    var debug2 = DXGIDebug.TryCreate();
    debug2.ReportLiveObjects(DebugId.Dx, DebugRloFlags.Summary | DebugRloFlags.Detail);

#else

Then I set the app to run on the dedicated GPU on my notebook, since I know for sure that it will cause an InvalidException.

I ran the app and tried to output1.DuplicateOutput(Device); and it failed as expected.

After that I tried to run the app while DebugView was running too and it only gave me some messages when closing the app, not when having the error.

00000001    0.00000000  [14488] OnFocusWindowChanged to Lizard Mode 
00000002    0.39583239  [14488] Lizard Mode: Unprivileged process   
00000003    0.39594769  [14488] Lizard Mode: Restoring app mapping  
00000004    9.81729603  [21620] D3D11 WARNING: Process is terminating. Using simple reporting. Please call ReportLiveObjects() at runtime for standard reporting. [ STATE_CREATION WARNING #0: UNKNOWN] 
00000005    9.81732273  [21620] D3D11: **BREAK** enabled for the previous message, which was: [ WARNING STATE_CREATION #0: UNKNOWN ]    
00000006    9.81803799  [21620] DXGI WARNING: Process is terminating. Using simple reporting. Please call ReportLiveObjects() at runtime for standard reporting. [ STATE_CREATION WARNING #0: ] 
00000007    9.81806469  [21620] DXGI: **BREAK** enabled for the previous message, which was: [ WARNING STATE_CREATION #0:  ]    
00000008    10.78524113 [14488] Lizard Mode: Privileged process 
00000009    10.78589630 [14488] Lizard Mode: Reverting to default M/KB Configuration    
00000010    10.78692913 [14488] OnFocusWindowChanged to Lizard Mode 

So, I tried to capture errors using the dxcap, using this command:

dxcap -debug -toXML '[path]\debug.xml' -file '[path]\debug.vsglog' -c '[path]\bin\Debug\MyApp.exe'

Unfortunatelly, CreateDevice() fails with:

HRESULT: [0x887A0004], Module: [SharpDX.DXGI], ApiCode: [DXGI_ERROR_UNSUPPORTED/Unsupported], Message: The specified device interface or feature level is not supported on this system.

Then I tried again, but this time with only DeviceCreationFlags.Debug and it worked. I'm still analyzing the file.

Debug

Afterpiece answered 12/2, 2020 at 23:31 Comment(17)
So, have you enabled the debug layer? It's often helpful.Bearskin
I know how to enabled it, but I don't know what to do with it. Is not crashing on my machine. How can I tell users to test it for me?Afterpiece
If it's enabled and working (you should test it on your side first make sure it works with some error you create artificially), you can ask them to run DebugView from sysinternals learn.microsoft.com/en-us/sysinternals/downloads/debugview for example during the crash there will be some message output. You can filter using some text.Bearskin
Just to check, to enable I have to create the device using DeviceCreationFlags.Debug and have the D3D11*SDKLayers.dll in the same folder as the app, right?Afterpiece
I added the dlls to the folder where the app is located and tried to make it crash with an InvalidCall (trying to run on a notebook, on the dedicated GPU) and DebugView gave me nothing besides some system trace events.Afterpiece
How the debug layer must be configured is dependent on Windows version: walbourn.github.io/direct3d-sdk-debug-layer-tricks read carefully the docs and make sure you can get output, and yes, a generic invalid call typically comes with more explanation, for example imgur.com/a/Q6lex7j (I tried to resize a swapchain that was used).Bearskin
First statement is wrong. If TryAcquireNextFrame() fails with an exception then the finally block will call ReleaseFrame() incorrectly.Shaia
Having looked at the code, I think that the problem description is misleading. The reason is incorrectly structured acquire/release loop rather than external cause. Debug layer will not help. Just make sure ReleaseFrame is called if and only if necessary.Suet
@HansPassant Makes sense, I replaced with var res = new Result(-1);Afterpiece
@RomanR. I would still need to know why would TryAcquireNextFrame throw an exception anyway.Afterpiece
@RomanR. Also, the other method which does not capture the cursor is not having the same issue. (It's basically the same code, except the GetCursor() part)Afterpiece
You are mixing issues here. The subject says failure in ReleaseFrame and this is what my comment above refers to. AcquireFrame has its own good reasons for this or similar error.Suet
@RomanR. You are right, I mixed up.Afterpiece
FYI, your program (latest version from github) continuously outputs errors in the debug layer when capturing (CaptureWithCursor): pastebin.com/raw/JTWjrqQBBearskin
Thanks. I would like to know to make my DebugView work in order to see that. Not sure what's wrong. What's even stranger is that it works normally for me, but for some few users, it does not.Afterpiece
@SimonMourier Apparently, that was the problem. I fixed the bug and sent to an user and it is no longer crashing. Strange that it was not crashing on my end.Afterpiece
Cool. I personally always enable the debug layer, never develop w/o it enabled and effective.Bearskin
A
1

It seems like we forked the same code for C# desktop duplication, I also had some problems with releasing frames. I modified it a little bit and at the moment I don't have any problems on my (still incomplete) ambilight app. You can take a look at the code here: https://github.com/leocb/Ambilight feel free to copy any portion of the code you like. The relevant part for you is inside the DesktopDuplication project, also check the functions I use on the ConfigForm.cs

I ignore any errors ReleaseFrame() may throw by putting it inside a blank try/catch. IMO, the only important one you should check is DXGI_ERROR_ACCESS_LOST, because in this case, you'll need a new IDXGIOutputDuplication instance (I'm not doing this on my code yet). I couldn't measure any performance loss by doing this.

Make sure you're initializing the SharpDX correctly (device, factory, etc..) and setting the correct flags for the surface texture

I recommend using a double buffer for the GDI image, this avoids race condition errors and improves performance

Also, for performance reasons, I execute the capture code on another thread and I only call ReleaseFrame() just before doing another capture, as recommended by microsoft:

[...] we recommend that you release the frame just before you call the IDXGIOutputDuplication::AcquireNextFrame method to acquire the next frame. When the client does not own the frame, the operating system copies all desktop updates to the surface.

If this all fails, a "simpler" solutions would be to capture the mouse position with standard c# code and draw it on top the screen capture

Adelbert answered 4/3, 2020 at 14:0 Comment(1)
My issue was with another part of the code. The GetCursor(StagingTexture, info, frame); call. It was trying to copy a part of the texture that was out of bounds, which cause a crash on some systems, but not mine for some reason. I had to enable the debug layer in order to have a crash too.Afterpiece

© 2022 - 2024 — McMap. All rights reserved.