简体   繁体   English

DirectX屏幕捕获 - 桌面复制API - AcquireNextFrame的有限帧速率

[英]DirectX Screen Capture - Desktop Duplication API - limited frame rate of AcquireNextFrame

I'm trying to use Windows Desktop Duplication API to capture the screen and save the raw output to a video. 我正在尝试使用Windows 桌面复制API来捕获屏幕并将原始输出保存到视频中。 I'm using AcquireNextFrame with a very high timeout value (999ms). 我正在使用具有非常高的超时值(999ms)的AcquireNextFrame This way I should get every new frame from windows as soon as it at has one, which naturally should be at 60fps anyway. 这样我就可以从窗口获得每个新帧,只要它有一个,无论如何自然应该是60fps。 I end up getting sequences where everything looks good (frame 6-11), and then sequences where things look bad (frame 12-14). 我最终获得了一切看起来都很好的序列(第6-11帧),然后是事物看起来很糟糕的序列(第12-14帧)。 If I check AccumulatedFrames 如果我检查AccumulatedFrames

lFrameInfo.AccumulatedFrames

the value is often 2 or higher. 该值通常为2或更高。 From my understanding, this means windows is saying "hey hold up, I don't have a frame for you yet", because calls to AcquireNextFrame take so long. 根据我的理解,这意味着Windows会说“嘿嘿,我还没有框架”,因为调用AcquireNextFrame花了这么长时间。 But once windows does finally give me a frame, it is saying "hey you were actually too slow and ended up missing a frame". 但是,一旦Windows终于给了我一个框架,它就是说“嘿,你实际上太慢了,最后错过了一个框架”。 If i could somehow get these frames I think I would be getting 60hz. 如果我能以某种方式获得这些帧,我想我会得到60hz。

This can be further clarified with logging: 这可以通过记录进一步说明:

I0608 10:40:16.964375  4196 window_capturer_dd.cc:438] 206 - Frame 6 start acquire
I0608 10:40:16.973867  4196 window_capturer_dd.cc:451] 216 - Frame 6 acquired
I0608 10:40:16.981364  4196 window_capturer_dd.cc:438] 223 - Frame 7 start acquire
I0608 10:40:16.990864  4196 window_capturer_dd.cc:451] 233 - Frame 7 acquired
I0608 10:40:16.998364  4196 window_capturer_dd.cc:438] 240 - Frame 8 start acquire
I0608 10:40:17.007876  4196 window_capturer_dd.cc:451] 250 - Frame 8 acquired
I0608 10:40:17.015393  4196 window_capturer_dd.cc:438] 257 - Frame 9 start acquire
I0608 10:40:17.023905  4196 window_capturer_dd.cc:451] 266 - Frame 9 acquired
I0608 10:40:17.032411  4196 window_capturer_dd.cc:438] 274 - Frame 10 start acquire
I0608 10:40:17.039912  4196 window_capturer_dd.cc:451] 282 - Frame 10 acquired
I0608 10:40:17.048925  4196 window_capturer_dd.cc:438] 291 - Frame 11 start acquire
I0608 10:40:17.058428  4196 window_capturer_dd.cc:451] 300 - Frame 11 acquired
I0608 10:40:17.065943  4196 window_capturer_dd.cc:438] 308 - Frame 12 start acquire
I0608 10:40:17.096945  4196 window_capturer_dd.cc:451] 336 - Frame 12 acquired
I0608 10:40:17.098947  4196 window_capturer_dd.cc:464] 1 FRAMES MISSED on frame: 12
I0608 10:40:17.101444  4196 window_capturer_dd.cc:438] 343 - Frame 13 start acquire
I0608 10:40:17.128958  4196 window_capturer_dd.cc:451] 368 - Frame 13 acquired
I0608 10:40:17.130957  4196 window_capturer_dd.cc:464] 1 FRAMES MISSED on frame: 13
I0608 10:40:17.135459  4196 window_capturer_dd.cc:438] 377 - Frame 14 start acquire
I0608 10:40:17.160959  4196 window_capturer_dd.cc:451] 399 - Frame 14 acquired
I0608 10:40:17.162958  4196 window_capturer_dd.cc:464] 1 FRAMES MISSED on frame: 14

Frame 6-11 look good, the acquires are roughly 17ms apart. 6-11帧看起来不错,获得的距离大约相差17毫秒。 Frame 12 should be acquired at (300+17=317ms). 帧12应在(300 + 17 = 317ms)处获得。 Frame 12 starts waiting at 308, but doesn't get anything until 336ms. 第12帧开始在308等待,但直到336ms才得到任何东西。 Windows didn't have anything for me until the frame after (300+17+17~=336ms). Windows之前的帧没有任何东西(300 + 17 + 17~ = 336ms)。 Okay sure maybe windows just missed a frame, but when I finally get it, I can check AccumulatedFrames and its value was 2 (meaning I missed a frame because I waited too long before calling AcquireNextFrame). 好吧肯定也许窗户错过了一个框架,但是当我最终得到它时,我可以检查AccumulatedFrames并且它的值是2(意思是我错过了一个框架,因为我在调用AcquireNextFrame之前等了太久)。 In my understanding, it only makes sense for AccumulatedFrames to be larger than 1 if AcquireNextFrame returns immediately. 根据我的理解,如果AcquireNextFrame立即返回,则AccumulatedFrames大于1才有意义。

Furthermore, I can use PresentMon while my capture software is running. 此外,我可以在我的捕获软件运行时使用PresentMon。 The logs show MsBetweenDisplayChange for every frame, which is fairly steady at 16.666ms (with a couple outliers, but much less than my capture software is seeing). 日志显示每帧的MsBetweenDisplayChange,相当稳定在16.666ms(有几个异常值,但比我的捕获软件看到的要少得多)。

These people ( 1 , 2 ) seem to have been able to get 60fps, so I'm wondering what I am doing incorrectly. 这些人( 12 )似乎已经能够得到60fps的,所以我不知道我在做什么错误。

My code is based on this : 我的代码基于

int main() {
    int FPS = 60;
    int video_length_sec = 5;

    int total_frames = FPS * video_length_sec;
    for (int i = 0; i < total_frames; i++) {
        if(!CaptureSingleFrame()){
            i--;
        }
    }
}

ComPtr<ID3D11Device> lDevice;
ComPtr<ID3D11DeviceContext> lImmediateContext;
ComPtr<IDXGIOutputDuplication> lDeskDupl;
ComPtr<ID3D11Texture2D> lAcquiredDesktopImage;
ComPtr<ID3D11Texture2D> lGDIImage;
ComPtr<ID3D11Texture2D> lDestImage;
DXGI_OUTPUT_DESC lOutputDesc;
DXGI_OUTDUPL_DESC lOutputDuplDesc;
D3D11_TEXTURE2D_DESC desc;

// Driver types supported
D3D_DRIVER_TYPE gDriverTypes[] = {
    D3D_DRIVER_TYPE_HARDWARE
};
UINT gNumDriverTypes = ARRAYSIZE(gDriverTypes);

// Feature levels supported
D3D_FEATURE_LEVEL gFeatureLevels[] = {
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_1
};
UINT gNumFeatureLevels = ARRAYSIZE(gFeatureLevels);


bool Init() {
    int lresult(-1);

    D3D_FEATURE_LEVEL lFeatureLevel;

    HRESULT hr(E_FAIL);

    // Create device
    for (UINT DriverTypeIndex = 0; DriverTypeIndex < gNumDriverTypes; ++DriverTypeIndex)
    {
        hr = D3D11CreateDevice(
            nullptr,
            gDriverTypes[DriverTypeIndex],
            nullptr,
            0,
            gFeatureLevels,
            gNumFeatureLevels,
            D3D11_SDK_VERSION,
            &lDevice,
            &lFeatureLevel,
            &lImmediateContext);

        if (SUCCEEDED(hr))
        {
            // Device creation success, no need to loop anymore
            break;
        }

        lDevice.Reset();

        lImmediateContext.Reset();
    }

    if (FAILED(hr))
        return false;

    if (lDevice == nullptr)
        return false;

    // Get DXGI device
    ComPtr<IDXGIDevice> lDxgiDevice;
    hr = lDevice.As(&lDxgiDevice);

    if (FAILED(hr))
        return false;

    // Get DXGI adapter
    ComPtr<IDXGIAdapter> lDxgiAdapter;
    hr = lDxgiDevice->GetParent(
        __uuidof(IDXGIAdapter), &lDxgiAdapter);

    if (FAILED(hr))
        return false;

    lDxgiDevice.Reset();

    UINT Output = 0;

    // Get output
    ComPtr<IDXGIOutput> lDxgiOutput;
    hr = lDxgiAdapter->EnumOutputs(
        Output,
        &lDxgiOutput);

    if (FAILED(hr))
        return false;

    lDxgiAdapter.Reset();

    hr = lDxgiOutput->GetDesc(
        &lOutputDesc);

    if (FAILED(hr))
        return false;

    // QI for Output 1
    ComPtr<IDXGIOutput1> lDxgiOutput1;
    hr = lDxgiOutput.As(&lDxgiOutput1);

    if (FAILED(hr))
        return false;

    lDxgiOutput.Reset();

    // Create desktop duplication
    hr = lDxgiOutput1->DuplicateOutput(
        lDevice.Get(), //TODO what im i doing here
        &lDeskDupl);

    if (FAILED(hr))
        return false;

    lDxgiOutput1.Reset();

    // Create GUI drawing texture
    lDeskDupl->GetDesc(&lOutputDuplDesc);
    desc.Width = lOutputDuplDesc.ModeDesc.Width;
    desc.Height = lOutputDuplDesc.ModeDesc.Height;
    desc.Format = lOutputDuplDesc.ModeDesc.Format;
    desc.ArraySize = 1;
    desc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_RENDER_TARGET;
    desc.MiscFlags = D3D11_RESOURCE_MISC_GDI_COMPATIBLE;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.MipLevels = 1;
    desc.CPUAccessFlags = 0;
    desc.Usage = D3D11_USAGE_DEFAULT;


    hr = lDevice->CreateTexture2D(&desc, NULL, &lGDIImage);

    if (FAILED(hr))
        return false;

    if (lGDIImage == nullptr)
        return false;

    // Create CPU access texture
    desc.Width = lOutputDuplDesc.ModeDesc.Width;
    desc.Height = lOutputDuplDesc.ModeDesc.Height;
    desc.Format = lOutputDuplDesc.ModeDesc.Format;
    std::cout << desc.Width << "x" << desc.Height << "\n\n\n";
    desc.ArraySize = 1;
    desc.BindFlags = 0;
    desc.MiscFlags = 0;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.MipLevels = 1;
    desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
    desc.Usage = D3D11_USAGE_STAGING;

    return true;
}

void WriteFrameToCaptureFile(ID3D11Texture2D* texture) {

    D3D11_MAPPED_SUBRESOURCE* pRes = new D3D11_MAPPED_SUBRESOURCE;
    UINT subresource = D3D11CalcSubresource(0, 0, 0);

    lImmediateContext->Map(texture, subresource, D3D11_MAP_READ_WRITE, 0, pRes);

    void* d = pRes->pData;
    char* data = reinterpret_cast<char*>(d);

    // writes data to file
    WriteFrameToCaptureFile(data, 0);
}

bool CaptureSingleFrame()
{
    HRESULT hr(E_FAIL);
    ComPtr<IDXGIResource> lDesktopResource = nullptr;
    DXGI_OUTDUPL_FRAME_INFO lFrameInfo;
    ID3D11Texture2D* currTexture;

    hr = lDeskDupl->AcquireNextFrame(
        999,
        &lFrameInfo,
        &lDesktopResource);

    if (FAILED(hr)) {
        LOG(INFO) << "Failed to acquire new frame";
        return false;
    }

    if (lFrameInfo.LastPresentTime.HighPart == 0) {
        // not interested in just mouse updates, which can happen much faster than 60fps if you really shake the mouse
        hr = lDeskDupl->ReleaseFrame();
        return false;
    }

    int accum_frames = lFrameInfo.AccumulatedFrames;
    if (accum_frames > 1 && current_frame != 1) {
        // TOO MANY OF THESE is the problem
        // especially after having to wait >17ms in AcquireNextFrame()
    }

    // QI for ID3D11Texture2D
    hr = lDesktopResource.As(&lAcquiredDesktopImage);

    // Copy image into a newly created CPU access texture
    hr = lDevice->CreateTexture2D(&desc, NULL, &currTexture);
    if (FAILED(hr))
        return false;
    if (currTexture == nullptr)
        return false;

    lImmediateContext->CopyResource(currTexture, lAcquiredDesktopImage.Get());


    writer_thread->Schedule(
        FROM_HERE, [this, currTexture]() {
        WriteFrameToCaptureFile(currTexture);
    });
    pending_write_counts_++;

    hr = lDeskDupl->ReleaseFrame();

    return true;
}

**EDIT - According to my measurements , you must call AcquireNextFrame() before the frame will actually appear by about ~10ms, or windows will fail to acquire it and get you the next one. **编辑 - 根据我的测量 ,您必须在帧实际出现约10ms之前调用AcquireNextFrame(),否则Windows将无法获取它并让您获得下一个。 Every time my recording program takes more than 7 ms to wrap around (after acquiring frame i until calling AcquireNextFrame() on i+1), frame i+1 is missed. 每次我的录制程序需要超过7毫秒来换行(在获取帧i之后直到在i + 1上调用AcquireNextFrame()),帧i + 1将被丢失。

***EDIT - Heres a screenshot of GPU View showing what I'm talking about. ***编辑- 继承人的GPU查看截图显示我在说什么。 The first 6 frames process in no time, then the 7th frame takes 119ms. 前6帧立即处理,然后第7帧需要119ms。 The long rectangle beside "capture_to_argb.exe" corresponds to me being stuck inside AcquireNextFrame(). “capture_to_argb.exe”旁边的长矩形对应于我被困在AcquireNextFrame()中。 If you look up to the hardware queue, you can see it cleanly rendering at 60fps, even while I'm stuck in AcquireNextFrame(). 如果你查看硬件队列,你可以看到它以60fps干净地渲染,即使我陷入了AcquireNextFrame()。 At least this is my interpretation (I have no idea what I'm doing). 至少这是我的解释(我不知道我在做什么)。

"Current Display Mode: 3840 x 2160 (32 bit) (60hz)" refers to display refresh rate, that is how many frames can be passed to display per second. “当前显示模式:3840 x 2160(32位)(60hz)”是指显示刷新率,即每秒可以传递多少帧。 However the rate at which new frames are rendered is typically much lower. 但是,渲染新帧的速率通常要低得多。 You can inspect this rate using PresentMon or similar utilities. 您可以使用PresentMon或类似实用程序检查此速率。 When I don't move the mouse it reports me something like this: 当我不移动鼠标时,它报告我这样的事情:

本报告

As you can see when nothing happens Windows presents new frame only twice per second or even slower. 正如您所看到的,当没有任何反应时,Windows每秒只会呈现两次新帧,甚至更慢。 However this is typically really good for video encoding because even if you are recording video at 60 fps and AcquireNextFrame reports that no new frame is available then it means that current frame is exactly the same as previous. 然而,这通常非常适合视频编码,因为即使您以60 fps录制视频而AcquireNextFrame报告没有新帧可用,则表示当前帧与之前的帧完全相同。

Doing a blocking wait before next call of AcquireNextFrame you are missing the actual frames. 在下次调用AcquireNextFrame之前执行阻塞等待您缺少实际帧。 Desktop Duplication API logic suggests that you attempt to acquire next frame immediately if you expect a decent frame rate. 桌面复制API逻辑建议您尝试立即获取下一帧,如果您期望一个合适的帧速率。 Your sleeping call effectively relinquishes the available remainder of execution timeout without hard promise that you get a new slice in scheduled interval of time. 您的休眠呼叫有效地放弃了可用的剩余执行超时,而没有在计划的时间间隔内获得新切片的硬性承诺。

You have to poll at maximal frame rate. 您必须以最大帧速率进行轮询。 Do not sleep (even with zero sleep time) and request next frame immediately. 不要睡觉(即使睡眠时间为零)并立即请求下一帧。 You will have the option to drop the frames that come too early. 您可以选择删除过早出现的帧。 Desktop Duplication API is designed in a way that getting extra frames might be not too expensive of you identify them early and stop their processing. 桌面复制API的设计方式是,获取额外的帧可能不会太昂贵,无法及早识别它们并停止处理。

If you still prefer to sleep between the frames, you might want to read the accuracy remark : 如果您仍然希望在帧之间休眠,则可能需要阅读准确性备注

To increase the accuracy of the sleep interval, call the timeGetDevCaps function to determine the supported minimum timer resolution and the timeBeginPeriod function to set the timer resolution to its minimum. 要提高休眠间隔的准确性,请调用timeGetDevCaps函数以确定支持的最小计时器分辨率,并调用timeGetDevCaps函数将计时器分辨率设置为最小值。 Use caution when calling timeBeginPeriod, as frequent calls can significantly affect the system clock, system power usage, and the scheduler. 调用timeBeginPeriod时请小心,因为频繁的调用会显着影响系统时钟,系统电源使用和调度程序。 If you call timeBeginPeriod, call it one time early in the application and be sure to call the timeEndPeriod function at the very end of the application. 如果你调用timeBeginPeriod,请在应用程序的早期调用它一次,并确保在应用程序的最后调用timeEndPeriod函数。

As others have mentioned, the 60Hz refresh rate only indicates the frequency with which the display may change. 正如其他人所提到的,60Hz刷新率仅表示显示器可能改变的频率。 It doesn't actually mean that it will change that frequently. 它实际上并不意味着它经常改变。 AcquireNextFrame will only return a frame when what is being displayed on the duplicated output has changed. AcquireNextFrame仅在重复输出上显示的内容发生更改时才返回帧。

My recommendation is to ... 我的建议是......

  1. Create a Timer Queue timer with the desired video frame interval 创建具有所需视频帧间隔的计时器队列计时器
  2. Create a compatible resource in which to buffer the desktop bitmap 创建用于缓冲桌面位图的兼容资源
  3. When the timer goes off, call AcquireNextFrame with a zero timeout 当计时器关闭时,调用AcquireNextFrame并使其超时为零
  4. If there has been a change, copy the returned resource to your buffer and release it 如果有更改,请将返回的资源复制到缓冲区并释放它
  5. Send the buffered frame to the encoder or whatever further processing 将缓冲帧发送到编码器或任何进一步处理

This will yield a sequence of frames at the desired rate. 这将以所需的速率产生一系列帧。 If the display hasn't changed, you'll have a copy of the previous frame to use to maintain your frame rate. 如果显示未更改,您将拥有用于保持帧速率的上一帧的副本。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM