【问题标题】:DirectX Screen Capture - Desktop Duplication API - limited frame rate of AcquireNextFrameDirectX 屏幕捕获 - 桌面复制 API - AcquireNextFrame 的有限帧速率
【发布时间】:2017-11-08 05:56:30
【问题描述】:

我正在尝试使用 Windows Desktop Duplication API 捕获屏幕并将原始输出保存到视频中。我正在使用具有非常高的超时值(999 毫秒)的AcquireNextFrame。这样一来,我就应该从 windows 中获取每个新帧,不管怎样,它自然应该是 60fps。我最终得到一切看起来都不错的序列(第 6-11 帧),然后是看起来很糟糕的序列(第 12-14 帧)。如果我检查AccumulatedFrames

lFrameInfo.AccumulatedFrames

该值通常为 2 或更高。据我了解,这意味着 Windows 正在说“嘿,等等,我还没有给你的框架”,因为对 AcquireNextFrame 的调用需要很长时间。但是一旦windows最终给了我一个框架,它就会说“嘿,你实际上太慢了,最终错过了一个框架”。如果我能以某种方式获得这些帧,我想我会获得 60hz。

这可以通过日志进一步澄清:

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

第 6-11 帧看起来不错,采集间隔大约 17 毫秒。第 12 帧应在 (300+17=317ms) 获取。第 12 帧在 308 开始等待,但直到 336 毫秒才得到任何东西。在 (300+17+17~=336ms) 之后的帧之前,Windows 对我没有任何帮助。好吧,也许 windows 只是错过了一个帧,但是当我终于得到它时,我可以检查 AccumulatedFrames 并且它的值为 2(这意味着我错过了一个帧,因为我在调用 AcquireNextFrame 之前等待了太长时间)。据我了解,如果 AcquireNextFrame 立即返回,则 AccumulatedFrames 大于 1 才有意义。

此外,我可以在捕获软件运行时使用 PresentMon。日志显示每一帧的 MsBetweenDisplayChange,相当稳定在 16.666 毫秒(有几个异常值,但比我的捕获软件看到的要少得多)。

这些人(12)似乎已经能够达到 60fps,所以我想知道我做错了什么。

我的代码基于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 - 根据my measurements,您必须在帧实际出现约 10 毫秒之前调用 AcquireNextFrame(),否则 windows 将无法获取它并为您获取下一个。每次我的录制程序需要超过 7 毫秒的时间来回绕(在获取第 i 帧之后直到在 i+1 上调用 AcquireNextFrame()),都会错过第 i+1 帧。

***编辑 - Heres GPU 视图的屏幕截图,显示了我在说什么。前 6 帧很快处理,然后第 7 帧需要 119 毫秒。 “capture_to_argb.exe”旁边的长矩形对应于我被困在 AcquireNextFrame() 中。如果您查看硬件队列,您可以看到它以 60fps 清晰地呈现,即使我被困在 AcquireNextFrame() 中。至少这是我的解释(我不知道我在做什么)。

【问题讨论】:

  • 您看到的 "60 Hz" 是显示刷新率,即物理显示器将像素投射到屏幕上的速率。显示刷新不需要有可用的新数据。新数据可用是导致AcquireNextFrame 返回成功代码的原因。新帧不需要以与显示刷新相同的速率到达(尽管这将是最佳的)。
  • 见下面的评论; PresentMon 说我的浏览器正在以 60hz 更新,但 AcquireNextFrame 有同样的困难
  • “每 17 毫秒我问...”所以你 Sleep 在某个循环内的调用之间?
  • 我切换到繁忙的while循环(见问题)而不是睡眠,因为睡眠只保证最短时间;即,有可能睡过17ms
  • Sleep 不做这样的保证。目前尚不清楚,当IDXGIOutputDuplication::AcquireNextFrame 提供了您已经尝试复制的功能时,您为什么要实现超时。

标签: c++ winapi directx screen-capture desktop-duplication


【解决方案1】:

“当前显示模式:3840 x 2160 (32 bit) (60hz)”是指显示刷新率,即每秒可以通过多少帧显示。然而,新帧的渲染速率通常要低得多。您可以使用PresentMon 或类似实用程序检查此速率。当我不移动鼠标时,它会向我报告如下内容:

正如您所见,当什么都没有发生时,Windows 每秒只显示两次新帧,甚至更慢。但是,这通常非常适合视频编码,因为即使您以 60 fps 录制视频并且AcquireNextFrame 报告没有可用的新帧,这意味着当前帧与之前的帧完全相同。

【讨论】:

  • 不幸的是,我认为这不是问题所在。例如,当我以 60fps 播放 youtube 视频时,Present Mon 报告“...chrome.exe[8884]: 0000018348E28F50 (DXGI): SyncInterval 1 | Flags 0 | 16.67 ms/frame (60.0 fps, 60.0 shown fps, 14.87 ms CPU,44.94 ms 延迟)...”但我在同一时间录制的桌面视频有 300 帧的 182 次超时。
【解决方案2】:

在下次调用 AcquireNextFrame 之前进行阻塞等待,您将丢失实际帧。桌面复制 API 逻辑建议您尝试立即获取下一帧,如果您期望一个不错的帧速率。您的睡眠调用有效地放弃了可用的剩余执行超时时间,而不是硬保证您会在预定的时间间隔内获得一个新切片。

您必须以最大帧速率进行轮询。不要睡眠(即使睡眠时间为零)并立即请求下一帧。您可以选择丢弃过早出现的帧。 Desktop Duplication API 的设计方式是获取额外帧可能不会太昂贵,因为您可以尽早识别它们并停止处理。

如果您仍然喜欢在帧之间睡觉,您可能需要阅读accuracy remark

要提高睡眠间隔的准确性,请调用timeGetDevCaps 函数来确定支持的最小定时器分辨率,并调用timeBeginPeriod 函数来将定时器分辨率设置为最小值。调用 timeBeginPeriod 时要小心,因为频繁调用会显着影响系统时钟、系统电源使用和调度程序。如果您调用 timeBeginPeriod,请在应用程序的早期调用一次,并确保在应用程序的最后调用 timeEndPeriod 函数。

【讨论】:

  • 不幸的是,如果我理解正确,这并不能解决问题。我的问题提到我已经尝试过 1) 超时和睡眠/忙碌时 0 以及 2) 无限超时,尽可能快地获取帧。第二种方法的问题是我经常在 AcquireNextFrame() 上等待超过 17 毫秒。最糟糕的是,当这种情况发生时,AccumulatedFrames 大于 1,这意味着我错过了一帧。我添加了一张新的 gpuView 图片,可以说明我在说什么
  • 我已编辑问题以删除第一种方法(超时为零)。这样就简单多了
  • 超时不必是无限的,只是非零且大于帧间时间,以便您查看是否捕获了足够多的帧或帧。如果监视器上没有任何更改,则可以从 AcquireNextFrame 获取超时。如果存在更改但您获取帧较晚,您可能仍然开始调用 AcquireNextFrame 较晚(尤其是因为线程上的其他活动)。
  • 我目前使用的超时时间为 999 毫秒(抱歉,我没有意识到您可以传递实际的 INFINITE 值)。在这一点上,它永远不会真正超时。正如你所说,我已经尝试使用略高于 17 毫秒的超时。但后来我确实超时了。但是,我知道这不是因为没有任何改变。 PresentMon 显示它以 60fps 运行。此外,如果您看到我上面附上的屏幕截图,即使我的应用程序“capture_to_argb.exe”停滞了 100 毫秒,显卡的硬件队列仍然每 17 毫秒渲染帧。所以更新正在发生,但 AcquireNextFrame 一直在等待。
  • 我想一种可能性是,虽然我调用了 AcquireNextFrame(),但它实际上并没有运行,因为它仍然被一些先前的操作阻塞,比如前一帧的 CopyResource。这是你想说的吗?如果这是真的,我想我们会在 gpuView 中看到 GPU 上的队列,对吧?
【解决方案3】:

正如其他人所提到的,60Hz 刷新率仅表示显示可能变化的频率。这实际上并不意味着它经常改变。 AcquireNextFrame 只会在重复输出上显示的内容发生更改时返回一个帧。

我的建议是...

  1. 创建具有所需视频帧间隔的Timer Queue timer
  2. 创建一个compatible resource 用于缓冲桌面位图
  3. 当计时器关闭时,调用 AcquireNextFrame 并设置零超时
  4. 如果有更改,请将返回的资源复制到缓冲区并释放它
  5. 将缓冲的帧发送到编码器或任何进一步处理

这将以所需的速率产生一系列帧。如果显示没有改变,您将拥有上一帧的副本以用于保持帧速率。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-04
    • 2021-04-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-03
    相关资源
    最近更新 更多