Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qffmpegscreencapture_dxgi_p.h"
++ #include <QtMultimedia/private/qsurfacecapturegrabber_p.h>
++ #include "qabstractvideobuffer.h"
++ #include <private/qmultimediautils_p.h>
++ #include <private/qvideoframe_p.h>
++ #include <QtGui/qscreen_platform.h>
++ #include "qvideoframe.h"
++
++ #include <qloggingcategory.h>
++ #include <qwaitcondition.h>
++ #include <qmutex.h>
++ #include <QtCore/private/qsystemerror_p.h>
++ #include <QtCore/private/qexpected_p.h>
++
++ #include "d3d11.h"
++ #include "dxgi1_2.h"
++
++ #include <system_error>
++
++ #include <mutex> // std::scoped_lock
++
++ QT_BEGIN_NAMESPACE
++
++ Q_STATIC_LOGGING_CATEGORY(qLcScreenCaptureDxgi, "qt.multimedia.ffmpeg.screencapturedxgi");
++
++ using namespace Qt::StringLiterals;
++
++ namespace {
++
++ // Convenience wrapper that combines an HRESULT
++ // status code with an optional textual description.
++ class ComStatus
++ {
++ public:
++ ComStatus() = default;
++ ComStatus(HRESULT hr) : m_hr{ hr } { }
++ ComStatus(HRESULT hr, QAnyStringView msg) : m_hr{ hr }, m_msg{ msg.toString() } { }
++
++ ComStatus(const ComStatus &) = default;
++ ComStatus(ComStatus &&) = default;
++ ComStatus &operator=(const ComStatus &) = default;
++ ComStatus &operator=(ComStatus &&) = default;
++
++ explicit operator bool() const { return m_hr == S_OK; }
++
++ HRESULT code() const { return m_hr; }
++ QString str() const
++ {
++ if (!m_msg)
++ return QSystemError::windowsComString(m_hr);
++ return *m_msg + u" " + QSystemError::windowsComString(m_hr);
++ }
++
++ private:
++ HRESULT m_hr = S_OK;
++ std::optional<QString> m_msg;
++ };
++
++ template <typename T>
++ using ComProduct = q23::expected<ComPtr<T>, ComStatus>;
++ }
++
++ class QD3D11TextureVideoBuffer : public QAbstractVideoBuffer
++ {
++ public:
++ QD3D11TextureVideoBuffer(const ComPtr<ID3D11Device> &device, std::shared_ptr<QMutex> &mutex,
++ const ComPtr<ID3D11Texture2D> &texture)
++ : m_device(device), m_texture(texture), m_ctxMutex(mutex)
++ {}
++
++ ~QD3D11TextureVideoBuffer() { Q_ASSERT(m_mapMode == QVideoFrame::NotMapped); }
++
++ MapData map(QVideoFrame::MapMode mode) override
++ {
++ MapData mapData;
++ if (!m_ctx && mode == QVideoFrame::ReadOnly) {
++ D3D11_TEXTURE2D_DESC texDesc = {};
++ m_texture->GetDesc(&texDesc);
++ texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
++ texDesc.Usage = D3D11_USAGE_STAGING;
++ texDesc.MiscFlags = 0;
++ texDesc.BindFlags = 0;
++
++ HRESULT hr = m_device->CreateTexture2D(&texDesc, nullptr, m_cpuTexture.GetAddressOf());
++ if (FAILED(hr)) {
++ qCDebug(qLcScreenCaptureDxgi) << "Failed to create texture with CPU access"
++ << std::system_category().message(hr).c_str();
++ qCDebug(qLcScreenCaptureDxgi) << m_device->GetDeviceRemovedReason();
++ return {};
++ }
++
++ m_device->GetImmediateContext(m_ctx.GetAddressOf());
++ m_ctxMutex->lock();
++ m_ctx->CopyResource(m_cpuTexture.Get(), m_texture.Get());
++
++ D3D11_MAPPED_SUBRESOURCE resource = {};
++ hr = m_ctx->Map(m_cpuTexture.Get(), 0, D3D11_MAP_READ, 0, &resource);
++ m_ctxMutex->unlock();
++ if (FAILED(hr)) {
++ qCDebug(qLcScreenCaptureDxgi) << "Failed to map texture" << m_cpuTexture.Get()
++ << std::system_category().message(hr).c_str();
++ return {};
++ }
++
++ m_mapMode = mode;
++ mapData.planeCount = 1;
++ mapData.bytesPerLine[0] = int(resource.RowPitch);
++ mapData.data[0] = reinterpret_cast<uchar*>(resource.pData);
++ mapData.dataSize[0] = int(texDesc.Height * resource.RowPitch);
++ }
++
++ return mapData;
++ }
++
++ void unmap() override
++ {
++ if (m_mapMode == QVideoFrame::NotMapped)
++ return;
++ if (m_ctx) {
++ m_ctxMutex->lock();
++ m_ctx->Unmap(m_cpuTexture.Get(), 0);
++ m_ctxMutex->unlock();
++ m_ctx.Reset();
++ }
++ m_cpuTexture.Reset();
++ m_mapMode = QVideoFrame::NotMapped;
++ }
++
++ QVideoFrameFormat format() const override { return {}; }
++
++ QSize getSize() const
++ {
++ if (!m_texture)
++ return {};
++
++ D3D11_TEXTURE2D_DESC desc{};
++ m_texture->GetDesc(&desc);
++
++ return { static_cast<int>(desc.Width), static_cast<int>(desc.Height) };
++ }
++
++ private:
++ ComPtr<ID3D11Device> m_device;
++ ComPtr<ID3D11Texture2D> m_texture;
++ ComPtr<ID3D11Texture2D> m_cpuTexture;
++ ComPtr<ID3D11DeviceContext> m_ctx;
++ std::shared_ptr<QMutex> m_ctxMutex;
++ QVideoFrame::MapMode m_mapMode = QVideoFrame::NotMapped;
++ };
++
++ namespace {
++
++ struct DxgiScreen
++ {
++ q23::expected<QSize, ComStatus> physicalSize() const
++ {
++ DXGI_OUTPUT_DESC desc{};
++ const HRESULT hr = output->GetDesc(&desc);
++ if (hr != S_OK)
++ return q23::unexpected{ hr };
++
++ const RECT bounds = desc.DesktopCoordinates;
++ const QRect displayRect{ bounds.left, bounds.top,
++ bounds.right - bounds.left,
++ bounds.bottom - bounds.top };
++ return displayRect.size();
++ }
++
++ q23::expected<QtVideo::Rotation, ComStatus> rotation() const
++ {
++ DXGI_OUTPUT_DESC desc{};
++ const HRESULT hr = output->GetDesc(&desc);
++ if (hr != S_OK)
++ return q23::unexpected{ hr };
++
++ switch (desc.Rotation) {
++
++ case DXGI_MODE_ROTATION_ROTATE90:
++ return QtVideo::Rotation::Clockwise90;
++ case DXGI_MODE_ROTATION_ROTATE180:
++ return QtVideo::Rotation::Clockwise180;
++ case DXGI_MODE_ROTATION_ROTATE270:
++ return QtVideo::Rotation::Clockwise270;
++ default:
++ return QtVideo::Rotation::None;
++ }
++ }
++
++ ComPtr<IDXGIAdapter1> adapter;
++ ComPtr<IDXGIOutput> output;
++ };
++
++ q23::expected<DxgiScreen, ComStatus> findDxgiScreen(const QScreen *screen)
++ {
++ if (!screen)
++ return q23::unexpected{ ComStatus{ E_FAIL, "Cannot find nullptr screen"_L1 } };
++
++ auto *winScreen = screen->nativeInterface<QNativeInterface::QWindowsScreen>();
++ HMONITOR handle = winScreen ? winScreen->handle() : nullptr;
++
++ ComPtr<IDXGIFactory1> factory;
++ HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory));
++ if (FAILED(hr))
++ return q23::unexpected{ ComStatus{ hr, "Failed to create IDXGIFactory"_L1 } };
++
++ ComPtr<IDXGIAdapter1> adapter;
++ for (quint32 i = 0; factory->EnumAdapters1(i, adapter.ReleaseAndGetAddressOf()) == S_OK; i++) {
++ ComPtr<IDXGIOutput> output;
++ for (quint32 j = 0; adapter->EnumOutputs(j, output.ReleaseAndGetAddressOf()) == S_OK; ++j) {
++ DXGI_OUTPUT_DESC desc = {};
++ output->GetDesc(&desc);
++ qCDebug(qLcScreenCaptureDxgi) << i << j << QString::fromWCharArray(desc.DeviceName);
++ auto match = handle ? handle == desc.Monitor
++ : QString::fromWCharArray(desc.DeviceName) == screen->name();
++ if (match)
++ return DxgiScreen{ adapter, output };
++ }
++ }
++ return q23::unexpected{ ComStatus{
++ DXGI_ERROR_NOT_FOUND,
++ "Could not find screen adapter "_L1 + screen->name(),
++ } };
++ }
++
++ class DxgiDuplication
++ {
++ public:
++ ~DxgiDuplication()
++ {
++ if (m_releaseFrame)
++ m_dup->ReleaseFrame();
++ }
++
++ ComStatus initialize(QScreen const *screen)
++ {
++ q23::expected<DxgiScreen, ComStatus> dxgiScreen = findDxgiScreen(screen);
++ if (!dxgiScreen)
++ return dxgiScreen.error();
++
++ const ComPtr<IDXGIAdapter1> adapter = dxgiScreen->adapter;
++
++ ComPtr<ID3D11Device> d3d11dev;
++ HRESULT hr =
++ D3D11CreateDevice(adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, nullptr, 0,
++ D3D11_SDK_VERSION, d3d11dev.GetAddressOf(), nullptr, nullptr);
++ if (FAILED(hr))
++ return { hr, "Failed to create ID3D11Device device"_L1 };
++
++ ComPtr<IDXGIOutput1> output;
++ hr = dxgiScreen->output.As(&output);
++ if (FAILED(hr))
++ return { hr, "Failed to create IDXGIOutput1"_L1 };
++
++ ComPtr<IDXGIOutputDuplication> dup;
++ hr = output->DuplicateOutput(d3d11dev.Get(), dup.GetAddressOf());
++ if (FAILED(hr))
++ return { hr, "Failed to duplicate IDXGIOutput1"_L1 };
++
++ m_adapter = dxgiScreen->adapter;
++ m_output = output;
++ m_device = d3d11dev;
++ m_dup = dup;
++ return { S_OK };
++ }
++
++ bool valid() const { return m_dup != nullptr; }
++ q23::expected<std::unique_ptr<QD3D11TextureVideoBuffer>, ComStatus> getNextVideoFrame()
++ {
++ const ComProduct<ID3D11Texture2D> texture = getNextFrame();
++
++ if (!texture)
++ return q23::unexpected{ texture.error() };
++
++ return std::make_unique<QD3D11TextureVideoBuffer>(m_device, m_ctxMutex, *texture);
++ }
++
++ private:
++ ComProduct<ID3D11Texture2D> getNextFrame()
++ {
++ std::scoped_lock guard{ *m_ctxMutex };
++
++ if (m_releaseFrame) {
++ m_releaseFrame = false;
++
++ HRESULT hr = m_dup->ReleaseFrame();
++
++ if (hr != S_OK)
++ return q23::unexpected{ ComStatus{ hr,
++ "Failed to release duplication frame."_L1 } };
++ }
++
++ ComPtr<IDXGIResource> frame;
++ DXGI_OUTDUPL_FRAME_INFO info;
++
++ HRESULT hr = m_dup->AcquireNextFrame(0, &info, frame.GetAddressOf());
++
++ if (hr != S_OK)
++ return q23::unexpected{ ComStatus{ hr, "Failed to grab the screen content"_L1 } };
++
++ m_releaseFrame = true;
++
++ ComPtr<ID3D11Texture2D> tex;
++ hr = frame.As(&tex);
++ if (hr != S_OK)
++ return q23::unexpected{ ComStatus{ hr, "Failed to obtain D3D11 texture"_L1 } };
++
++ D3D11_TEXTURE2D_DESC texDesc = {};
++ tex->GetDesc(&texDesc);
++ texDesc.MiscFlags = 0;
++ texDesc.BindFlags = 0;
++
++ ComPtr<ID3D11Texture2D> texCopy;
++ hr = m_device->CreateTexture2D(&texDesc, nullptr, texCopy.GetAddressOf());
++ if (hr != S_OK)
++ return q23::unexpected{ ComStatus{ hr,
++ "Failed to create texture with CPU access"_L1 } };
++
++ ComPtr<ID3D11DeviceContext> ctx;
++ m_device->GetImmediateContext(ctx.GetAddressOf());
++ ctx->CopyResource(texCopy.Get(), tex.Get());
++
++ return texCopy;
++ }
++
++ ComPtr<IDXGIAdapter1> m_adapter;
++ ComPtr<IDXGIOutput> m_output;
++ ComPtr<ID3D11Device> m_device;
++ ComPtr<IDXGIOutputDuplication> m_dup;
++ bool m_releaseFrame = false;
++ std::shared_ptr<QMutex> m_ctxMutex = std::make_shared<QMutex>();
++ };
++
++ q23::expected<QVideoFrameFormat, ComStatus> getFrameFormat(const QScreen *screen)
++ {
++ const auto dxgiScreen = findDxgiScreen(screen);
++ if (!dxgiScreen)
++ return q23::unexpected{ dxgiScreen.error() };
++
++ const auto screenSize = dxgiScreen->physicalSize();
++ if (!screenSize)
++ return q23::unexpected{ screenSize.error() };
++
++ const auto rotation = dxgiScreen->rotation();
++ if (!rotation)
++ return q23::unexpected{ rotation.error() };
++
++ QVideoFrameFormat format = { *screenSize, QVideoFrameFormat::Format_BGRA8888 };
++ format.setRotation(*rotation);
++
++ return format;
++ }
++
++ } // namespace
++
++ class QFFmpegScreenCaptureDxgi::Grabber : public QSurfaceCaptureGrabber
++ {
++ public:
++ Grabber(QFFmpegScreenCaptureDxgi &screenCapture, QScreen *screen,
++ const QVideoFrameFormat &format)
++ : QSurfaceCaptureGrabber(), m_screen(screen), m_format(format)
++ {
++ addFrameCallback(&screenCapture, &QFFmpegScreenCaptureDxgi::newVideoFrame);
++ connect(this, &Grabber::errorUpdated, &screenCapture, &QFFmpegScreenCaptureDxgi::updateError);
++
++ Q_ASSERT(screen);
++ setFrameRate(screenCapture.frameRate().value_or(
++ qMin(screen->refreshRate(), DefaultScreenCaptureFrameRate)));
++ }
++
++ ~Grabber() {
++ stop();
++ }
++
++ QVideoFrameFormat format() {
++ return m_format;
++ }
++
++ QVideoFrame grabFrame() override
++ {
++ QVideoFrame frame;
++ if (!m_duplication.valid()) {
++ const ComStatus status = m_duplication.initialize(m_screen);
++ if (!status) {
++ if (status.code() == E_ACCESSDENIED) {
++ // May occur for some time after pushing Ctrl+Alt+Del.
++ updateError(QPlatformSurfaceCapture::Error::NoError, status.str());
++ qCWarning(qLcScreenCaptureDxgi) << status.str();
++ }
++ return frame;
++ }
++ }
++
++ auto maybeBuf = m_duplication.getNextVideoFrame();
++ if (maybeBuf) {
++ std::unique_ptr<QD3D11TextureVideoBuffer> buffer = std::move(*maybeBuf);
++
++ const QSize bufSize = buffer->getSize();
++ if (bufSize != m_format.frameSize())
++ m_format.setFrameSize(bufSize);
++
++ if (m_format.streamFrameRate() != activeFrameRate())
++ m_format.setStreamFrameRate(activeFrameRate());
++
++ frame = QVideoFramePrivate::createFrame(std::move(buffer), format());
++ } else {
++ const ComStatus &status = maybeBuf.error();
++
++ if (status.code() == DXGI_ERROR_WAIT_TIMEOUT) {
++ // All is good, we just didn't get a new frame yet
++ updateError(QPlatformSurfaceCapture::Error::NoError, status.str());
++ } else if (status.code() == DXGI_ERROR_ACCESS_LOST) {
++ // Can happen for example when pushing Ctrl + Alt + Del
++ m_duplication = {};
++ updateError(QPlatformSurfaceCapture::Error::NoError, status.str());
++ qCWarning(qLcScreenCaptureDxgi) << status.str();
++ } else if (!status) {
++ updateError(QPlatformSurfaceCapture::Error::CaptureFailed, status.str());
++ qCWarning(qLcScreenCaptureDxgi) << status.str();
++ }
++ }
++
++ return frame;
++ }
++
++ protected:
++ void initializeGrabbingContext() override
++ {
++ m_duplication = DxgiDuplication();
++ const ComStatus status = m_duplication.initialize(m_screen);
++ if (!status) {
++ updateError(QPlatformSurfaceCapture::Error::CaptureFailed, status.str());
++ return;
++ }
++
++ QSurfaceCaptureGrabber::initializeGrabbingContext();
++ }
++
++ private:
++ const QScreen *m_screen = nullptr;
++ QVideoFrameFormat m_format;
++ DxgiDuplication m_duplication;
++ };
++
++ QFFmpegScreenCaptureDxgi::QFFmpegScreenCaptureDxgi() : QPlatformSurfaceCapture(ScreenSource{}) { }
++
++ QFFmpegScreenCaptureDxgi::~QFFmpegScreenCaptureDxgi() = default;
++
++ QVideoFrameFormat QFFmpegScreenCaptureDxgi::frameFormat() const
++ {
++ if (m_grabber)
++ return m_grabber->format();
++ return {};
++ }
++
++ bool QFFmpegScreenCaptureDxgi::setActiveInternal(bool active)
++ {
++ if (static_cast<bool>(m_grabber) == active)
++ return true;
++
++ if (m_grabber) {
++ m_grabber.reset();
++ } else {
++ auto screen = source<ScreenSource>();
++
++ if (!checkScreenWithError(screen))
++ return false;
++
++ const auto format = getFrameFormat(screen);
++ if (!format) {
++ updateError(
++ QPlatformSurfaceCapture::Error::NotFound,
++ "Unable to determine screen size or format"_L1 + format.error().str());
++ return false;
++ }
++
++ m_grabber.reset(new Grabber(*this, screen, *format));
++ m_grabber->start();
++ }
++
++ return true;
++ }
++
++ QT_END_NAMESPACE