Revision control
Copy as Markdown
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QQuick3DObject>
#include <QVector3D>
#include <QCommandLineParser>
#include <QQuickWindow>
#include <private/qquick3dxrview_p.h>
#include <private/qquick3dsceneenvironment_p.h>
#include <rhi/qrhi.h>
#include <QtCore/qloggingcategory.h>
#ifdef Q_OS_WIN
# include <fcntl.h>
# include <io.h>
#endif // Q_OS_WIN
// A valid screen grab requires the scene to not change
// for SCENE_STABLE_TIME ms
#define SCENE_STABLE_TIME 200
static int getLancelotSceneTimeout()
{
static int sceneTimeout = qEnvironmentVariableIntValue("LANCELOT_SCENE_TIMEOUT");
if (!sceneTimeout)
sceneTimeout = 6000; // Default value in ms
return sceneTimeout;
}
static bool writeOutGrabbedFrame(QImage lastGrab, QSize outputSize, const QString &ofile)
{
if (outputSize.isValid())
lastGrab = lastGrab.scaled(outputSize, Qt::KeepAspectRatio);
if (ofile == "-") { // Write to stdout
QFile of;
#ifdef Q_OS_WIN
// Make sure write to stdout doesn't do LF->CRLF
_setmode(_fileno(stdout), _O_BINARY);
#endif // Q_OS_WIN
if (!of.open(1, QIODevice::WriteOnly) || !lastGrab.save(&of, "ppm"))
qWarning() << "Error: failed to write grabbed image to stdout.";
} else {
if (!lastGrab.save(ofile))
qWarning() << "Error: failed to store grabbed image to" << ofile;
}
return true;
}
enum class MultiviewOutputStrategy {
Single,
Left,
Right,
Separate,
Merged
};
template <MultiviewOutputStrategy Strategy>
static QString generateOfName(int frame)
{
if constexpr (Strategy == MultiviewOutputStrategy::Left)
return QString::asprintf("frame_left_%d.png", frame);
if constexpr (Strategy == MultiviewOutputStrategy::Right)
return QString::asprintf("frame_right_%d.png", frame);
if constexpr (Strategy == MultiviewOutputStrategy::Merged)
return QString::asprintf("frame_multiview_%d.png", frame);
return QString::asprintf("frame_%d.png", frame);
}
template <MultiviewOutputStrategy Strategy>
static bool writeOutMultiview(const QImage &left, const QImage &right, QSize outputSize, int frame, QString ofile)
{
// Assumes the images are of the same size
if (!outputSize.isValid())
outputSize = left.size();
if (!outputSize.isValid())
outputSize = right.size();
if constexpr (Strategy != MultiviewOutputStrategy::Separate) {
if (ofile.isEmpty())
ofile = generateOfName<Strategy>(frame);
}
if constexpr (Strategy == MultiviewOutputStrategy::Left) {
Q_UNUSED(right);
return writeOutGrabbedFrame(left, outputSize, ofile);
}
if constexpr (Strategy == MultiviewOutputStrategy::Right) {
Q_UNUSED(left);
return writeOutGrabbedFrame(right, outputSize, ofile);
}
if constexpr (Strategy == MultiviewOutputStrategy::Separate) {
if (ofile.size() == 1) {
qWarning() << "Error: separate output strategy requires two output files";
return false;
}
QString ofileLeft;
QString ofileRight;
if (ofile.isEmpty()) {
ofileLeft = generateOfName<MultiviewOutputStrategy::Left>(frame);
ofileRight = generateOfName<MultiviewOutputStrategy::Right>(frame);
} else {
ofileLeft = QStringLiteral("left_") + ofile;
ofileRight = QStringLiteral("right_") + ofile;
}
if (!writeOutGrabbedFrame(left, outputSize, ofileLeft))
return false;
return writeOutGrabbedFrame(right, outputSize, ofileRight);
}
if constexpr (Strategy == MultiviewOutputStrategy::Merged) {
QImage mergedImage(left.width() + right.width(), qMax(left.height(), right.height()), QImage::Format_ARGB32);
QPainter painter(&mergedImage);
painter.drawImage(0, 0, left);
painter.drawImage(left.width(), 0, right);
painter.end();
return writeOutGrabbedFrame(mergedImage, outputSize, ofile);
}
}
int main(int argc, char *argv[])
{
QHashSeed::setDeterministicGlobalSeed();
QCoreApplication::setAttribute(Qt::AA_DisableShaderDiskCache);
QGuiApplication app(argc, argv);
QUnifiedTimer::instance()->setConsistentTiming(true);
QElapsedTimer elapsedTimer;
QCommandLineParser cmdLineParser;
// NOTE: Keep the same behavior as the non-XR scenegrabber which uses single dash word options.
cmdLineParser.setSingleDashWordOptionMode(QCommandLineParser::SingleDashWordOptionMode::ParseAsLongOptions);
const QString appDesc = QString::asprintf("qmlxr %s", qVersion());
cmdLineParser.setApplicationDescription(appDesc);
app.setApplicationVersion(QStringLiteral(QT_VERSION_STR));
cmdLineParser.addHelpOption();
cmdLineParser.addPositionalArgument(QStringLiteral("file"),
QStringLiteral(".qml file with a Node root"),
QStringLiteral("file"));
QCommandLineOption disableMultiviewOption("disable-multiview", QStringLiteral("Disable multiview rendering (Enabled by default if supported)"));
cmdLineParser.addOption(disableMultiviewOption);
QCommandLineOption multiviewStrategyOption("output-strategy", QStringLiteral("Output <strategy> for captured multiview frames are outputed.\n <strategy> should be one of: left, right, separate, or merged (default)"),
QStringLiteral("strategy"),
QStringLiteral("merged"));
cmdLineParser.addOption(multiviewStrategyOption);
QCommandLineOption outputSizeOption({ "s", "size" }, QStringLiteral("Output size for the grabbed frame"), QStringLiteral("widthxheight"));
cmdLineParser.addOption(outputSizeOption);
QCommandLineOption shaderDebugOption("print-shaders", QStringLiteral("Print generated material shaders"));
cmdLineParser.addOption(shaderDebugOption);
QCommandLineOption captureOption({ "c", "capture" }, QStringLiteral("Take RenderDoc frame captures (qtbase must be built with RenderDoc support)"));
cmdLineParser.addOption(captureOption);
QCommandLineOption viewonlyOption({ "v", "viewonly"}, QStringLiteral("Only view the scene, do not grab frames"));
cmdLineParser.addOption(viewonlyOption);
QCommandLineOption outputOption({ "o", "output" }, QStringLiteral("Output file for the grabbed frame"), QStringLiteral("file"));
cmdLineParser.addOption(outputOption);
QCommandLineOption exitOption({ "x", "exit-after" }, QStringLiteral("Exit after <num_frames> frames"), QStringLiteral("num_frames"), QStringLiteral("1"));
cmdLineParser.addOption(exitOption);
QCommandLineOption debugOption({ "l", "validate" }, QStringLiteral("Enable D3D12/Vulkan/OpenXR debug or validation layer, if available. Also enables QRhi leak checking."));
cmdLineParser.addOption(debugOption);
QCommandLineOption msaaOption({ "a", "msaa" }, QStringLiteral("Request MSAA with <num_samples> samples. <num_samples> is 2 or 4."), QStringLiteral("num_samples"));
cmdLineParser.addOption(msaaOption);
QCommandLineOption showControllersOptions("show-controllers", QStringLiteral("Show controllers in the scene"));
cmdLineParser.addOption(showControllersOptions);
QCommandLineOption showRuntimeDialogOption("show-runtime-dialog", QStringLiteral("Show runtime dialog"));
cmdLineParser.addOption(showRuntimeDialogOption);
QCommandLineOption submitDepthOption({ "e", "submit-depth" }, QStringLiteral("Forces submitting the depth buffer (XR_KHR_composition_layer_depth), if supported."));
cmdLineParser.addOption(submitDepthOption);
QCommandLineOption nullOption("backend-null", QStringLiteral("Use QRhi backend Null"));
cmdLineParser.addOption(nullOption);
QCommandLineOption glOption("backend-opengl", QStringLiteral("Use QRhi backend OpenGL (ES)"));
cmdLineParser.addOption(glOption);
QCommandLineOption vkOption("backend-vulkan", QStringLiteral("Use QRhi backend Vulkan [default on Android]"));
cmdLineParser.addOption(vkOption);
QCommandLineOption d3d11Option("backend-d3d11", QStringLiteral("Use QRhi backend Direct3D 11"));
cmdLineParser.addOption(d3d11Option);
QCommandLineOption d3d12Option("backend-d3d12", QStringLiteral("Use QRhi backend Direct3D 12 [default on Windows]"));
cmdLineParser.addOption(d3d12Option);
QCommandLineOption mtlOption("backend-metal", QStringLiteral("Use QRhi backend Metal"));
cmdLineParser.addOption(mtlOption);
QCommandLineOption devOpt("q", QStringLiteral("Enable developer settings and logging"));
cmdLineParser.addOption(devOpt);
cmdLineParser.process(app);
MultiviewOutputStrategy multiviewStrategy = MultiviewOutputStrategy::Merged;
if (cmdLineParser.isSet(multiviewStrategyOption)) {
const QString strategy = cmdLineParser.value(multiviewStrategyOption);
if (strategy == QStringLiteral("left"))
multiviewStrategy = MultiviewOutputStrategy::Left;
else if (strategy == QStringLiteral("right"))
multiviewStrategy = MultiviewOutputStrategy::Right;
else if (strategy == QStringLiteral("separate"))
multiviewStrategy = MultiviewOutputStrategy::Separate;
else if (strategy == QStringLiteral("merged"))
multiviewStrategy = MultiviewOutputStrategy::Merged;
else {
qWarning() << "Error: unknown output strategy" << strategy;
return 1;
}
}
QString ofile;
if (cmdLineParser.isSet(outputOption)) {
ofile = cmdLineParser.value(outputOption);
if (ofile.isEmpty()) {
qWarning() << "Error: output file is required in combination with the output option";
return 1;
}
}
QSize outputSize;
if (cmdLineParser.isSet(outputSizeOption)) {
const QStringList size = cmdLineParser.value(outputSizeOption).split('x');
if (size.size() != 2) {
qWarning() << "Error: output size must be in the format widthxheight";
return 1;
}
outputSize = QSize(size[0].toInt(), size[1].toInt());
}
const auto positionalArguments = cmdLineParser.positionalArguments();
if (positionalArguments.isEmpty()) {
cmdLineParser.showHelp();
return 0;
}
const QString filename = positionalArguments.first();
const bool devOptSet = cmdLineParser.isSet(devOpt);
if (devOptSet) {
qputenv("QSG_INFO", "1");
qputenv("QSG_RHI_PROFILE", "1");
QLoggingCategory::setFilterRules(QStringLiteral("qt.quick3d.xr=true"));
}
#ifdef Q_OS_WIN
QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12);
#endif
#ifdef Q_OS_ANDROID
QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);
#endif
if (cmdLineParser.isSet(nullOption))
QQuickWindow::setGraphicsApi(QSGRendererInterface::Null);
else if (cmdLineParser.isSet(glOption))
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
else if (cmdLineParser.isSet(vkOption))
QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);
else if (cmdLineParser.isSet(d3d11Option))
QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11);
else if (cmdLineParser.isSet(d3d12Option))
QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12);
else if (cmdLineParser.isSet(mtlOption))
QQuickWindow::setGraphicsApi(QSGRendererInterface::Metal);
qputenv("QT_QUICK_CONTROLS_STYLE", "Basic");
if (cmdLineParser.isSet(disableMultiviewOption))
qputenv("QT_QUICK3D_XR_DISABLE_MULTIVIEW", "1");
if (cmdLineParser.isSet(shaderDebugOption))
qputenv("QT_RHI_SHADER_DEBUG", "1");
if (cmdLineParser.isSet(captureOption)) {
qputenv("QT_QUICK3D_SHADER_DEBUG_INFO", "1");
qputenv("QT_QUICK3D_XR_FRAME_CAPTURE", "1");
}
if (cmdLineParser.isSet(debugOption)) {
qputenv("QSG_RHI_DEBUG_LAYER", "1");
qputenv("QT_RHI_LEAK_CHECK", "1");
}
if (cmdLineParser.isSet(submitDepthOption))
qputenv("QT_QUICK3D_XR_SUBMIT_DEPTH", "1");
const bool viewOnly = cmdLineParser.isSet(viewonlyOption);
QCoreApplication::setApplicationName("Qt XR Runner");
QCoreApplication::setOrganizationName("The Qt Company");
QCoreApplication::setApplicationVersion(QT_VERSION_STR);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/QtQuick3D/Lancelot/Xr/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url, viewOnly](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl) {
qWarning() << "Error: failed to load" << url;
QCoreApplication::exit(-1);
} else if (!viewOnly) {
QTimer::singleShot(getLancelotSceneTimeout(), qApp, [] {
qWarning() << "Error: timed out waiting for scene to stabilize!";
QCoreApplication::exit(3);
});
}
}, Qt::QueuedConnection);
engine.load(url);
const QList<QObject *> rootObjects = engine.rootObjects();
std::unique_ptr<QQmlComponent> component;
int currentSingleViewEye = 0;
int currentFrame = 1;
int exitAfter = 1;
if (cmdLineParser.isSet(exitOption))
exitAfter = qMax(0, cmdLineParser.value(exitOption).toInt());
if (!rootObjects.isEmpty()) {
QQuick3DXrView *xrView = qobject_cast<QQuick3DXrView *>(rootObjects.first());
if (cmdLineParser.isSet(showControllersOptions))
xrView->setProperty("showControllers", true);
if (cmdLineParser.isSet(showRuntimeDialogOption))
xrView->setProperty("showRuntimeDialog", true);
component.reset(new QQmlComponent(&engine, QUrl::fromLocalFile(filename)));
if (component->isError()) {
for (const QQmlError &error : component->errors()) {
qWarning() << error.url() << error.line() << error;
return 1;
}
}
QObject *subRoot = component->create();
if (component->isError()) {
for (const QQmlError &error : component->errors()) {
qWarning() << error.url() << error.line() << error;
return 1;
}
}
if (QQuick3DObject *obj = qobject_cast<QQuick3DObject *>(subRoot)) {
obj->setParentItem(xrView);
QObject *origin = xrView->findChild<QObject *>("xrorigin");
QVariant posProp = obj->property("qmlxr_originPosition");
if (!posProp.isNull()) {
QVector3D camPos = posProp.value<QVector3D>();
if (devOptSet)
qDebug() << "Setting origin position to" << camPos;
origin->setProperty("position", camPos);
}
QVariant rotProp = obj->property("qmlxr_originRotation");
if (!rotProp.isNull()) {
QVector3D camRot = rotProp.value<QVector3D>();
if (devOptSet)
qDebug() << "Setting origin rotation to" << camRot;
origin->setProperty("eulerRotation", camRot);
}
QVariant envProp = obj->property("qmlxr_environment");
if (!envProp.isNull()) {
QQuick3DSceneEnvironment *env = envProp.value<QQuick3DSceneEnvironment *>();
if (devOptSet)
qDebug() << "Setting SceneEnvironment to" << env;
xrView->setEnvironment(env);
}
} else {
qWarning() << subRoot << "is not a QQuick3DObject";
return 1;
}
if (cmdLineParser.isSet(msaaOption)) {
const int samples = qBound(1, cmdLineParser.value(msaaOption).toInt(), 8);
if (samples > 1) {
QQuick3DSceneEnvironment *sceneEnv = xrView->environment();
if (devOptSet)
qDebug() << "Requesting MSAA with sample count" << samples << "on" << sceneEnv;
sceneEnv->setAntialiasingMode(QQuick3DSceneEnvironment::MSAA);
if (samples == 2)
sceneEnv->setAntialiasingQuality(QQuick3DSceneEnvironment::Medium);
else if (samples == 4)
sceneEnv->setAntialiasingQuality(QQuick3DSceneEnvironment::High);
else
sceneEnv->setAntialiasingQuality(QQuick3DSceneEnvironment::VeryHigh);
}
}
const bool grab = !cmdLineParser.isSet(viewonlyOption) && !cmdLineParser.isSet(captureOption);
if (grab) {
QObject::connect(xrView, &QQuick3DXrView::frameReady, xrView, [&]() {
if (!elapsedTimer.isValid())
elapsedTimer.start();
if (elapsedTimer.elapsed() < SCENE_STABLE_TIME)
return;
QRhiTexture *colorBuffer = nullptr;
auto *view = QQuick3DXrViewPrivate::getView3d(xrView);
if (view && view->window()) {
QQuickWindow *quickWindow = view->window();
QRhiRenderTarget *rt = QQuickWindowPrivate::get(quickWindow)->activeCustomRhiRenderTarget();
if (rt && rt->resourceType() == QRhiResource::TextureRenderTarget)
colorBuffer = static_cast<QRhiTextureRenderTarget *>(rt)->description().colorAttachmentAt(0)->texture();
}
if (!colorBuffer)
return;
const int viewCount = qMax(1, colorBuffer->arraySize());
if (!cmdLineParser.isSet(viewonlyOption)) {
QRhi *rhi = colorBuffer->rhi();
QRhiCommandBuffer *cb = nullptr;
if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
return;
QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch();
QVarLengthArray<QImage, 2> images(viewCount);
QVarLengthArray<QRhiReadbackResult, 2> results(viewCount);
QImage::Format imageFormat = QImage::Format_ARGB32;
for (int layer = 0; layer < viewCount; ++layer) {
results[layer].completed = [&results, &imageFormat, &images, rhi, layer] {
const uchar *p = reinterpret_cast<const uchar *>(results[layer].data.constData());
imageFormat = results[layer].format == QRhiTexture::BGRA8 ? QImage::Format_ARGB32 : QImage::Format_RGBA8888;
const QImage img(p, results[layer].pixelSize.width(), results[layer].pixelSize.height(), imageFormat);
// the order of the callbacks is undefined, hence must rely on the layer index
if (rhi->isYUpInFramebuffer())
images[layer] = img.flipped();
else
images[layer] = img.copy();
};
QRhiReadbackDescription readbackDesc(colorBuffer);
readbackDesc.setLayer(layer);
resourceUpdates->readBackTexture(readbackDesc, &results[layer]);
}
cb->resourceUpdate(resourceUpdates);
rhi->endOffscreenFrame();
if (images.count() >= 2) {
// multiview, this is the complete frame (2D texture array)
const auto &left = images.at(0);
const auto &right = images.at(1);
switch (multiviewStrategy) {
case MultiviewOutputStrategy::Single:
Q_FALLTHROUGH();
case MultiviewOutputStrategy::Left:
writeOutMultiview<MultiviewOutputStrategy::Left>(left, {}, outputSize, currentFrame, ofile);
break;
case MultiviewOutputStrategy::Right:
writeOutMultiview<MultiviewOutputStrategy::Right>({}, right, outputSize, currentFrame, ofile);
break;
case MultiviewOutputStrategy::Separate:
writeOutMultiview<MultiviewOutputStrategy::Separate>(left, right, outputSize, currentFrame, ofile);
break;
case MultiviewOutputStrategy::Merged:
writeOutMultiview<MultiviewOutputStrategy::Merged>(left, right, outputSize, currentFrame, ofile);
break;
}
} else if (images.count() == 1) {
// this is either the left or right eye content (2D texture)
if (ofile.isEmpty())
ofile = generateOfName<MultiviewOutputStrategy::Single>(currentFrame);
writeOutGrabbedFrame(images[0], outputSize, ofile);
}
}
if (viewCount >= 2) {
currentFrame += 1;
} else {
currentSingleViewEye = (currentSingleViewEye + 1) % 2;
if (currentSingleViewEye == 0)
currentFrame += 1;
}
if (exitAfter > 0 && currentFrame > exitAfter)
QCoreApplication::exit();
}, Qt::DirectConnection);
}
}
return app.exec();
}