diff --git a/src/tools/qmlpuppet/CMakeLists.txt b/src/tools/qmlpuppet/CMakeLists.txt index ffad8a01fae..9f20bbe3d9e 100644 --- a/src/tools/qmlpuppet/CMakeLists.txt +++ b/src/tools/qmlpuppet/CMakeLists.txt @@ -127,6 +127,12 @@ extend_qtc_executable(qmlpuppet import3d.cpp import3d.h ) +extend_qtc_executable(qmlpuppet + SOURCES_PREFIX qmlpuppet/renderer + SOURCES + qmlrenderer.cpp qmlrenderer.h +) + extend_qtc_executable(qmlpuppet SOURCES_PREFIX qmlpuppet/instances SOURCES diff --git a/src/tools/qmlpuppet/qmlpuppet/qmlbase.h b/src/tools/qmlpuppet/qmlpuppet/qmlbase.h index 45be0cae71d..7c1333544a9 100644 --- a/src/tools/qmlpuppet/qmlpuppet/qmlbase.h +++ b/src/tools/qmlpuppet/qmlpuppet/qmlbase.h @@ -26,6 +26,7 @@ public: { m_argParser.setApplicationDescription("QML Runtime Provider for QDS"); m_argParser.addOption({"qml-puppet", "Run QML Puppet (default)"}); + m_argParser.addOption({"qml-renderer", "Run QML Renderer"}); #ifdef ENABLE_INTERNAL_QML_RUNTIME m_argParser.addOption({"qml-runtime", "Run QML Runtime"}); #endif diff --git a/src/tools/qmlpuppet/qmlpuppet/qmlpuppetmain.cpp b/src/tools/qmlpuppet/qmlpuppet/qmlpuppetmain.cpp index 937bb36d8eb..eb1ab9b6bb0 100644 --- a/src/tools/qmlpuppet/qmlpuppet/qmlpuppetmain.cpp +++ b/src/tools/qmlpuppet/qmlpuppet/qmlpuppetmain.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qmlpuppet.h" +#include "qmlrenderer.h" #ifdef ENABLE_INTERNAL_QML_RUNTIME #include "runner/qmlruntime.h" @@ -37,15 +38,23 @@ void registerMessageHandler( auto getQmlRunner(int &argc, char **argv) { -#ifdef ENABLE_INTERNAL_QML_RUNTIME - QString qmlRuntime("--qml-runtime"); + const QString qmlRuntime("--qml-runtime"); + const QString qmlRenderer("--qml-renderer"); for (int i = 0; i < argc; i++) { - if (!qmlRuntime.compare(QString::fromLocal8Bit(argv[i]))) { + const QString currentArg = QString::fromLocal8Bit(argv[i]); + if (!qmlRuntime.compare(currentArg)) { +#ifdef ENABLE_INTERNAL_QML_RUNTIME qInfo() << "Starting QML Runtime"; return std::unique_ptr(new QmlRuntime(argc, argv)); +#else + qInfo() << "QML Runtime not supported"; +#endif + } else if (!qmlRenderer.compare(currentArg)) { + qInfo() << "Starting QML Renderer"; + return std::unique_ptr(new QmlRenderer(argc, argv)); + } } -#endif qInfo() << "Starting QML Puppet"; return std::unique_ptr(new QmlPuppet(argc, argv)); } diff --git a/src/tools/qmlpuppet/qmlpuppet/renderer/qmlrenderer.cpp b/src/tools/qmlpuppet/qmlpuppet/renderer/qmlrenderer.cpp new file mode 100644 index 00000000000..bd67246271b --- /dev/null +++ b/src/tools/qmlpuppet/qmlpuppet/renderer/qmlrenderer.cpp @@ -0,0 +1,318 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmlrenderer.h" + +#ifdef QUICK3D_MODULE +#include "../editor3d/generalhelper.h" + +#include +#include +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include + +void QmlRenderer::initCoreApp() +{ +#if defined QT_WIDGETS_LIB + createCoreApp(); +#else + createCoreApp(); +#endif //QT_WIDGETS_LIB +} + +void QmlRenderer::populateParser() +{ + m_argParser.addOptions({ + {QStringList() << "i" << "importpath", + "Prepend the given path to the import paths.", + "path"}, + + {QStringList() << "o" << "outfile", + "Output image file path.", + "path"}, + + // "h" is reserved arg for help, so use capital letters for height/width + {QStringList() << "H" << "height", + "Height of the final rendered image.", + "pixels"}, + + {QStringList() << "W" << "width", + "Width of the final rendered image.", + "pixels"}, + + {QStringList() << "v" << "verbose", "Display additional output."} + }); + + m_argParser.addPositionalArgument("file", "QML file to render.", "file"); +} + +void QmlRenderer::initQmlRunner() +{ + if (m_argParser.isSet("importpath")) + m_importPaths = m_argParser.value("importpath").split(";"); + if (m_argParser.isSet("width")) + m_requestedSize.setWidth(m_argParser.value("width").toInt()); + if (m_argParser.isSet("height")) + m_requestedSize.setHeight(m_argParser.value("height").toInt()); + if (m_argParser.isSet("verbose")) + m_verbose = true; + + QStringList posArgs = m_argParser.positionalArguments(); + if (!posArgs.isEmpty()) + m_sourceFile = posArgs[0]; + + QFileInfo sourceInfo(m_sourceFile); + if (!sourceInfo.exists()) + error("Source QML file must be specified and exist."); + + if (m_argParser.isSet("outfile")) + m_outFile = m_argParser.value("outfile"); + else + m_outFile = m_sourceFile + ".png"; + + if (m_requestedSize.width() <= 0) + m_requestedSize.setWidth(150); + if (m_requestedSize.height() <= 0) + m_requestedSize.setHeight(150); + + if (m_verbose) { + info(QString("Import path = %1").arg(m_importPaths.join(";"))); + info(QString("Requested size = %1 x %2").arg(m_requestedSize.width()) + .arg(m_requestedSize.height())); + info(QString("Source file = %1").arg(m_sourceFile)); + info(QString("Output file = %1").arg(m_outFile)); + } + + if (setupRenderer()) { + render(); + asyncQuit(0); + } +} + +bool QmlRenderer::setupRenderer() +{ + info("Setting up renderer."); + + QQuickDesignerSupport::activateDesignerMode(); + + QQmlEngine *engine = new QQmlEngine; + + for (const QString &path : std::as_const(m_importPaths)) + engine->addImportPath(path); + + m_renderControl = new QQuickRenderControl; + m_window = new QQuickWindow(m_renderControl); + m_window->setDefaultAlphaBuffer(true); + m_window->setColor(Qt::transparent); + m_renderControl->initialize(); + + QQmlComponent component(engine); + component.loadUrl(QUrl::fromLocalFile(m_sourceFile)); + QObject *renderObj = component.create(); + + if (renderObj) { +#ifdef QUICK3D_MODULE + QQuickItem *contentItem3D = nullptr; + if (qobject_cast(renderObj)) { + auto helper = new QmlDesigner::Internal::GeneralHelper(); + engine->rootContext()->setContextProperty("_generalHelper", helper); + + QQmlComponent component(engine); + component.loadUrl(QUrl("qrc:/qtquickplugin/mockfiles/qt6/ModelNode3DImageView.qml")); + m_containerItem = qobject_cast(component.create()); + if (!m_containerItem) { + error("Failed to create 3D container view."); + return false; + } + m_containerItem->setParentItem(m_window->contentItem()); + + contentItem3D = QQmlProperty::read(m_containerItem, "contentItem").value(); + if (qobject_cast(renderObj)) { + QMetaObject::invokeMethod( + m_containerItem, "createViewForNode", + Q_ARG(QVariant, QVariant::fromValue(renderObj))); + m_fit3D = true; + } else { + m_fit3D = qobject_cast(renderObj); + QMetaObject::invokeMethod( + m_containerItem, "createViewForObject", + Q_ARG(QVariant, QVariant::fromValue(renderObj)), + Q_ARG(QVariant, ""), Q_ARG(QVariant, ""), Q_ARG(QVariant, "")); + } + + m_is3D = true; + m_renderSize = m_requestedSize; + contentItem3D->setSize(m_requestedSize); + } else +#endif // QUICK3D_MODULE + if (auto renderItem = qobject_cast(renderObj)) { + m_renderSize = renderItem->size().toSize(); + if (m_renderSize.width() <= 0) + m_renderSize.setWidth(m_requestedSize.width()); + if (m_renderSize.height() <= 0) + m_renderSize.setHeight(m_requestedSize.height()); + renderItem->setSize(m_renderSize); + renderItem->setParentItem(m_window->contentItem()); + // When rendering 2D scenes, we just render the given QML without extra container + m_containerItem = renderItem; + } else if (auto renderWindow = qobject_cast(renderObj)) { + // Hack to render Window items: reparent window content to m_window->contentItem() + m_renderSize = renderWindow->size(); + renderWindow->setVisible(false); + m_containerItem = m_window->contentItem(); + const QList childItems = renderWindow->contentItem()->childItems(); + for (QQuickItem *item : childItems) + item->setParentItem(m_window->contentItem()); + } + + if ((m_containerItem) && (contentItem3D || !m_is3D)) { + m_window->setGeometry(0, 0, m_renderSize.width(), m_renderSize.height()); + m_window->contentItem()->setSize(m_renderSize); + m_containerItem->setSize(m_renderSize); + + if (!initRhi()) + return false; + } else { + error("Failed to initialize content."); + return false; + } + } else { + error("Failed to initialize content."); + return false; + } + + return true; +} + +bool QmlRenderer::initRhi() +{ + if (!m_rhi) { + QQuickRenderControlPrivate *rd = QQuickRenderControlPrivate::get(m_renderControl); + m_rhi = rd->rhi; + if (!m_rhi) { + error("Rhi is null."); + return false; + } + } + + m_texTarget = nullptr; + m_rpDesc = nullptr; + m_buffer = nullptr; + m_texture = nullptr; + + m_texture = m_rhi->newTexture(QRhiTexture::RGBA8, m_renderSize, 1, + QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource); + if (!m_texture->create()) { + error("QRhiTexture creation failed."); + return false; + } + + m_buffer = m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, m_renderSize, 1); + if (!m_buffer->create()) { + error("Depth/stencil buffer creation failed."); + return false; + } + + QRhiTextureRenderTargetDescription rtDesc {QRhiColorAttachment(m_texture)}; + rtDesc.setDepthStencilBuffer(m_buffer); + m_texTarget = m_rhi->newTextureRenderTarget(rtDesc); + m_rpDesc = m_texTarget->newCompatibleRenderPassDescriptor(); + m_texTarget->setRenderPassDescriptor(m_rpDesc); + if (!m_texTarget->create()) { + error("Texture render target creation failed."); + return false; + } + + // redirect Qt Quick rendering into our texture + m_window->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(m_texTarget)); + + return true; +} + +void QmlRenderer::render() +{ + info(QString("Rendering: %1").arg(m_sourceFile)); + + std::function updateNodesRecursive; + updateNodesRecursive = [&updateNodesRecursive](QQuickItem *item) { + const auto childItems = item->childItems(); + for (QQuickItem *childItem : childItems) + updateNodesRecursive(childItem); + if (item->flags() & QQuickItem::ItemHasContents) + item->update(); + }; + + QImage renderImage; + + // Need to render fitted 3D views twice, first render updates spatial node geometries + const int renderCount = m_fit3D ? 2 : 1; + for (int i = 0; i < renderCount; ++i) { + if (m_fit3D && i == 1) + QMetaObject::invokeMethod(m_containerItem, "fitToViewPort", Qt::DirectConnection); + + updateNodesRecursive(m_containerItem); + m_renderControl->polishItems(); + m_renderControl->beginFrame(); + m_renderControl->sync(); + m_renderControl->render(); + + if (m_fit3D && i == 0) + m_renderControl->endFrame(); + } + + bool readCompleted = false; + QRhiReadbackResult readResult; + readResult.completed = [&] { + readCompleted = true; + QImage wrapperImage(reinterpret_cast(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888_Premultiplied); + if (m_rhi->isYUpInFramebuffer()) + renderImage = wrapperImage.mirrored().scaled(m_requestedSize); + else + renderImage = wrapperImage.copy().scaled(m_requestedSize); + }; + QRhiResourceUpdateBatch *readbackBatch = m_rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture(m_texture, &readResult); + + QQuickRenderControlPrivate *rd = QQuickRenderControlPrivate::get(m_renderControl); + rd->cb->resourceUpdate(readbackBatch); + + m_renderControl->endFrame(); + + info(QString("Saving render result: %1").arg(m_outFile)); + + QFileInfo fi(m_outFile); + if (fi.suffix().isEmpty()) + renderImage.save(m_outFile, "PNG"); + else + renderImage.save(m_outFile); +} + +void QmlRenderer::info(const QString &msg) +{ + if (m_verbose) + qInfo().noquote() << "QmlRenderer -" << msg; +} + +void QmlRenderer::error(const QString &msg) +{ + qCritical().noquote() << "QmlRenderer -" << msg; + asyncQuit(1); +} + +void QmlRenderer::asyncQuit(int errorCode) +{ + QTimer::singleShot(0, qGuiApp, [errorCode]() { + exit(errorCode); + }); +} diff --git a/src/tools/qmlpuppet/qmlpuppet/renderer/qmlrenderer.h b/src/tools/qmlpuppet/qmlpuppet/renderer/qmlrenderer.h new file mode 100644 index 00000000000..2a0fe3642a5 --- /dev/null +++ b/src/tools/qmlpuppet/qmlpuppet/renderer/qmlrenderer.h @@ -0,0 +1,54 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../qmlbase.h" + +QT_BEGIN_NAMESPACE +class QQuickItem; +class QQuickRenderControl; +class QQuickWindow; +class QRhi; +class QRhiRenderBuffer; +class QRhiRenderPassDescriptor; +class QRhiTexture; +class QRhiTextureRenderTarget; +QT_END_NAMESPACE + +class QmlRenderer : public QmlBase +{ + using QmlBase::QmlBase; + +private: + void initCoreApp() override; + void populateParser() override; + void initQmlRunner() override; + + bool setupRenderer(); + bool initRhi(); + void render(); + + void info(const QString &msg); + void error(const QString &msg); + void asyncQuit(int errorCode); + + QStringList m_importPaths; + QSize m_requestedSize; + QSize m_renderSize; + QString m_sourceFile; + QString m_outFile; + bool m_verbose = false; + bool m_is3D = false; + bool m_fit3D = false; + + QQuickWindow *m_window = nullptr; + QQuickItem *m_containerItem = nullptr; + + QQuickRenderControl *m_renderControl = nullptr; + QRhi *m_rhi = nullptr; + QRhiTexture *m_texture = nullptr; + QRhiRenderBuffer *m_buffer = nullptr; + QRhiTextureRenderTarget *m_texTarget = nullptr; + QRhiRenderPassDescriptor *m_rpDesc = nullptr; +};