DirectX 屏幕捕捉 - 桌面复制 API - AcquireNextFrame 的帧率受限

6
我正在尝试使用Windows 桌面镜像 API 来捕获屏幕并将原始输出保存为视频。我使用AcquireNextFrame,带有非常高的超时值(999ms)。这样,我应该能够尽快从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=317毫秒)时获取。第12帧开始等待时间为308,但直到336毫秒后才有响应。Windows直到之后一帧才有响应(300+17+17~=336毫秒)。好吧,也许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;
}

**编辑 - 根据我的测量,在帧实际出现前必须调用AcquireNextFrame()约10毫秒,否则Windows将无法获取它并给您下一个。每当我的录制程序需要超过7毫秒才能绕过(在获取第i帧到调用i + 1上的AcquireNextFrame()之后),就会错过第i + 1帧。

***编辑 - 这里有一张GPU View的截图,显示了我所说的情况。前6帧在瞬间处理完,然后第7帧花费了119毫秒。在“capture_to_argb.exe”旁边的长方形对应我被卡在AcquireNextFrame()中。如果您向上查看硬件队列,则可以看到它以60fps清晰地呈现,即使我被困在AcquireNextFrame()中也是如此。至少这是我的解释(我不知道我在做什么)。


1
你所看到的“60 Hz”是显示器的刷新率,即物理显示器向屏幕投放像素的速度。显示器的刷新不需要有新数据可用。有新数据可用才会导致AcquireNextFrame返回成功代码。新帧不必以与显示器刷新相同的速率到达(尽管这将是最佳选择)。 - IInspectable
2
每17毫秒我会询问...所以你在某个循环中的调用之间需要Sleep吗? - Roman R.
我改用了一个繁忙的while循环(参见问题),而不是使用sleep,因为sleep只保证最短时间;也就是说,有可能会睡过头17毫秒。 - Aaron Germuth
Sleep没有提供这样的保证。如果您已经使用了IDXGIOutputDuplication::AcquireNextFrame,为什么还要实现超时功能呢?您尝试复制的功能已经被该函数提供了。 - IInspectable
抱歉,我使用的是sleep_for,而不是sleep。我尝试了有和没有超时的情况,但都没有成功(请参见问题)。 - Aaron Germuth
显示剩余2条评论
3个回答

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

present report

正如你所看到的,当没有任何变化时,Windows 每秒只会呈现新的帧两次甚至更慢。然而,这通常对于视频编码来说非常好,因为即使你以 60 fps 录制视频并且 AcquireNextFrame 报告没有可用的新帧,那么就意味着当前帧与前一帧完全相同。

很遗憾,我认为这不是问题所在。例如,当我以60fps播放YouTube视频时,Present Mon报告:“...chrome.exe[8884]:0000018348E28F50(DXGI):SyncInterval 1 | Flags 0 |每帧16.67毫秒(60.0 fps,60.0显示的fps,14.87 ms CPU,44.94 ms延迟)...”然而,在同一时间内,我的桌面录像在300帧中有182个超时。 - Aaron Germuth

1
在下一次调用AcquireNextFrame之前进行阻塞等待,您将会丢失实际的帧。Desktop Duplication API 的逻辑建议如果您期望有一个良好的帧率,则应立即尝试获取下一帧。您的睡眠调用实际上放弃了执行超时的可用剩余时间,并不能保证您会在计划的时间间隔内得到新的数据片段。
您必须以最大帧率轮询。不要休眠(即使是零休眠时间),并立即请求下一帧。您将有选项放弃过早到达的帧。Desktop Duplication API 的设计方式是,在您及早识别并停止处理额外的帧后,获取额外的帧可能不会太昂贵。
如果您仍然喜欢在帧之间休眠,您可能需要阅读准确性备注
为了提高睡眠间隔的准确性,请调用timeGetDevCaps函数以确定支持的最小计时器分辨率,然后调用timeBeginPeriod函数将计时器分辨率设置为其最小值。在调用timeBeginPeriod时要小心,因为频繁调用会显著影响系统时钟、系统功耗和调度程序。如果您调用了timeBeginPeriod,请在应用程序早期仅调用一次,并确保在应用程序的最后调用timeEndPeriod函数。

很遗憾,如果我理解正确的话,这并没有解决问题。我的问题提到了我已经尝试过1)使用0超时和睡眠/繁忙-同时以及2)使用无限超时,尽可能快地获取帧。第二种方法的问题是,我经常在AcquireNextFrame()上等待超过17ms。最糟糕的是,当这种情况发生时,AccumulatedFrames大于1,这意味着我错过了一帧。我添加了一张新的gpuView图片,可能说明了我的意思。 - Aaron Germuth
我已经编辑了问题,删除了第一种方法(超时为零)。这样做更简单。 - Aaron Germuth
超时时间不必是无限的,只需非零且大于帧间时间,以便您可以看到是否捕获了足够的帧。如果监视器上没有任何更改,则可以从AcquireNextFrame获取超时时间。如果存在更改但您晚了一步获取帧,则可能仍然会晚调用AcquireNextFrame(特别是由于线程上的其他活动)。 - Roman R.
我目前使用的超时时间为999毫秒(抱歉,我没有意识到可以传递一个实际的无限值)。在这一点上,它永远不会真正超时。我尝试过使用略高于17毫秒的超时时间,就像你说的那样。但是,我确实遇到了超时问题。然而,我知道这不是因为什么都没有改变。PresentMon显示它以60fps运行。此外,如果您看到我上面附加的截图,图形卡的硬件队列仍在每17ms渲染帧,即使我的应用程序“capture_to_argb.exe”停顿了100ms。因此更新正在发生,但AcquireNextFrame正在等待它。 - Aaron Germuth
我想可能的一个原因是,虽然我已经调用了AcquireNextFrame(),但它实际上并没有运行,因为它仍然被阻止在某些先前操作上,比如复制上一帧的资源。这是你想说的吗?不过,如果是这样的话,我认为我们会在gpuView中看到GPU上的队列,对吗? - Aaron Germuth

0

正如其他人所提到的,60Hz刷新率只是指显示器可能更改的频率。它并不意味着它一定会以那个频率更改。只有在复制输出上显示的内容发生变化时,AcquireNextFrame才会返回一个帧。

我的建议是...

  1. 使用所需的视频帧间隔创建计时器队列计时器
  2. 创建兼容资源以缓冲桌面位图。
  3. 当计时器触发时,使用零超时调用AcquireNextFrame。
  4. 如果发生了更改,请将返回的资源复制到您的缓冲区并释放它。
  5. 将缓冲帧发送到编码器或任何进一步处理。

这将产生所需速率的帧序列。如果显示器没有更改,则您将拥有先前帧的副本,以维护帧速率。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接