| 1 | // Copyright (C) 2022 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include "qvideoframeconverter_p.h" | 
| 5 | #include "qvideoframeconversionhelper_p.h" | 
| 6 | #include "qvideoframeformat.h" | 
| 7 | #include "qvideoframe_p.h" | 
| 8 | #include "qmultimediautils_p.h" | 
| 9 | #include "qthreadlocalrhi_p.h" | 
| 10 | #include "qcachedvalue_p.h" | 
| 11 |  | 
| 12 | #include <QtCore/qcoreapplication.h> | 
| 13 | #include <QtCore/qsize.h> | 
| 14 | #include <QtCore/qhash.h> | 
| 15 | #include <QtCore/qfile.h> | 
| 16 | #include <QtGui/qimage.h> | 
| 17 | #include <QtCore/qloggingcategory.h> | 
| 18 |  | 
| 19 | #include <private/qvideotexturehelper_p.h> | 
| 20 |  | 
| 21 | #include <rhi/qrhi.h> | 
| 22 |  | 
| 23 | #ifdef Q_OS_DARWIN | 
| 24 | #include <QtCore/private/qcore_mac_p.h> | 
| 25 | #endif | 
| 26 |  | 
| 27 | QT_BEGIN_NAMESPACE | 
| 28 |  | 
| 29 | static Q_LOGGING_CATEGORY(qLcVideoFrameConverter, "qt.multimedia.video.frameconverter" ) | 
| 30 |  | 
| 31 | // TODO: investigate if we should use thread_local instead, QTBUG-133565 | 
| 32 | static const float g_quad[] = { | 
| 33 |     // Rotation 0 CW | 
| 34 |     1.f, -1.f,   1.f, 1.f, | 
| 35 |     1.f,  1.f,   1.f, 0.f, | 
| 36 |    -1.f, -1.f,   0.f, 1.f, | 
| 37 |    -1.f,  1.f,   0.f, 0.f, | 
| 38 |     // Rotation 90 CW | 
| 39 |     1.f, -1.f,   1.f, 0.f, | 
| 40 |     1.f,  1.f,   0.f, 0.f, | 
| 41 |    -1.f, -1.f,   1.f, 1.f, | 
| 42 |    -1.f,  1.f,   0.f, 1.f, | 
| 43 |     // Rotation 180 CW | 
| 44 |     1.f, -1.f,   0.f, 0.f, | 
| 45 |     1.f,  1.f,   0.f, 1.f, | 
| 46 |    -1.f, -1.f,   1.f, 0.f, | 
| 47 |    -1.f,  1.f,   1.f, 1.f, | 
| 48 |     // Rotation 270 CW | 
| 49 |     1.f, -1.f,  0.f, 1.f, | 
| 50 |     1.f,  1.f,  1.f, 1.f, | 
| 51 |    -1.f, -1.f,  0.f, 0.f, | 
| 52 |    -1.f,  1.f,  1.f, 0.f, | 
| 53 | }; | 
| 54 |  | 
| 55 | static bool pixelFormatHasAlpha(QVideoFrameFormat::PixelFormat format) | 
| 56 | { | 
| 57 |     switch (format) { | 
| 58 |     case  QVideoFrameFormat::Format_ARGB8888: | 
| 59 |     case  QVideoFrameFormat::Format_ARGB8888_Premultiplied: | 
| 60 |     case  QVideoFrameFormat::Format_BGRA8888: | 
| 61 |     case  QVideoFrameFormat::Format_BGRA8888_Premultiplied: | 
| 62 |     case  QVideoFrameFormat::Format_ABGR8888: | 
| 63 |     case  QVideoFrameFormat::Format_RGBA8888: | 
| 64 |     case  QVideoFrameFormat::Format_AYUV: | 
| 65 |     case  QVideoFrameFormat::Format_AYUV_Premultiplied: | 
| 66 |         return true; | 
| 67 |     default: | 
| 68 |         return false; | 
| 69 |     } | 
| 70 | }; | 
| 71 |  | 
| 72 | static QShader ensureShader(const QString &name) | 
| 73 | { | 
| 74 |     static QCachedValueMap<QString, QShader> shaderCache; | 
| 75 |  | 
| 76 |     return shaderCache.ensure(key: name, creator: [&name]() { | 
| 77 |         QFile f(name); | 
| 78 |         return f.open(flags: QIODevice::ReadOnly) ? QShader::fromSerialized(data: f.readAll()) : QShader(); | 
| 79 |     }); | 
| 80 | } | 
| 81 |  | 
| 82 | static void rasterTransform(QImage &image, VideoTransformation transformation) | 
| 83 | { | 
| 84 |     QTransform t; | 
| 85 |     if (transformation.rotation != QtVideo::Rotation::None) | 
| 86 |         t.rotate(a: qreal(transformation.rotation)); | 
| 87 |     if (transformation.mirrorredHorizontallyAfterRotation) | 
| 88 |         t.scale(sx: -1., sy: 1); | 
| 89 |     if (!t.isIdentity()) | 
| 90 |         image = image.transformed(matrix: t); | 
| 91 | } | 
| 92 |  | 
| 93 | static void imageCleanupHandler(void *info) | 
| 94 | { | 
| 95 |     QByteArray *imageData = reinterpret_cast<QByteArray *>(info); | 
| 96 |     delete imageData; | 
| 97 | } | 
| 98 |  | 
| 99 | static bool updateTextures(QRhi *rhi, | 
| 100 |                            std::unique_ptr<QRhiBuffer> &uniformBuffer, | 
| 101 |                            std::unique_ptr<QRhiSampler> &textureSampler, | 
| 102 |                            std::unique_ptr<QRhiShaderResourceBindings> &shaderResourceBindings, | 
| 103 |                            std::unique_ptr<QRhiGraphicsPipeline> &graphicsPipeline, | 
| 104 |                            std::unique_ptr<QRhiRenderPassDescriptor> &renderPass, | 
| 105 |                            QVideoFrame &frame, | 
| 106 |                            const QVideoFrameTexturesUPtr &videoFrameTextures) | 
| 107 | { | 
| 108 |     auto format = frame.surfaceFormat(); | 
| 109 |     auto pixelFormat = format.pixelFormat(); | 
| 110 |  | 
| 111 |     auto textureDesc = QVideoTextureHelper::textureDescription(format: pixelFormat); | 
| 112 |  | 
| 113 |     QRhiShaderResourceBinding bindings[4]; | 
| 114 |     auto *b = bindings; | 
| 115 |     *b++ = QRhiShaderResourceBinding::uniformBuffer(binding: 0, stage: QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, | 
| 116 |                                                     buf: uniformBuffer.get()); | 
| 117 |     for (int i = 0; i < textureDesc->nplanes; ++i) | 
| 118 |         *b++ = QRhiShaderResourceBinding::sampledTexture(binding: i + 1, stage: QRhiShaderResourceBinding::FragmentStage, | 
| 119 |                                                          tex: videoFrameTextures->texture(plane: i), sampler: textureSampler.get()); | 
| 120 |     shaderResourceBindings->setBindings(first: bindings, last: b); | 
| 121 |     shaderResourceBindings->create(); | 
| 122 |  | 
| 123 |     graphicsPipeline.reset(p: rhi->newGraphicsPipeline()); | 
| 124 |     graphicsPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip); | 
| 125 |  | 
| 126 |     QShader vs = ensureShader(name: QVideoTextureHelper::vertexShaderFileName(format)); | 
| 127 |     if (!vs.isValid()) | 
| 128 |         return false; | 
| 129 |  | 
| 130 |     QShader fs = ensureShader(name: QVideoTextureHelper::fragmentShaderFileName(format, rhi)); | 
| 131 |     if (!fs.isValid()) | 
| 132 |         return false; | 
| 133 |  | 
| 134 |     graphicsPipeline->setShaderStages({ | 
| 135 |         { QRhiShaderStage::Vertex, vs }, | 
| 136 |         { QRhiShaderStage::Fragment, fs } | 
| 137 |     }); | 
| 138 |  | 
| 139 |     QRhiVertexInputLayout inputLayout; | 
| 140 |     inputLayout.setBindings({ | 
| 141 |         { 4 * sizeof(float) } | 
| 142 |     }); | 
| 143 |     inputLayout.setAttributes({ | 
| 144 |         { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, | 
| 145 |         { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) } | 
| 146 |     }); | 
| 147 |  | 
| 148 |     graphicsPipeline->setVertexInputLayout(inputLayout); | 
| 149 |     graphicsPipeline->setShaderResourceBindings(shaderResourceBindings.get()); | 
| 150 |     graphicsPipeline->setRenderPassDescriptor(renderPass.get()); | 
| 151 |     graphicsPipeline->create(); | 
| 152 |  | 
| 153 |     return true; | 
| 154 | } | 
| 155 |  | 
| 156 | static QImage convertJPEG(const QVideoFrame &frame, const VideoTransformation &transform) | 
| 157 | { | 
| 158 |     QVideoFrame varFrame = frame; | 
| 159 |     if (!varFrame.map(mode: QVideoFrame::ReadOnly)) { | 
| 160 |         qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": frame mapping failed" ; | 
| 161 |         return {}; | 
| 162 |     } | 
| 163 |     QImage image; | 
| 164 |     image.loadFromData(buf: varFrame.bits(plane: 0), len: varFrame.mappedBytes(plane: 0), format: "JPG" ); | 
| 165 |     varFrame.unmap(); | 
| 166 |     rasterTransform(image, transformation: transform); | 
| 167 |     return image; | 
| 168 | } | 
| 169 |  | 
| 170 | static QImage convertCPU(const QVideoFrame &frame, const VideoTransformation &transform) | 
| 171 | { | 
| 172 |     VideoFrameConvertFunc convert = qConverterForFormat(format: frame.pixelFormat()); | 
| 173 |     if (!convert) { | 
| 174 |         qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": unsupported pixel format"  << frame.pixelFormat(); | 
| 175 |         return {}; | 
| 176 |     } else { | 
| 177 |         QVideoFrame varFrame = frame; | 
| 178 |         if (!varFrame.map(mode: QVideoFrame::ReadOnly)) { | 
| 179 |             qCDebug(qLcVideoFrameConverter) << Q_FUNC_INFO << ": frame mapping failed" ; | 
| 180 |             return {}; | 
| 181 |         } | 
| 182 |         auto format = pixelFormatHasAlpha(format: varFrame.pixelFormat()) ? QImage::Format_ARGB32_Premultiplied : QImage::Format_RGB32; | 
| 183 |         QImage image = QImage(varFrame.width(), varFrame.height(), format); | 
| 184 |         convert(varFrame, image.bits()); | 
| 185 |         varFrame.unmap(); | 
| 186 |         rasterTransform(image, transformation: transform); | 
| 187 |         return image; | 
| 188 |     } | 
| 189 | } | 
| 190 |  | 
| 191 | QImage qImageFromVideoFrame(const QVideoFrame &frame, bool forceCpu) | 
| 192 | { | 
| 193 |     // by default, surface transformation is applied, as full transformation is used for presentation only | 
| 194 |     return qImageFromVideoFrame(frame, transformation: qNormalizedSurfaceTransformation(format: frame.surfaceFormat()), | 
| 195 |                                 forceCpu); | 
| 196 | } | 
| 197 |  | 
| 198 | QImage qImageFromVideoFrame(const QVideoFrame &frame, const VideoTransformation &transformation, | 
| 199 |                             bool forceCpu) | 
| 200 | { | 
| 201 | #ifdef Q_OS_DARWIN | 
| 202 |     QMacAutoReleasePool releasePool; | 
| 203 | #endif | 
| 204 |  | 
| 205 |     std::unique_ptr<QRhiRenderPassDescriptor> renderPass; | 
| 206 |     std::unique_ptr<QRhiBuffer> vertexBuffer; | 
| 207 |     std::unique_ptr<QRhiBuffer> uniformBuffer; | 
| 208 |     std::unique_ptr<QRhiTexture> targetTexture; | 
| 209 |     std::unique_ptr<QRhiTextureRenderTarget> renderTarget; | 
| 210 |     std::unique_ptr<QRhiSampler> textureSampler; | 
| 211 |     std::unique_ptr<QRhiShaderResourceBindings> shaderResourceBindings; | 
| 212 |     std::unique_ptr<QRhiGraphicsPipeline> graphicsPipeline; | 
| 213 |  | 
| 214 |     if (frame.size().isEmpty() || frame.pixelFormat() == QVideoFrameFormat::Format_Invalid) | 
| 215 |         return {}; | 
| 216 |  | 
| 217 |     if (frame.pixelFormat() == QVideoFrameFormat::Format_Jpeg) | 
| 218 |         return convertJPEG(frame, transform: transformation); | 
| 219 |  | 
| 220 |     if (forceCpu) // For test purposes | 
| 221 |         return convertCPU(frame, transform: transformation); | 
| 222 |  | 
| 223 |     QRhi *rhi = nullptr; | 
| 224 |  | 
| 225 |     if (QHwVideoBuffer *buffer = QVideoFramePrivate::hwBuffer(frame)) | 
| 226 |         rhi = buffer->rhi(); | 
| 227 |  | 
| 228 |     if (!rhi || !rhi->thread()->isCurrentThread()) | 
| 229 |         rhi = ensureThreadLocalRhi(referenceRhi: rhi); | 
| 230 |  | 
| 231 |     if (!rhi || rhi->isRecordingFrame()) | 
| 232 |         return convertCPU(frame, transform: transformation); | 
| 233 |  | 
| 234 |     // Do conversion using shaders | 
| 235 |  | 
| 236 |     const QSize frameSize = qRotatedFrameSize(size: frame.size(), rotation: frame.surfaceFormat().rotation()); | 
| 237 |  | 
| 238 |     vertexBuffer.reset(p: rhi->newBuffer(type: QRhiBuffer::Immutable, usage: QRhiBuffer::VertexBuffer, size: sizeof(g_quad))); | 
| 239 |     vertexBuffer->create(); | 
| 240 |  | 
| 241 |     uniformBuffer.reset(p: rhi->newBuffer(type: QRhiBuffer::Dynamic, usage: QRhiBuffer::UniformBuffer, size: sizeof(QVideoTextureHelper::UniformData))); | 
| 242 |     uniformBuffer->create(); | 
| 243 |  | 
| 244 |     textureSampler.reset(p: rhi->newSampler(magFilter: QRhiSampler::Linear, minFilter: QRhiSampler::Linear, mipmapMode: QRhiSampler::None, | 
| 245 |                                          addressU: QRhiSampler::ClampToEdge, addressV: QRhiSampler::ClampToEdge)); | 
| 246 |     textureSampler->create(); | 
| 247 |  | 
| 248 |     shaderResourceBindings.reset(p: rhi->newShaderResourceBindings()); | 
| 249 |  | 
| 250 |     targetTexture.reset(p: rhi->newTexture(format: QRhiTexture::RGBA8, pixelSize: frameSize, sampleCount: 1, flags: QRhiTexture::RenderTarget)); | 
| 251 |     if (!targetTexture->create()) { | 
| 252 |         qCDebug(qLcVideoFrameConverter) << "Failed to create target texture. Using CPU conversion." ; | 
| 253 |         return convertCPU(frame, transform: transformation); | 
| 254 |     } | 
| 255 |  | 
| 256 |     renderTarget.reset(p: rhi->newTextureRenderTarget(desc: { { targetTexture.get() } })); | 
| 257 |     renderPass.reset(p: renderTarget->newCompatibleRenderPassDescriptor()); | 
| 258 |     renderTarget->setRenderPassDescriptor(renderPass.get()); | 
| 259 |     renderTarget->create(); | 
| 260 |  | 
| 261 |     QRhiCommandBuffer *cb = nullptr; | 
| 262 |     QRhi::FrameOpResult r = rhi->beginOffscreenFrame(cb: &cb); | 
| 263 |     if (r != QRhi::FrameOpSuccess) { | 
| 264 |         qCDebug(qLcVideoFrameConverter) << "Failed to set up offscreen frame. Using CPU conversion." ; | 
| 265 |         return convertCPU(frame, transform: transformation); | 
| 266 |     } | 
| 267 |  | 
| 268 |     QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch(); | 
| 269 |     Q_ASSERT(rub); | 
| 270 |  | 
| 271 |     rub->uploadStaticBuffer(buf: vertexBuffer.get(), data: g_quad); | 
| 272 |  | 
| 273 |     QVideoFrame frameTmp = frame; | 
| 274 |     auto videoFrameTextures = QVideoTextureHelper::createTextures(frame: frameTmp, rhi&: *rhi, rub&: *rub, oldTextures: {}); | 
| 275 |     if (!videoFrameTextures) { | 
| 276 |         qCDebug(qLcVideoFrameConverter) << "Failed obtain textures. Using CPU conversion." ; | 
| 277 |         return convertCPU(frame, transform: transformation); | 
| 278 |     } | 
| 279 |  | 
| 280 |     if (!updateTextures(rhi, uniformBuffer, textureSampler, shaderResourceBindings, | 
| 281 |                         graphicsPipeline, renderPass, frame&: frameTmp, videoFrameTextures)) { | 
| 282 |         qCDebug(qLcVideoFrameConverter) << "Failed to update textures. Using CPU conversion." ; | 
| 283 |         return convertCPU(frame, transform: transformation); | 
| 284 |     } | 
| 285 |  | 
| 286 |     float xScale = transformation.mirrorredHorizontallyAfterRotation ? -1.0 : 1.0; | 
| 287 |     float yScale = 1.f; | 
| 288 |  | 
| 289 |     if (rhi->isYUpInFramebuffer()) | 
| 290 |         yScale = -yScale; | 
| 291 |  | 
| 292 |     QMatrix4x4 transform; | 
| 293 |     transform.scale(x: xScale, y: yScale); | 
| 294 |  | 
| 295 |     QByteArray uniformData(sizeof(QVideoTextureHelper::UniformData), Qt::Uninitialized); | 
| 296 |     QVideoTextureHelper::updateUniformData(dst: &uniformData, rhi, format: frame.surfaceFormat(), frame, | 
| 297 |                                            transform, opacity: 1.f); | 
| 298 |     rub->updateDynamicBuffer(buf: uniformBuffer.get(), offset: 0, size: uniformData.size(), data: uniformData.constData()); | 
| 299 |  | 
| 300 |     cb->beginPass(rt: renderTarget.get(), colorClearValue: Qt::black, depthStencilClearValue: { 1.0f, 0 }, resourceUpdates: rub); | 
| 301 |     cb->setGraphicsPipeline(graphicsPipeline.get()); | 
| 302 |  | 
| 303 |     cb->setViewport({ 0, 0, float(frameSize.width()), float(frameSize.height()) }); | 
| 304 |     cb->setShaderResources(srb: shaderResourceBindings.get()); | 
| 305 |  | 
| 306 |     const quint32 vertexOffset = quint32(sizeof(float)) * 16 * transformation.rotationIndex(); | 
| 307 |     const QRhiCommandBuffer::VertexInput vbufBinding(vertexBuffer.get(), vertexOffset); | 
| 308 |     cb->setVertexInput(startBinding: 0, bindingCount: 1, bindings: &vbufBinding); | 
| 309 |     cb->draw(vertexCount: 4); | 
| 310 |  | 
| 311 |     QRhiReadbackDescription readDesc(targetTexture.get()); | 
| 312 |     QRhiReadbackResult readResult; | 
| 313 |     bool readCompleted = false; | 
| 314 |  | 
| 315 |     readResult.completed = [&readCompleted] { readCompleted = true; }; | 
| 316 |  | 
| 317 |     rub = rhi->nextResourceUpdateBatch(); | 
| 318 |     rub->readBackTexture(rb: readDesc, result: &readResult); | 
| 319 |  | 
| 320 |     cb->endPass(resourceUpdates: rub); | 
| 321 |  | 
| 322 |     rhi->endOffscreenFrame(); | 
| 323 |  | 
| 324 |     if (!readCompleted) { | 
| 325 |         qCDebug(qLcVideoFrameConverter) << "Failed to read back texture. Using CPU conversion." ; | 
| 326 |         return convertCPU(frame, transform: transformation); | 
| 327 |     } | 
| 328 |  | 
| 329 |     QByteArray *imageData = new QByteArray(readResult.data); | 
| 330 |  | 
| 331 |     return QImage(reinterpret_cast<const uchar *>(imageData->constData()), | 
| 332 |                   readResult.pixelSize.width(), readResult.pixelSize.height(), | 
| 333 |                   QImage::Format_RGBA8888_Premultiplied, imageCleanupHandler, imageData); | 
| 334 | } | 
| 335 |  | 
| 336 | QImage videoFramePlaneAsImage(QVideoFrame &frame, int plane, QImage::Format targetFormat, | 
| 337 |                               QSize targetSize) | 
| 338 | { | 
| 339 |     if (plane >= frame.planeCount()) | 
| 340 |         return {}; | 
| 341 |  | 
| 342 |     if (!frame.map(mode: QVideoFrame::ReadOnly)) { | 
| 343 |         qWarning() << "Cannot map a video frame in ReadOnly mode!" ; | 
| 344 |         return {}; | 
| 345 |     } | 
| 346 |  | 
| 347 |     auto frameHandle = QVideoFramePrivate::handle(frame); | 
| 348 |  | 
| 349 |     // With incrementing the reference counter, we share the mapped QVideoFrame | 
| 350 |     // with the target QImage. The function imageCleanupFunction is going to adopt | 
| 351 |     // the frameHandle by QVideoFrame and dereference it upon the destruction. | 
| 352 |     frameHandle->ref.ref(); | 
| 353 |  | 
| 354 |     auto imageCleanupFunction = [](void *data) { | 
| 355 |         QVideoFrame frame = reinterpret_cast<QVideoFramePrivate *>(data)->adoptThisByVideoFrame(); | 
| 356 |         Q_ASSERT(frame.isMapped()); | 
| 357 |         frame.unmap(); | 
| 358 |     }; | 
| 359 |  | 
| 360 |     const auto bytesPerLine = frame.bytesPerLine(plane); | 
| 361 |     const auto height = | 
| 362 |             bytesPerLine ? qMin(a: targetSize.height(), b: frame.mappedBytes(plane) / bytesPerLine) : 0; | 
| 363 |  | 
| 364 |     return QImage(reinterpret_cast<const uchar *>(frame.bits(plane)), targetSize.width(), height, | 
| 365 |                   bytesPerLine, targetFormat, imageCleanupFunction, frameHandle); | 
| 366 | } | 
| 367 |  | 
| 368 | QT_END_NAMESPACE | 
| 369 |  | 
| 370 |  |