Revision control

Copy as Markdown

// 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 "qvideowindow_p.h"
#include <QPlatformSurfaceEvent>
#include <qfile.h>
#include <qpainter.h>
#include <private/qguiapplication_p.h>
#include <private/qmemoryvideobuffer_p.h>
#include <private/qhwvideobuffer_p.h>
#include <private/qmultimediautils_p.h>
#include <private/qvideoframe_p.h>
#include <qpa/qplatformintegration.h>
QT_BEGIN_NAMESPACE
static QSurface::SurfaceType platformSurfaceType()
{
#if defined(Q_OS_DARWIN)
return QSurface::MetalSurface;
#elif defined (Q_OS_WIN)
return QSurface::Direct3DSurface;
#endif
auto *integration = QGuiApplicationPrivate::platformIntegration();
if (!integration->hasCapability(QPlatformIntegration::OpenGL))
return QSurface::RasterSurface;
if (QCoreApplication::testAttribute(Qt::AA_ForceRasterWidgets))
return QSurface::RasterSurface;
return QSurface::OpenGLSurface;
}
QVideoWindowPrivate::QVideoWindowPrivate(QVideoWindow *q)
: q(q),
m_sink(new QVideoSink)
{
Q_ASSERT(q);
if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::RhiBasedRendering)) {
auto surfaceType = ::platformSurfaceType();
q->setSurfaceType(surfaceType);
switch (surfaceType) {
case QSurface::RasterSurface:
case QSurface::OpenVGSurface:
default:
// can't use those surfaces, need to render in SW
m_graphicsApi = QRhi::Null;
break;
case QSurface::OpenGLSurface:
m_graphicsApi = QRhi::OpenGLES2;
break;
case QSurface::VulkanSurface:
m_graphicsApi = QRhi::Vulkan;
break;
case QSurface::MetalSurface:
m_graphicsApi = QRhi::Metal;
break;
case QSurface::Direct3DSurface:
m_graphicsApi = QRhi::D3D11;
break;
}
}
QObject::connect(m_sink.get(), &QVideoSink::videoFrameChanged, q, &QVideoWindow::setVideoFrame);
}
QVideoWindowPrivate::~QVideoWindowPrivate()
{
QObject::disconnect(m_sink.get(), &QVideoSink::videoFrameChanged,
q, &QVideoWindow::setVideoFrame);
}
static const float g_vw_quad[] = {
// 4 clockwise rotation of texture vertexes (the second pair)
// Rotation 0
-1.f, -1.f, 0.f, 0.f,
-1.f, 1.f, 0.f, 1.f,
1.f, -1.f, 1.f, 0.f,
1.f, 1.f, 1.f, 1.f,
// Rotation 90
-1.f, -1.f, 0.f, 1.f,
-1.f, 1.f, 1.f, 1.f,
1.f, -1.f, 0.f, 0.f,
1.f, 1.f, 1.f, 0.f,
// Rotation 180
-1.f, -1.f, 1.f, 1.f,
-1.f, 1.f, 1.f, 0.f,
1.f, -1.f, 0.f, 1.f,
1.f, 1.f, 0.f, 0.f,
// Rotation 270
-1.f, -1.f, 1.f, 0.f,
-1.f, 1.f, 0.f, 0.f,
1.f, -1.f, 1.f, 1.f,
1.f, 1.f, 0.f, 1.f
};
static QShader vwGetShader(const QString &name)
{
QFile f(name);
if (f.open(QIODevice::ReadOnly))
return QShader::fromSerialized(f.readAll());
return QShader();
}
void QVideoWindowPrivate::initRhi()
{
if (m_graphicsApi == QRhi::Null)
return;
[[maybe_unused]] QRhi::Flags rhiFlags = {};//QRhi::EnableDebugMarkers | QRhi::EnableProfiling;
#if QT_CONFIG(opengl)
if (m_graphicsApi == QRhi::OpenGLES2) {
m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface(q->format()));
QRhiGles2InitParams params;
params.fallbackSurface = m_fallbackSurface.get();
params.window = q;
params.format = q->format();
m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params, rhiFlags));
}
#endif
#if QT_CONFIG(vulkan)
if (m_graphicsApi == QRhi::Vulkan) {
QRhiVulkanInitParams params;
params.inst = q->vulkanInstance();
params.window = q;
m_rhi.reset(QRhi::create(QRhi::Vulkan, &params, rhiFlags));
}
#endif
#ifdef Q_OS_WIN
if (m_graphicsApi == QRhi::D3D11) {
QRhiD3D11InitParams params;
params.enableDebugLayer = true;
m_rhi.reset(QRhi::create(QRhi::D3D11, &params, rhiFlags));
}
#endif
#if QT_CONFIG(metal)
if (m_graphicsApi == QRhi::Metal) {
QRhiMetalInitParams params;
m_rhi.reset(QRhi::create(QRhi::Metal, &params, rhiFlags));
}
#endif
if (!m_rhi)
return;
m_swapChain.reset(m_rhi->newSwapChain());
m_swapChain->setWindow(q);
m_renderPass.reset(m_swapChain->newCompatibleRenderPassDescriptor());
m_swapChain->setRenderPassDescriptor(m_renderPass.get());
m_vertexBuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(g_vw_quad)));
m_vertexBuf->create();
m_vertexBufReady = false;
m_uniformBuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(QVideoTextureHelper::UniformData)));
m_uniformBuf->create();
m_textureSampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
m_textureSampler->create();
m_shaderResourceBindings.reset(m_rhi->newShaderResourceBindings());
m_subtitleResourceBindings.reset(m_rhi->newShaderResourceBindings());
m_subtitleUniformBuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(QVideoTextureHelper::UniformData)));
m_subtitleUniformBuf->create();
}
void QVideoWindowPrivate::setupGraphicsPipeline(QRhiGraphicsPipeline *pipeline, QRhiShaderResourceBindings *bindings, const QVideoFrameFormat &fmt)
{
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = vwGetShader(QVideoTextureHelper::vertexShaderFileName(fmt));
Q_ASSERT(vs.isValid());
QShader fs = vwGetShader(QVideoTextureHelper::fragmentShaderFileName(
fmt, m_rhi.get(), m_swapChain->format()));
Q_ASSERT(fs.isValid());
pipeline->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
{ 4 * sizeof(float) }
});
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },
{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
});
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(bindings);
pipeline->setRenderPassDescriptor(m_renderPass.get());
pipeline->create();
}
void QVideoWindowPrivate::updateTextures(QRhiResourceUpdateBatch *rub)
{
// We render a 1x1 black pixel when we don't have a video
if (!m_texturePool.currentFrame().isValid())
m_texturePool.setCurrentFrame(QVideoFramePrivate::createFrame(
std::make_unique<QMemoryVideoBuffer>(QByteArray{ 4, 0 }, 4),
QVideoFrameFormat(QSize(1, 1), QVideoFrameFormat::Format_RGBA8888)));
if (!m_texturePool.texturesDirty())
return;
QVideoFrameTextures *textures = m_texturePool.updateTextures(*m_rhi, *rub);
if (!textures)
return;
QRhiShaderResourceBinding bindings[4];
auto *b = bindings;
*(b++) = QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
m_uniformBuf.get());
auto fmt = m_texturePool.currentFrame().surfaceFormat();
auto textureDesc = QVideoTextureHelper::textureDescription(fmt.pixelFormat());
for (int i = 0; i < textureDesc->nplanes; ++i)
(*b++) = QRhiShaderResourceBinding::sampledTexture(
i + 1, QRhiShaderResourceBinding::FragmentStage, textures->texture(i),
m_textureSampler.get());
m_shaderResourceBindings->setBindings(bindings, b);
m_shaderResourceBindings->create();
if (fmt != format) {
format = fmt;
if (!m_graphicsPipeline)
m_graphicsPipeline.reset(m_rhi->newGraphicsPipeline());
setupGraphicsPipeline(m_graphicsPipeline.get(), m_shaderResourceBindings.get(), format);
}
}
void QVideoWindowPrivate::updateSubtitle(QRhiResourceUpdateBatch *rub, const QSize &frameSize)
{
m_subtitleDirty = false;
m_hasSubtitle = !m_texturePool.currentFrame().subtitleText().isEmpty();
if (!m_hasSubtitle)
return;
m_subtitleLayout.update(frameSize, m_texturePool.currentFrame().subtitleText());
QSize size = m_subtitleLayout.bounds.size().toSize();
QImage img = m_subtitleLayout.toImage();
m_subtitleTexture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, size));
m_subtitleTexture->create();
rub->uploadTexture(m_subtitleTexture.get(), img);
QRhiShaderResourceBinding bindings[2];
bindings[0] = QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
m_subtitleUniformBuf.get());
bindings[1] = QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage,
m_subtitleTexture.get(), m_textureSampler.get());
m_subtitleResourceBindings->setBindings(bindings, bindings + 2);
m_subtitleResourceBindings->create();
if (!m_subtitlePipeline) {
m_subtitlePipeline.reset(m_rhi->newGraphicsPipeline());
QRhiGraphicsPipeline::TargetBlend blend;
blend.enable = true;
m_subtitlePipeline->setTargetBlends({ blend });
setupGraphicsPipeline(m_subtitlePipeline.get(), m_subtitleResourceBindings.get(), QVideoFrameFormat(QSize(1, 1), QVideoFrameFormat::Format_RGBA8888));
}
}
void QVideoWindowPrivate::init()
{
if (initialized)
return;
initialized = true;
initRhi();
if (!m_rhi)
backingStore = new QBackingStore(q);
else
m_sink->setRhi(m_rhi.get());
}
void QVideoWindowPrivate::resizeSwapChain()
{
m_hasSwapChain = m_swapChain->createOrResize();
}
void QVideoWindowPrivate::releaseSwapChain()
{
if (m_hasSwapChain) {
m_hasSwapChain = false;
m_swapChain->destroy();
}
}
void QVideoWindowPrivate::render()
{
if (!initialized)
init();
if (!q->isExposed() || !isExposed)
return;
QRect rect(0, 0, q->width(), q->height());
if (backingStore) {
if (backingStore->size() != q->size())
backingStore->resize(q->size());
backingStore->beginPaint(rect);
QPaintDevice *device = backingStore->paintDevice();
if (!device)
return;
QPainter painter(device);
QVideoFrame frame = m_texturePool.currentFrame();
frame.paint(&painter, rect, { Qt::black, aspectRatioMode });
painter.end();
backingStore->endPaint();
backingStore->flush(rect);
return;
}
const VideoTransformation frameTransformation =
qNormalizedFrameTransformation(m_texturePool.currentFrame().surfaceFormat());
const QSize frameSize = qRotatedFramePresentationSize(m_texturePool.currentFrame());
const QSize scaled = frameSize.scaled(rect.size(), aspectRatioMode);
QRect videoRect = QRect(QPoint(0, 0), scaled);
videoRect.moveCenter(rect.center());
QRect subtitleRect = videoRect.intersected(rect);
if (!m_hasSwapChain || (m_swapChain->currentPixelSize() != m_swapChain->surfacePixelSize()))
resizeSwapChain();
const auto requiredSwapChainFormat =
qGetRequiredSwapChainFormat(m_texturePool.currentFrame().surfaceFormat());
if (qShouldUpdateSwapChainFormat(m_swapChain.get(), requiredSwapChainFormat)) {
releaseSwapChain();
m_swapChain->setFormat(requiredSwapChainFormat);
resizeSwapChain();
}
if (!m_hasSwapChain)
return;
QRhi::FrameOpResult r = m_rhi->beginFrame(m_swapChain.get());
if (r == QRhi::FrameOpSwapChainOutOfDate) {
resizeSwapChain();
if (!m_hasSwapChain)
return;
r = m_rhi->beginFrame(m_swapChain.get());
}
if (r != QRhi::FrameOpSuccess) {
qWarning("beginFrame failed with %d, retry", r);
q->requestUpdate();
return;
}
QRhiResourceUpdateBatch *rub = m_rhi->nextResourceUpdateBatch();
if (!m_vertexBufReady) {
m_vertexBufReady = true;
rub->uploadStaticBuffer(m_vertexBuf.get(), g_vw_quad);
}
updateTextures(rub);
if (m_subtitleDirty || m_subtitleLayout.videoSize != subtitleRect.size())
updateSubtitle(rub, subtitleRect.size());
const float mirrorFrame = frameTransformation.mirroredHorizontallyAfterRotation ? -1.f : 1.f;
const float xscale = mirrorFrame * float(videoRect.width()) / float(rect.width());
const float yscale = -1.f * float(videoRect.height()) / float(rect.height());
QMatrix4x4 transform;
transform.scale(xscale, yscale);
float maxNits = 100;
if (m_swapChain->format() == QRhiSwapChain::HDRExtendedSrgbLinear) {
auto info = m_swapChain->hdrInfo();
if (info.limitsType == QRhiSwapChainHdrInfo::ColorComponentValue)
maxNits = 100 * info.limits.colorComponentValue.maxColorComponentValue;
else
maxNits = info.limits.luminanceInNits.maxLuminance;
}
QByteArray uniformData;
QVideoTextureHelper::updateUniformData(&uniformData, m_rhi.get(),
m_texturePool.currentFrame().surfaceFormat(),
m_texturePool.currentFrame(), transform, 1.f, maxNits);
rub->updateDynamicBuffer(m_uniformBuf.get(), 0, uniformData.size(), uniformData.constData());
if (m_hasSubtitle) {
QMatrix4x4 st;
st.translate(0, -2.f * (float(m_subtitleLayout.bounds.center().y()) + float(subtitleRect.top()))/ float(rect.height()) + 1.f);
st.scale(float(m_subtitleLayout.bounds.width())/float(rect.width()),
-1.f * float(m_subtitleLayout.bounds.height())/float(rect.height()));
QByteArray uniformData;
QVideoFrameFormat fmt(m_subtitleLayout.bounds.size().toSize(), QVideoFrameFormat::Format_ARGB8888);
QVideoTextureHelper::updateUniformData(&uniformData, m_rhi.get(), fmt, QVideoFrame(), st,
1.f);
rub->updateDynamicBuffer(m_subtitleUniformBuf.get(), 0, uniformData.size(), uniformData.constData());
}
QRhiCommandBuffer *cb = m_swapChain->currentFrameCommandBuffer();
cb->beginPass(m_swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, rub);
cb->setGraphicsPipeline(m_graphicsPipeline.get());
auto size = m_swapChain->currentPixelSize();
cb->setViewport({ 0, 0, float(size.width()), float(size.height()) });
cb->setShaderResources(m_shaderResourceBindings.get());
const quint32 vertexOffset = quint32(sizeof(float)) * 16 * frameTransformation.rotationIndex();
const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuf.get(), vertexOffset);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(4);
if (m_hasSubtitle) {
cb->setGraphicsPipeline(m_subtitlePipeline.get());
cb->setShaderResources(m_subtitleResourceBindings.get());
const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuf.get(), 0);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(4);
}
cb->endPass();
m_rhi->endFrame(m_swapChain.get());
m_texturePool.onFrameEndInvoked();
}
/*!
\class QVideoWindow
\internal
*/
QVideoWindow::QVideoWindow(QScreen *screen)
: QWindow(screen)
, d(new QVideoWindowPrivate(this))
{
}
QVideoWindow::QVideoWindow(QWindow *parent)
: QWindow(parent)
, d(new QVideoWindowPrivate(this))
{
}
QVideoWindow::~QVideoWindow() = default;
QVideoSink *QVideoWindow::videoSink() const
{
return d->m_sink.get();
}
Qt::AspectRatioMode QVideoWindow::aspectRatioMode() const
{
return d->aspectRatioMode;
}
void QVideoWindow::setAspectRatioMode(Qt::AspectRatioMode mode)
{
if (d->aspectRatioMode == mode)
return;
d->aspectRatioMode = mode;
Q_EMIT aspectRatioModeChanged(mode);
}
bool QVideoWindow::event(QEvent *e)
{
switch (e->type()) {
case QEvent::UpdateRequest:
d->render();
return true;
case QEvent::PlatformSurface:
// this is the proper time to tear down the swapchain (while the native window and surface are still around)
if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) {
d->releaseSwapChain();
d->isExposed = false;
}
break;
case QEvent::Expose:
d->isExposed = isExposed();
if (d->isExposed)
d->render();
return true;
default:
break;
}
return QWindow::event(e);
}
void QVideoWindow::resizeEvent(QResizeEvent *resizeEvent)
{
if (!d->backingStore)
return;
if (!d->initialized)
d->init();
d->backingStore->resize(resizeEvent->size());
}
void QVideoWindow::setVideoFrame(const QVideoFrame &frame)
{
if (d->m_texturePool.currentFrame().subtitleText() != frame.subtitleText())
d->m_subtitleDirty = true;
d->m_texturePool.setCurrentFrame(frame);
if (d->isExposed)
requestUpdate();
}
QT_END_NAMESPACE
#include "moc_qvideowindow_p.cpp"