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

Я пытаюсь использовать винду API дублирования рабочего стола захватить экран и сохранить исходный вывод в видео. я использую 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 пропускается.

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

1

Решение

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

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

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

2

Другие решения

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

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

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

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

1

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

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

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

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

0
По вопросам рекламы [email protected]