Снимок экрана DirectX - API дублирования рабочего стола - ограниченная частота кадров AcquireNextFrame

Я пытаюсь использовать API дублирования рабочего стола Windows для захвата экрана и сохранения необработанного вывода в видео. Я использую AcquireNextFrame с очень высоким значением времени ожидания (999 мс). Таким образом, я должен получать каждый новый кадр из окон, как только он у него будет, который, естественно, должен быть на 60fps в любом случае. Я получаю последовательности, где все выглядит хорошо (кадр 6-11), а затем последовательности, где все выглядит плохо (кадр 12-14). Если я проверю AccumulatedFrames

lFrameInfo.AccumulatedFrames

значение часто составляет 2 или выше. Насколько я понимаю, это означает, что Windows говорит: "Эй, держись, у меня еще нет рамки для тебя", потому что вызовы AcquireNextFrame занимают так много времени. Но как только Windows, наконец, дает мне кадр, он говорит: "Эй, ты на самом деле был слишком медленным и в итоге пропустил кадр". Если бы я мог как-то получить эти кадры, я думаю, что получил бы 60 Гц.

Это может быть уточнено с помощью регистрации:

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 мс. У меня ничего не было до тех пор, пока после кадра (300+17+17~=336 мс). Хорошо, конечно, возможно, окна просто пропустили кадр, но когда я, наконец, получил его, я могу проверить AccumulatedFrames, и его значение равнялось 2 (то есть я пропустил кадр, потому что слишком долго ждал, прежде чем вызвать AcquireNextFrame). В моем понимании, для AccumulatedFrames имеет смысл быть больше 1, если AcquireNextFrame возвращается немедленно.

Кроме того, я могу использовать PresentMon во время работы моего программного обеспечения для захвата. Журналы показывают MsBetweenDisplayChange для каждого кадра, который довольно стабилен на 16.666 мс (с парой выбросов, но намного меньше, чем мое программное обеспечение захвата видит).

Эти люди ( 1, 2), кажется, смогли получить 60 кадров в секунду, поэтому мне интересно, что я делаю неправильно.

Мой код основан на этом:

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 мс, иначе окна не получат его и не получат следующий. Каждый раз, когда моей программе записи требуется более 7 мс, чтобы обернуться (после получения кадра i до вызова AcquireNextFrame() для i+1), кадр i + 1 пропускается.

*** РЕДАКТИРОВАТЬ - Вот скриншот GPU View, показывающий, о чем я говорю. Первые 6 кадров обрабатываются в кратчайшие сроки, затем 7-й кадр занимает 119 мс. Длинный прямоугольник рядом с "capture_to_argb.exe" соответствует тому, что я застрял внутри AcquireNextFrame(). Если вы посмотрите на аппаратную очередь, вы увидите, что она чисто рендерится со скоростью 60 кадров в секунду, даже когда я застрял в AcquireNextFrame(). По крайней мере, это моя интерпретация (я понятия не имею, что я делаю).

3 ответа

"Текущий режим отображения: 3840 x 2160 (32 бита) (60 Гц)" относится к частоте обновления дисплея, то есть сколько кадров может быть передано для отображения в секунду. Однако скорость рендеринга новых кадров обычно намного ниже. Вы можете проверить эту скорость, используя PresentMon или аналогичные утилиты. Когда я не двигаю мышь, она сообщает мне что-то вроде этого:

настоящий отчет

Как вы можете видеть, когда ничего не происходит, Windows представляет новый кадр только два раза в секунду или даже медленнее. Однако, как правило, это действительно хорошо для кодирования видео, потому что даже если вы записываете видео со скоростью 60 кадров в секунду и AcquireNextFrame сообщает, что новый кадр недоступен, то это означает, что текущий кадр точно такой же, как и предыдущий.

Выполнение блокировки ожидания до следующего вызова AcquireNextFrame вам не хватает реальных кадров. Логика Desktop Duplication API предполагает, что вы пытаетесь получить следующий кадр немедленно, если ожидаете приемлемую частоту кадров. Ваш спящий вызов эффективно освобождает доступную оставшуюся часть времени ожидания без жесткого обещания, что вы получите новый фрагмент в запланированный интервал времени.

Вы должны опросить с максимальной частотой кадров. Не спать (даже с нулевым временем сна) и немедленно запросить следующий кадр. У вас будет возможность отбросить кадры, которые приходят слишком рано. Desktop Duplication API разработан таким образом, что получение дополнительных кадров может быть не слишком дорогим, если вы заранее их идентифицируете и остановите их обработку.

Если вы все еще предпочитаете спать между кадрами, вы можете прочитать примечание о точности:

Чтобы увеличить точность интервала сна, вызовите timeGetDevCaps функция для определения поддерживаемого минимального разрешения таймера и функция timeBeginPeriod для установки минимального разрешения таймера. Будьте осторожны при вызове timeBeginPeriod, поскольку частые вызовы могут значительно повлиять на системные часы, энергопотребление системы и планировщик. Если вы вызываете timeBeginPeriod, вызывайте его один раз в начале приложения и обязательно вызовите функцию timeEndPeriod в самом конце приложения.

Как уже упоминалось, частота обновления 60 Гц указывает только частоту, с которой может изменяться дисплей. На самом деле это не значит, что это будет часто меняться. AcquireNextFrame будет возвращать кадр только тогда, когда то, что отображается на дублированном выводе, изменилось.

Я рекомендую...

  1. Создайте таймер очереди таймера с желаемым интервалом видеокадра
  2. Создайте совместимый ресурс для буферизации растрового изображения рабочего стола.
  3. Когда таймер отключается, вызовите AcquireNextFrame с нулевым тайм-аутом
  4. Если произошли изменения, скопируйте возвращенный ресурс в буфер и отпустите его.
  5. Отправьте буферизованный кадр в кодировщик или любую дальнейшую обработку

Это даст последовательность кадров с желаемой скоростью. Если отображение не изменилось, у вас будет копия предыдущего кадра, которую можно использовать для сохранения частоты кадров.

Другие вопросы по тегам