Страницы

Поиск по вопросам

вторник, 24 декабря 2019 г.

Воспроизведение большого видео с деформацией - DirectX

#mediaplayer #directx #gpu #directshow


Приветствую всех!

Ситуация следующая: есть видео разрешением 1920x2400, закодированное hevc (h265),
спрятанное в .mp4, подготовленное с частотой 30 фпс; аудиопотока нет, только видео

Есть задача - воспроизводить это видео на двух мониторах 1920x1080 (так называемая
вертикальная стерео-пара)
После поиска методов для реализации данного алгоритма, было принято решение использовать
DirectShow для получения кадров и DirectX для вывода текстуры. Получилась следующая
цепочка графов:

FileSourceFilter -> Lav Splitter -> Lav Video Decoder -> SampleGrabber -> NullRenderer

В callback-функции SampleGrabber'а беру и создаю Texture2D, из которой создаю ShaderResourceView.
Ну, и соответственно, есть два плоских меша, на которых накладывается текстура. Отрисовка
происходит вот так:

const float bg_clear_color[4] = { 1.0f, 1.0f, 1.0f, 1.0f };

pD3DDC->ClearRenderTargetView(pD3DRenderTarget, bg_clear_color);
pD3DDC->ClearDepthStencilView(pD3DDepthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0);

objects[0]->Render();
objects[1]->Render();

pD3DSwapChain->Present(0, 0);


И картинка идет, но происходят подтормаживания. Бьюсь над этим вопросом уже несколько
месяцев, не знаю в каком направлении рыть, пробовал всякое, и аппаратное ускорение
и снижение битрейта видео до 10-12 мбит/c. Ситуация немного улучшилась, но все равно
картинка дергается.

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

EDIT:

В данный момент по совету @Roman совершаю миграцию своего движка на Media Foundation
с DirectShow. Так как мы перешли на MF, начали использовать h264 для надежности, потому
что как я понял, h265 сложнее использовать и по-умолчанию драйвера не установлены,
придется подключать их дополнительно, как самописные фильтры DS (возможно, я ошибаюсь,
но это пока не важно).

Задача немного изменилась и теперь произносится так: есть четыре видео 1920x1080
h264 20 mbps 30 fps. Разрешение было выбрано именно такое, потому что в документации
на MSDN мной было замечено, что максимальное разрешение видео, при котором возможно
использовать аппаратное ускорение, - 1920x1088. Задача требует максимально быстро получать
кадры из каждого видео и писать их в текстуры ID3D11Texture2D, чтобы потом их выводить
в сцене Direct3D 11.

Сейчас на рабочей машине стоит две видеокарты NVIDIA GTX 950, к каждой видеокарте
подключено по два монитора, и еще один монитор. Все мониторы 1920x1080.

После того как я провел неделю кряду над поиском информации по MF, я был удивлен,
как мало информации можно найти на тему видео текстур с поддержкой HWA, проштрудировал
практически все мануалы по MF на MSDN, но все равно многое осталось мне непонятным.
Сейчас расскажу как у меня сейчас все работает и чего хотелось бы добиться. Выражаю
огромную благодарность товарищу @Roman, который многое подсказал и направил меня в
нужном направлении.

Direct3D устройство создается у меня следующим кодом:

D3D_FEATURE_LEVEL fl[] = {D3D_FEATURE_LEVEL_11_1};
CH(D3D11CreateDevice(0, D3D_DRIVER_TYPE_HARDWARE, 0, D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
fl, ARRAYSIZE(fl), D3D11_SDK_VERSION, &pD3DDevice, 0, &pD3DDC));


При перечислении доступных устройств у меня в списке отображается четыре, Intel(R)
HD Graphics, два NDIVIA GTX 950 и Microsoft Basic Render Driver.
Хотелось бы использовать два видео на первом NVIDIA устройстве, а остальные два на
другом, но вот в чем проблема. Если я создаю устройство с указанием одного из них,
мне приходится передавать параметр D3D_DRIVER_TYPE_UNKNOWN, что снижает фпс и одно
видео на NV выдает ~25 фпс. Если я инициализирую устройство как в коде выше - по-умолчанию
выбирается Intel, но зато фпс выдает ~50 (я подозреваю, что почему-то не включается
аппаратное ускорение), что странно.

Моя затея следующая: я хочу максимально задействовать силы NVIDIA карты с HWA, чтобы
все кадры видео сразу рендерелись в текстуру, без лишнего копирования в CPU, а потом
обратно в GPU, как у меня было реализовано.

CComPtr sample;
CH(reader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, 0, &flags, 0, &sample.p));

CComPtr buffer;
BYTE* data;
CH(sample->ConvertToContiguousBuffer(&buffer));
CH(buffer->Lock(&data, NULL_PTR, NULL_PTR));

DWORD dwSize;
CH(buffer->GetCurrentLength(&dwSize));
UpdateVideoTexture(data, dwSize);

buffer->Unlock();


Этот код был создан на основе вот этого, но там все также есть затык, получается
мы вытаскиваем текстуру из GPU и потом засовываем ее обратно. Этот код медленный. Поэтому
я начал копать дальше, и вот что получилось, как я инициализирую SourceReader.

//Инициализируем COM библиотеку для нашего приложения и MF библиотеку
CH(CoInitializeEx(NULL_PTR, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
CH(MFStartup(MF_VERSION));

//Говорим библиотеке D3D что мы используем мультипоточные вызовы MF, чтобы избежать
undefined behavior
const CComQIPtr pMultithread = pD3DDevice;
pMultithread->SetMultithreadProtected(TRUE);

//Создание Менеджера Устройств MF
UINT token;
CComPtr manager;
CH(MFCreateDXGIDeviceManager(&token, &manager));

//Привязываем наше D3D Устройство к Менеджеру Устройств
CH(manager->ResetDevice(pD3DDevice, token));

//Создаем свойства SourceReader
CComPtr attributes;
CH(MFCreateAttributes(&attributes, 2));
CH(attributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, manager));  //указываем менеджер
CH(attributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE));
CH(attributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE));

//Создаем SourceReader и загружаем видео, зажатое h264 кодеком
CH(MFCreateSourceReaderFromURL(L"D:\\Work\\WorkFiles\\1.mp4", attributes, &reader));

//Настраиваем выходной медиатип декодера в SourceReader
CComPtr output_type;
CH(MFCreateMediaType(&output_type));
CH(output_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video));
CH(output_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12));
CH(reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL_PTR, output_type));

//Создаем Видео Устройство
CComPtr pVideoDevice;
CH(pD3DDevice->QueryInterface(IID_PPV_ARGS(&pVideoDevice)));

//Создаем видеотекстуру
CH(D3DHelper_Create2DTexture(pD3DDevice, &pD3DVideoTexture, CORE_WIDTH, CORE_HEIGHT,
DXGI_FORMAT_NV12, D3D11_BIND_DECODER, 0, D3D11_USAGE_DEFAULT, 0));

D3D11_VIDEO_DECODER_OUTPUT_VIEW_DESC dc;
dc.DecodeProfile = D3D11_DECODER_PROFILE_H264_VLD_NOFGT;
dc.Texture2D.ArraySlice = 0;
dc.ViewDimension = D3D11_VDOV_DIMENSION_TEXTURE2D;

//И описываем D3D View текстуры
CH(pVideoDevice->CreateVideoDecoderOutputView((ID3D11Resource*)pD3DVideoTexture,
&dc, &pVideoDecoderOutputView));
CH(pD3DDC->QueryInterface(IID_PPV_ARGS(&pVideoContext)));

//Описываем енумератор Видео Процессора
D3D11_VIDEO_PROCESSOR_CONTENT_DESC ed;
CLEAR(ed);

ed.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE;
ed.InputFrameRate.Numerator = 60;
ed.InputFrameRate.Denominator = 1;
ed.InputWidth = CORE_WIDTH;
ed.InputHeight = CORE_HEIGHT;
ed.OutputFrameRate.Numerator = 60;
ed.OutputFrameRate.Denominator = 1;
ed.OutputWidth = CORE_WIDTH;
ed.OutputHeight = CORE_HEIGHT;
ed.Usage = D3D11_VIDEO_USAGE_OPTIMAL_SPEED;

CComPtr pVideoEnum;
CH(pVideoDevice->CreateVideoProcessorEnumerator(&ed, &pVideoEnum));

//Создаем Видео Процессор
CH(pVideoDevice->CreateVideoProcessor(pVideoEnum, 0, &pVideoProcessor));


В последствии я планирую вызывать ReadSample, а что делать дальше не знаю, прошу
помощи, спасибо. Ради поддержки вышестоящего кода пришлось даже пересесть с Win7 на
Win10. Благодарю за любую помощь.

EDIT:

P.S.: Вот код, которым сейчас инициализируется текстура:

D3D11_MAPPED_SUBRESOURCE resource;
CH(pD3DDC->Map(pD3DVideoTexture, 0, D3D11_MAP_WRITE_DISCARD, 0, &resource));

memcpy(resource.pData, data, dwSize);
pD3DDC->Unmap(pD3DVideoTexture, 0);

SAFE_COM_RELEASE(pD3DVideoTextureRV);

CH(pD3DDevice->CreateShaderResourceView(pD3DVideoTexture, 0, &pD3DVideoTextureRV));
pD3DDC->PSSetShaderResources(0, 1, &pD3DVideoTextureRV);

    


Ответы

Ответ 1



В этой схеме есть сразу несколько потенциальных заторов, один или несколько из которых ограничивают общую работу. Они таковы: Неизвестна производительность декодера, справляется ли он нормально с задачей или он и тормозит Подключение Sample Grabber таково, что возможно между ним и декодером происходит дополнительное преобразование данных, либо как дополнительный фильтр, либо в рамках LAV декодера - это может сказываться на производительности Sample Grabber выгребает данные в системной памяти независимо от режима аппаратного ускорения Sample Grabber может оперировать ограниченным числом буферов, что в свою очередь может ограничивать возможность декодера своевременно пополнять очередь; само присутствие Sample Grabber как правило является свидетельством неспособности потреблять данные более эффективным способом Не указан тип текстуры заполнение staging текстур в реальном времени имеет слабую производительность Так, в общем, хорошо бы декодировать аппаратно прямиком в текстуру и дальше уже данными в ресурсе графического адаптера манипулировать рисуя сцену. Такой подход, к сожалению, нереализуем с Sample Grabber. Media Foundation API, к примеру, такие фокусы позволяет.

Ответ 2



С помощью этого примера получилось разобраться с проблемой, выводится минимум по 50 фпс на всех четырех мониторах, спасибо за помощь! EDIT: Подробно расскажу как это получилось. Добавляю подробную информацию, поиск которой занял у меня около недели, но результат того стоит, надеюсь, этот код поможет быстрее понять принцип. Выкладываю кусок кода, отвечающий за получение видеотекстуры и ее запись в шейдер для дальнейшего отображения на моделях в сцене. Код максимально сжатый, с опущенными проверками для наглядности алгоритма. Следующая часть работает с инициализацией MF как в первом посте. Суть алгоритма в следующем: CComPtr pSample; CH(pSourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, 0, &flags, 0, &pSample)); CComPtr pBuffer; CH(pSample->GetBufferByIndex(0, &pBuffer)); CComPtr pDXGIBuffer; CH(pBuffer->QueryInterface(IID_PPV_ARGS(&pDXGIBuffer))); CComPtr pTexture2D; CH(pDXGIBuffer->GetResource(IID_PPV_ARGS(&pTexture2D))); D3D11_TEXTURE2D_DESC desc; pTexture2D->GetDesc(&desc); D3D11_VIDEO_PROCESSOR_CONTENT_DESC cd; CL(cd); cd.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST; cd.InputWidth = desc.Width; cd.InputHeight = desc.Height; cd.OutputWidth = desc.Width; cd.OutputHeight = desc.Height; cd.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL; CH(pVideoDevice->CreateVideoProcessorEnumerator(&cd, &pVideoEnum)); CH(pVideoEnum->CheckVideoProcessorFormat(DXGI_FORMAT_B8G8R8A8_UNORM, &uiFlags)); CH(pVideoDevice->CreateVideoProcessor(pVideoEnum, 0, &pVideoProcessor)); pSample.Release(); pBuffer.Release(); CH(MFCreateSample(&pSample)); CH(MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), pD3DVideoTexture, 0, FALSE, &pBuffer)); CH(pSample->AddBuffer(pBuffer)); D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC OutputViewDesc; CL(OutputViewDesc); OutputViewDesc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; OutputViewDesc.Texture2D.MipSlice = 0; OutputViewDesc.Texture2DArray.MipSlice = 0; OutputViewDesc.Texture2DArray.FirstArraySlice = 0; CComPtr pOutputView; CH(pVideoDevice->CreateVideoProcessorOutputView(pD3DVideoTexture, pVideoEnum, &OutputViewDesc, &pOutputView)); D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC ivd; CL(ivd); ivd.FourCC = 0; ivd.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D; ivd.Texture2D.MipSlice = 0; ivd.Texture2D.ArraySlice = 0; CComPtr pInputView; CH(pVideoDevice->CreateVideoProcessorInputView(pTexture2D, pVideoEnum, &ivd, &pInputView)); D3D11_VIDEO_PROCESSOR_STREAM sd; CL(sd); sd.Enable = TRUE; sd.OutputIndex = 0; sd.InputFrameOrField = 0; sd.PastFrames = 0; sd.FutureFrames = 0; sd.ppPastSurfaces = NULL; sd.ppFutureSurfaces = NULL; sd.pInputSurface = pInputView; sd.ppPastSurfacesRight = NULL; sd.ppFutureSurfacesRight = NULL; CH(pVideoContext->VideoProcessorBlt(pVideoProcessor, pOutputView, 0, 1, &sd)); SAFE_COM_RELEASE(pD3DVideoTextureRV); CH(pD3DDevice->CreateShaderResourceView(pD3DVideoTexture, 0, &pD3DVideoTextureRV)); pD3DDC->PSSetShaderResources(0, 1, &pD3DVideoTextureRV); А теперь объясню что здесь происходит. Как ранее подсказывал @Roman, мы при правильной инициализации, мы получаем с помощью IMFSourceReader указатель на наши сэмплы (IMFSample). Далее мы получаем указатель на IMFDXGIBuffer интерфейс, из которого в свою очередь вытягиваем указатель на нужный интерфейс ID3D11Texture2D. В этой текстуре уже хранится информация кадра видео, но как я понял, ее нельзя сразу использовать для рендеринга, нужно пропустить ее через некоторый видеопроцессинг, что мы и делаем далее. Стоит заметить, что ту текстуру, которую я использую для шейдера я создаю ранее, в другой части кода с BIND-флагами D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET, как это описано здесь. Далее по коду мы создаем нужные интерфейсы для того, чтобы указать что конечный результат будет записан в нашу текстуру (она объявлена в коде как pD3DVideoTexture). После всех операций, на основе этой текстуры, я создаю ID3D11ShaderResourceView, который связывает нашу текстуру с шейдером. При правильных действиях, программа выдает от 125 до 175 фпс на NVIDIA GTX 950 (для одного видео 1920x1080 h264 20 mbps). Хочу подчеркнуть, что это не максимум, MF поистине сильная вещь, хорошо оптимизированная под Windows 10. Если не использовать дополнительное копирование в pD3DVideoTexture, а указывать везде текстуру задника окна, которую мы можем получить из интерфейса IDXGISwapChain с помощью метода GetBuffer(), то фпс у меня доходил от 300 до 350, при том, что CPU был загружен на ~20%, а GPU на ~50-60%. Под словом "фпс", здесь я имею в виду максимально возможное количество кадров, которое может вытащить движок в секунду, надеюсь, понятно, что для многих видеоплееров не нужно больше 30-50. Надеюсь, этой информацией я кому-то помогу, и люди быстрее найдут решение! Еще раз спасибо за помощь.

Комментариев нет:

Отправить комментарий