diff options
Diffstat (limited to 'tests/manual/rhi/rhiwidgetproto')
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/CMakeLists.txt | 33 | ||||
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/examplewidget.cpp | 182 | ||||
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/examplewidget.h | 66 | ||||
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/main.cpp | 97 | ||||
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp | 538 | ||||
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/rhiwidget.h | 56 | ||||
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h | 38 |
7 files changed, 1010 insertions, 0 deletions
diff --git a/tests/manual/rhi/rhiwidgetproto/CMakeLists.txt b/tests/manual/rhi/rhiwidgetproto/CMakeLists.txt new file mode 100644 index 0000000000..5b62ef557d --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/CMakeLists.txt @@ -0,0 +1,33 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_manual_test(rhiwidgetproto + GUI + SOURCES + examplewidget.cpp examplewidget.h + rhiwidget.cpp rhiwidget.h rhiwidget_p.h + main.cpp + LIBRARIES + Qt::Gui + Qt::GuiPrivate + Qt::Widgets + Qt::WidgetsPrivate +) + +set_source_files_properties("../shared/texture.vert.qsb" + PROPERTIES QT_RESOURCE_ALIAS "texture.vert.qsb" +) +set_source_files_properties("../shared/texture.frag.qsb" + PROPERTIES QT_RESOURCE_ALIAS "texture.frag.qsb" +) +set(rhiwidgetproto_resource_files + "../shared/texture.vert.qsb" + "../shared/texture.frag.qsb" +) + +qt_internal_add_resource(rhiwidgetproto "rhiwidgetproto" + PREFIX + "/" + FILES + ${rhiwidgetproto_resource_files} +) diff --git a/tests/manual/rhi/rhiwidgetproto/examplewidget.cpp b/tests/manual/rhi/rhiwidgetproto/examplewidget.cpp new file mode 100644 index 0000000000..6a6fd5b326 --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/examplewidget.cpp @@ -0,0 +1,182 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "examplewidget.h" +#include "../shared/cube.h" +#include <QFile> +#include <QPainter> + +static const QSize CUBE_TEX_SIZE(512, 512); + +ExampleRhiWidget::ExampleRhiWidget(QWidget *parent, Qt::WindowFlags f) + : QRhiWidget(parent, f) +{ + setDebugLayer(true); +} + +void ExampleRhiWidget::initialize(QRhi *rhi, QRhiTexture *outputTexture) +{ + if (m_rhi != rhi) { + m_rt.reset(); + m_rp.reset(); + m_ds.reset(); + scene.vbuf.reset(); + } else if (m_output != outputTexture) { + m_rt.reset(); + m_rp.reset(); + } + + m_rhi = rhi; + m_output = outputTexture; + + if (!m_ds) { + m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, m_output->pixelSize())); + m_ds->create(); + } else if (m_ds->pixelSize() != m_output->pixelSize()) { + m_ds->setPixelSize(m_output->pixelSize()); + m_ds->create(); + } + + if (!m_rt) { + m_rt.reset(m_rhi->newTextureRenderTarget({ { m_output }, m_ds.data() })); + m_rp.reset(m_rt->newCompatibleRenderPassDescriptor()); + m_rt->setRenderPassDescriptor(m_rp.data()); + m_rt->create(); + } + + if (!scene.vbuf) { + initScene(); + updateCubeTexture(); + } + + const QSize outputSize = m_output->pixelSize(); + scene.mvp = m_rhi->clipSpaceCorrMatrix(); + scene.mvp.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f); + scene.mvp.translate(0, 0, -4); + updateMvp(); +} + +void ExampleRhiWidget::updateMvp() +{ + QMatrix4x4 mvp = scene.mvp * QMatrix4x4(QQuaternion::fromEulerAngles(QVector3D(30, itemData.cubeRotation, 0)).toRotationMatrix()); + if (!scene.resourceUpdates) + scene.resourceUpdates = m_rhi->nextResourceUpdateBatch(); + scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.data(), 0, 64, mvp.constData()); +} + +void ExampleRhiWidget::updateCubeTexture() +{ + QImage image(CUBE_TEX_SIZE, QImage::Format_RGBA8888); + const QRect r(QPoint(0, 0), CUBE_TEX_SIZE); + QPainter p(&image); + p.fillRect(r, QGradient::DeepBlue); + QFont font; + font.setPointSize(24); + p.setFont(font); + p.drawText(r, itemData.cubeText); + p.end(); + + if (!scene.resourceUpdates) + scene.resourceUpdates = m_rhi->nextResourceUpdateBatch(); + scene.resourceUpdates->uploadTexture(scene.cubeTex.data(), image); +} + +static QShader getShader(const QString &name) +{ + QFile f(name); + if (f.open(QIODevice::ReadOnly)) + return QShader::fromSerialized(f.readAll()); + + return QShader(); +} + +void ExampleRhiWidget::initScene() +{ + scene.vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube))); + scene.vbuf->create(); + + scene.resourceUpdates = m_rhi->nextResourceUpdateBatch(); + scene.resourceUpdates->uploadStaticBuffer(scene.vbuf.data(), cube); + + scene.ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68)); + scene.ubuf->create(); + + const qint32 flip = 0; + scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.data(), 64, 4, &flip); + + scene.cubeTex.reset(m_rhi->newTexture(QRhiTexture::RGBA8, CUBE_TEX_SIZE)); + scene.cubeTex->create(); + + scene.sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, + QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge)); + scene.sampler->create(); + + scene.srb.reset(m_rhi->newShaderResourceBindings()); + scene.srb->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, scene.ubuf.data()), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, scene.cubeTex.data(), scene.sampler.data()) + }); + scene.srb->create(); + + scene.ps.reset(m_rhi->newGraphicsPipeline()); + scene.ps->setDepthTest(true); + scene.ps->setDepthWrite(true); + scene.ps->setDepthOp(QRhiGraphicsPipeline::Less); + scene.ps->setCullMode(QRhiGraphicsPipeline::Back); + scene.ps->setFrontFace(QRhiGraphicsPipeline::CCW); + QShader vs = getShader(QLatin1String(":/texture.vert.qsb")); + Q_ASSERT(vs.isValid()); + QShader fs = getShader(QLatin1String(":/texture.frag.qsb")); + Q_ASSERT(fs.isValid()); + scene.ps->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 3 * sizeof(float) }, + { 2 * sizeof(float) } + }); + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, + { 1, 1, QRhiVertexInputAttribute::Float2, 0 } + }); + scene.ps->setVertexInputLayout(inputLayout); + scene.ps->setShaderResourceBindings(scene.srb.data()); + scene.ps->setRenderPassDescriptor(m_rp.data()); + scene.ps->create(); +} + +void ExampleRhiWidget::render(QRhiCommandBuffer *cb) +{ + if (itemData.cubeRotationDirty) { + itemData.cubeRotationDirty = false; + updateMvp(); + } + + if (itemData.cubeTextDirty) { + itemData.cubeTextDirty = false; + updateCubeTexture(); + } + + QRhiResourceUpdateBatch *rub = scene.resourceUpdates; + if (rub) + scene.resourceUpdates = nullptr; + + const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f); + + cb->beginPass(m_rt.data(), clearColor, { 1.0f, 0 }, rub); + + cb->setGraphicsPipeline(scene.ps.data()); + const QSize outputSize = m_output->pixelSize(); + cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height())); + cb->setShaderResources(); + const QRhiCommandBuffer::VertexInput vbufBindings[] = { + { scene.vbuf.data(), 0 }, + { scene.vbuf.data(), quint32(36 * 3 * sizeof(float)) } + }; + cb->setVertexInput(0, 2, vbufBindings); + cb->draw(36); + + cb->endPass(); +} diff --git a/tests/manual/rhi/rhiwidgetproto/examplewidget.h b/tests/manual/rhi/rhiwidgetproto/examplewidget.h new file mode 100644 index 0000000000..a17fe7bce1 --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/examplewidget.h @@ -0,0 +1,66 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef EXAMPLEWIDGET_H +#define EXAMPLEWIDGET_H + +#include "rhiwidget.h" +#include <rhi/qrhi.h> + +class ExampleRhiWidget : public QRhiWidget +{ +public: + ExampleRhiWidget(QWidget *parent = nullptr, Qt::WindowFlags f = {}); + + void initialize(QRhi *rhi, QRhiTexture *outputTexture) override; + void render(QRhiCommandBuffer *cb) override; + + void setCubeTextureText(const QString &s) + { + if (itemData.cubeText == s) + return; + itemData.cubeText = s; + itemData.cubeTextDirty = true; + update(); + } + + void setCubeRotation(float r) + { + if (itemData.cubeRotation == r) + return; + itemData.cubeRotation = r; + itemData.cubeRotationDirty = true; + update(); + } + +private: + QRhi *m_rhi = nullptr; + QRhiTexture *m_output = nullptr; + QScopedPointer<QRhiRenderBuffer> m_ds; + QScopedPointer<QRhiTextureRenderTarget> m_rt; + QScopedPointer<QRhiRenderPassDescriptor> m_rp; + + struct { + QRhiResourceUpdateBatch *resourceUpdates = nullptr; + QScopedPointer<QRhiBuffer> vbuf; + QScopedPointer<QRhiBuffer> ubuf; + QScopedPointer<QRhiShaderResourceBindings> srb; + QScopedPointer<QRhiGraphicsPipeline> ps; + QScopedPointer<QRhiSampler> sampler; + QScopedPointer<QRhiTexture> cubeTex; + QMatrix4x4 mvp; + } scene; + + void initScene(); + void updateMvp(); + void updateCubeTexture(); + + struct { + QString cubeText; + bool cubeTextDirty = false; + float cubeRotation = 0.0f; + bool cubeRotationDirty = false; + } itemData; +}; + +#endif diff --git a/tests/manual/rhi/rhiwidgetproto/main.cpp b/tests/manual/rhi/rhiwidgetproto/main.cpp new file mode 100644 index 0000000000..a3b1741855 --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/main.cpp @@ -0,0 +1,97 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QApplication> +#include <QVBoxLayout> +#include <QHBoxLayout> +#include <QSlider> +#include <QLineEdit> +#include <QPushButton> +#include <QLabel> +#include <QCheckBox> +#include <QFileDialog> +#include "examplewidget.h" + +static const bool TEST_OFFSCREEN_GRAB = false; + +int main(int argc, char **argv) +{ + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + + QVBoxLayout *layout = new QVBoxLayout; + + QLineEdit *edit = new QLineEdit(QLatin1String("Text on cube")); + QSlider *slider = new QSlider(Qt::Horizontal); + ExampleRhiWidget *rw = new ExampleRhiWidget; + + QObject::connect(edit, &QLineEdit::textChanged, edit, [edit, rw] { + rw->setCubeTextureText(edit->text()); + }); + + slider->setMinimum(0); + slider->setMaximum(360); + QObject::connect(slider, &QSlider::valueChanged, slider, [slider, rw] { + rw->setCubeRotation(slider->value()); + }); + + QPushButton *btn = new QPushButton(QLatin1String("Grab to image")); + QObject::connect(btn, &QPushButton::clicked, btn, [rw] { + QImage image = rw->grabTexture(); + qDebug() << image; + if (!image.isNull()) { + QFileDialog fd(rw->parentWidget()); + fd.setAcceptMode(QFileDialog::AcceptSave); + fd.setDefaultSuffix("png"); + fd.selectFile("test.png"); + if (fd.exec() == QDialog::Accepted) + image.save(fd.selectedFiles().first()); + } + }); + QHBoxLayout *btnLayout = new QHBoxLayout; + btnLayout->addWidget(btn); + QCheckBox *cbExplicitSize = new QCheckBox(QLatin1String("Use explicit size")); + QObject::connect(cbExplicitSize, &QCheckBox::stateChanged, cbExplicitSize, [cbExplicitSize, rw] { + if (cbExplicitSize->isChecked()) + rw->setExplicitSize(QSize(128, 128)); + else + rw->setExplicitSize(QSize()); + }); + btnLayout->addWidget(cbExplicitSize); + QPushButton *btnMakeWindow = new QPushButton(QLatin1String("Make top-level window")); + QObject::connect(btnMakeWindow, &QPushButton::clicked, btnMakeWindow, [rw, btnMakeWindow, layout] { + if (rw->parentWidget()) { + rw->setParent(nullptr); + rw->setAttribute(Qt::WA_DeleteOnClose, true); + rw->show(); + btnMakeWindow->setText(QLatin1String("Make child widget")); + } else { + rw->setAttribute(Qt::WA_DeleteOnClose, false); + layout->addWidget(rw); + btnMakeWindow->setText(QLatin1String("Make top-level window")); + } + }); + btnLayout->addWidget(btnMakeWindow); + + layout->addWidget(edit); + QHBoxLayout *sliderLayout = new QHBoxLayout; + sliderLayout->addWidget(new QLabel(QLatin1String("Cube rotation"))); + sliderLayout->addWidget(slider); + layout->addLayout(sliderLayout); + layout->addLayout(btnLayout); + layout->addWidget(rw); + + rw->setCubeTextureText(edit->text()); + + if (TEST_OFFSCREEN_GRAB) { + rw->resize(320, 200); + rw->grabTexture().save("offscreen_grab.png"); + } + + QWidget w; + w.setLayout(layout); + w.resize(1280, 720); + w.show(); + + return app.exec(); +} diff --git a/tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp b/tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp new file mode 100644 index 0000000000..d535c655d0 --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp @@ -0,0 +1,538 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "rhiwidget_p.h" + +#include <private/qguiapplication_p.h> +#include <qpa/qplatformintegration.h> +#include <private/qwidgetrepaintmanager_p.h> + +/*! + \class QRhiWidget + \inmodule QtWidgets + \since 6.x + + \brief The QRhiWidget class is a widget for rendering 3D graphics via an + accelerated grapics API, such as Vulkan, Metal, or Direct 3D. + + QRhiWidget provides functionality for displaying 3D content rendered through + the QRhi APIs within a QWidget-based application. + + QRhiWidget is expected to be subclassed. To render into the 2D texture that + is implicitly created and managed by the QRhiWidget, subclasses should + reimplement the virtual functions initialize() and render(). + + The size of the texture will by default adapt to the size of the item. If a + fixed size is preferred, set an explicit size specified in pixels by + calling setExplicitSize(). + + The QRhi for the widget's top-level window is configured to use a platform + specific backend and graphics API by default: Metal on macOS and iOS, + Direct 3D 11 on Windows, OpenGL otherwise. Call setApi() to override this. + + \note A single widget window can only use one QRhi backend, and so graphics + API. If two QRhiWidget or QQuickWidget widgets in the window's widget + hierarchy request different APIs, only one of them will function correctly. + */ + +/*! + Constructs a widget which is a child of \a parent, with widget flags set to \a f. + */ +QRhiWidget::QRhiWidget(QWidget *parent, Qt::WindowFlags f) + : QWidget(*(new QRhiWidgetPrivate), parent, f) +{ + Q_D(QRhiWidget); + if (Q_UNLIKELY(!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::RhiBasedRendering))) + qWarning("QRhiWidget: QRhi is not supported on this platform."); + else + d->setRenderToTexture(); + + d->config.setEnabled(true); +#if defined(Q_OS_DARWIN) + d->config.setApi(QPlatformBackingStoreRhiConfig::Metal); +#elif defined(Q_OS_WIN) + d->config.setApi(QPlatformBackingStoreRhiConfig::D3D11); +#else + d->config.setApi(QPlatformBackingStoreRhiConfig::OpenGL); +#endif +} + +/*! + Destructor. + */ +QRhiWidget::~QRhiWidget() +{ + Q_D(QRhiWidget); + // rhi resources must be destroyed here, cannot be left to the private dtor + delete d->t; + d->offscreenRenderer.reset(); +} + +/*! + Handles resize events that are passed in the \a e event parameter. Calls + the virtual function initialize(). + + \note Avoid overriding this function in derived classes. If that is not + feasible, make sure that QRhiWidget's implementation is invoked too. + Otherwise the underlying texture object and related resources will not get + resized properly and will lead to incorrect rendering. + */ +void QRhiWidget::resizeEvent(QResizeEvent *e) +{ + Q_D(QRhiWidget); + + if (e->size().isEmpty()) { + d->noSize = true; + return; + } + d->noSize = false; + + d->sendPaintEvent(QRect(QPoint(0, 0), size())); +} + +/*! + Handles paint events. + + Calling QWidget::update() will lead to sending a paint event \a e, and thus + invoking this function. (NB this is asynchronous and will happen at some + point after returning from update()). This function will then, after some + preparation, call the virtual render() to update the contents of the + QRhiWidget's associated texture. The widget's top-level window will then + composite the texture with the rest of the window. + */ +void QRhiWidget::paintEvent(QPaintEvent *) +{ + Q_D(QRhiWidget); + if (!updatesEnabled() || d->noSize) + return; + + d->ensureRhi(); + if (!d->rhi) { + qWarning("QRhiWidget: No QRhi"); + return; + } + + const QSize prevSize = d->t ? d->t->pixelSize() : QSize(); + d->ensureTexture(); + if (!d->t) + return; + if (d->t->pixelSize() != prevSize) + initialize(d->rhi, d->t); + + QRhiCommandBuffer *cb = nullptr; + d->rhi->beginOffscreenFrame(&cb); + render(cb); + d->rhi->endOffscreenFrame(); +} + +/*! + \reimp +*/ +bool QRhiWidget::event(QEvent *e) +{ + Q_D(QRhiWidget); + switch (e->type()) { + case QEvent::WindowChangeInternal: + // the QRhi will almost certainly change, prevent texture() from + // returning the existing QRhiTexture in the meantime + d->textureInvalid = true; + break; + case QEvent::Show: + if (isVisible()) + d->sendPaintEvent(QRect(QPoint(0, 0), size())); + break; + default: + break; + } + return QWidget::event(e); +} + +QPlatformBackingStoreRhiConfig QRhiWidgetPrivate::rhiConfig() const +{ + return config; +} + +void QRhiWidgetPrivate::ensureRhi() +{ + Q_Q(QRhiWidget); + QRhi *currentRhi = QWidgetPrivate::rhi(); + if (currentRhi && currentRhi->backend() != QBackingStoreRhiSupport::apiToRhiBackend(config.api())) { + qWarning("The top-level window is already using another graphics API for composition, " + "'%s' is not compatible with this widget", + currentRhi->backendName()); + return; + } + + if (currentRhi && rhi && rhi != currentRhi) { + // the texture belongs to the old rhi, drop it, this will also lead to + // initialize() being called again + delete t; + t = nullptr; + // if previously we created our own but now get a QRhi from the + // top-level, then drop what we have and start using the top-level's + if (rhi == offscreenRenderer.rhi()) + offscreenRenderer.reset(); + } + + rhi = currentRhi; +} + +void QRhiWidgetPrivate::ensureTexture() +{ + Q_Q(QRhiWidget); + + QSize newSize = explicitSize; + if (newSize.isEmpty()) + newSize = q->size() * q->devicePixelRatio(); + + if (!t) { + if (!rhi->isTextureFormatSupported(format)) + qWarning("QRhiWidget: The requested texture format is not supported by the graphics API implementation"); + t = rhi->newTexture(format, newSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource); + if (!t->create()) { + qWarning("Failed to create backing texture for QRhiWidget"); + delete t; + t = nullptr; + return; + } + } + + if (t->pixelSize() != newSize) { + t->setPixelSize(newSize); + if (!t->create()) + qWarning("Failed to rebuild texture for QRhiWidget after resizing"); + } + + textureInvalid = false; +} + +/*! + \return the currently set graphics API (QRhi backend). + + \sa setApi() + */ +QRhiWidget::Api QRhiWidget::api() const +{ + Q_D(const QRhiWidget); + switch (d->config.api()) { + case QPlatformBackingStoreRhiConfig::OpenGL: + return OpenGL; + case QPlatformBackingStoreRhiConfig::Metal: + return Metal; + case QPlatformBackingStoreRhiConfig::Vulkan: + return Vulkan; + case QPlatformBackingStoreRhiConfig::D3D11: + return D3D11; + default: + return Null; + } +} + +/*! + Sets the graphics API and QRhi backend to use to \a api. + + The default value depends on the platform: Metal on macOS and iOS, Direct + 3D 11 on Windows, OpenGL otherwise. + + \note This function must be called early enough, before the widget is added + to a widget hierarchy and displayed on screen. For example, aim to call the + function for the subclass constructor. If called too late, the function + will have no effect. + + The \a api can only be set once for the widget and its top-level window, + once it is done and takes effect, the window can only use that API and QRhi + backend to render. Attempting to set another value, or to add another + QRhiWidget with a different \a api will not function as expected. + + \sa setTextureFormat(), setDebugLayer(), api() + */ +void QRhiWidget::setApi(Api api) +{ + Q_D(QRhiWidget); + switch (api) { + case OpenGL: + d->config.setApi(QPlatformBackingStoreRhiConfig::OpenGL); + break; + case Metal: + d->config.setApi(QPlatformBackingStoreRhiConfig::Metal); + break; + case Vulkan: + d->config.setApi(QPlatformBackingStoreRhiConfig::Vulkan); + break; + case D3D11: + d->config.setApi(QPlatformBackingStoreRhiConfig::D3D11); + break; + default: + d->config.setApi(QPlatformBackingStoreRhiConfig::Null); + break; + } +} + +/*! + \return true if a debug or validation layer will be requested if applicable + to the graphics API in use. + + \sa setDebugLayer() + */ +bool QRhiWidget::isDebugLayerEnabled() const +{ + Q_D(const QRhiWidget); + return d->config.isDebugLayerEnabled(); +} + +/*! + Requests the debug or validation layer of the underlying graphics API + when \a enable is true. + + Applicable for Vulkan and Direct 3D. + + \note This function must be called early enough, before the widget is added + to a widget hierarchy and displayed on screen. For example, aim to call the + function for the subclass constructor. If called too late, the function + will have no effect. + + By default this is disabled. + + \sa setApi(), isDebugLayerEnabled() + */ +void QRhiWidget::setDebugLayer(bool enable) +{ + Q_D(QRhiWidget); + d->config.setDebugLayer(enable); +} + +/*! + \return the currently set texture format. + + The default value is QRhiTexture::RGBA8. + + \sa setTextureFormat() + */ +QRhiTexture::Format QRhiWidget::textureFormat() const +{ + Q_D(const QRhiWidget); + return d->format; +} + +/*! + Sets the associated texture's \a format. + + The default value is QRhiTexture::RGBA8. Only formats that are reported as + supported from QRhi::isTextureFormatSupported() should be specified, + rendering will not be functional otherwise. + + \note This function must be called early enough, before the widget is added + to a widget hierarchy and displayed on screen. For example, aim to call the + function for the subclass constructor. If called too late, the function + will have no effect. + + \sa setApi(), textureFormat() + */ +void QRhiWidget::setTextureFormat(QRhiTexture::Format format) +{ + Q_D(QRhiWidget); + d->format = format; +} + +/*! + \property QRhiWidget::explicitSize + + The fixed size (in pixels) of the QRhiWidget's associated texture. + + Only relevant when a fixed texture size is desired that does not depend on + the widget's size. + + By default the value is a null QSize. A null or empty QSize means that the + texture's size follows the QRhiWidget's size. (\c{texture size} = \c{widget + size} * \c{device pixel ratio}). + */ + +QSize QRhiWidget::explicitSize() const +{ + Q_D(const QRhiWidget); + return d->explicitSize; +} + +void QRhiWidget::setExplicitSize(const QSize &pixelSize) +{ + Q_D(QRhiWidget); + if (d->explicitSize != pixelSize) { + d->explicitSize = pixelSize; + emit explicitSizeChanged(pixelSize); + update(); + } +} + +/*! + Renders a new frame, reads the contents of the texture back, and returns it + as a QImage. + + When an error occurs, a null QImage is returned. + + \note This function only supports reading back QRhiTexture::RGBA8 textures + at the moment. For other formats, the implementer of render() should + implement their own readback logic as they see fit. + + The returned QImage will have a format of QImage::Format_RGBA8888. + QRhiWidget does not know the renderer's approach to blending and + composition, and therefore cannot know if the output has alpha + premultiplied. + + This function can also be called when the QRhiWidget is not added to a + widget hierarchy belonging to an on-screen top-level window. This allows + generating an image from a 3D rendering off-screen. + + \sa setTextureFormat() + */ +QImage QRhiWidget::grabTexture() +{ + Q_D(QRhiWidget); + if (d->noSize) + return QImage(); + + if (d->format != QRhiTexture::RGBA8) { + qWarning("QRhiWidget::grabTexture() only supports RGBA8 textures"); + return QImage(); + } + + d->ensureRhi(); + if (!d->rhi) { + // The widget (and its parent chain, if any) may not be shown at + // all, yet one may still want to use it for grabs. This is + // ridiculous of course because the rendering infrastructure is + // tied to the top-level widget that initializes upon expose, but + // it has to be supported. + d->offscreenRenderer.setConfig(d->config); + // no window passed in, so no swapchain, but we get a functional QRhi which we own + d->offscreenRenderer.create(); + d->rhi = d->offscreenRenderer.rhi(); + if (!d->rhi) { + qWarning("QRhiWidget: Failed to create dedicated QRhi for grabbing"); + return QImage(); + } + } + + const QSize prevSize = d->t ? d->t->pixelSize() : QSize(); + d->ensureTexture(); + if (!d->t) + return QImage(); + if (d->t->pixelSize() != prevSize) + initialize(d->rhi, d->t); + + QRhiReadbackResult readResult; + bool readCompleted = false; + readResult.completed = [&readCompleted] { readCompleted = true; }; + + QRhiCommandBuffer *cb = nullptr; + d->rhi->beginOffscreenFrame(&cb); + render(cb); + QRhiResourceUpdateBatch *readbackBatch = d->rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture(d->t, &readResult); + cb->resourceUpdate(readbackBatch); + d->rhi->endOffscreenFrame(); + + if (readCompleted) { + QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888); + QImage result = wrapperImage.copy(); + result.setDevicePixelRatio(devicePixelRatio()); + return result; + } else { + Q_UNREACHABLE(); + } + + return QImage(); +} + +/*! + Called when the widget is initialized, when the associated texture's size + changes, or when the QRhi and texture change for some reason. + + The implementation should be prepared that both \a rhi and \a outputTexture + can change between invocations of this function, although this is not + always going to happen in practice. When the widget size changes, this + function is called with the same \a rhi and \a outputTexture as before, but + \a outputTexture may have been rebuilt, meaning its + \l{QRhiTexture::pixelSize()}{size} and the underlying native texture + resource may be different than in the last invocation. + + One special case where the objects will be different is when performing a + grabTexture() with a widget that is not yet shown, and then making the + widget visible on-screen within a top-level widget. There the grab will + happen with a dedicated QRhi that is then replaced with the top-level + window's associated QRhi in subsequent initialize() and render() + invocations. + + Another, more common case is when the widget is reparented so that it + belongs to a new top-level window. In this case \a rhi and \a outputTexture + will definitely be different in the subsequent call to this function. Is is + then important that all existing QRhi resources are destroyed because they + belong to the previous QRhi that should not be used by the widget anymore. + + Implementations will typically create or rebuild a QRhiTextureRenderTarget + in order to allow the subsequent render() call to render into the texture. + When a depth buffer is necessary create a QRhiRenderBuffer as well. The + size if this must follow the size of \a outputTexture. A compact and + efficient way for this is the following: + + \code + if (m_rhi != rhi) { + // reset all resources (incl. m_ds, m_rt, m_rp) + } else if (m_output != outputTexture) { + // reset m_rt and m_rp + } + m_rhi = rhi; + m_output = outputTexture; + if (!m_ds) { + // no depth-stencil buffer yet, create one + m_ds = m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, m_output->pixelSize()); + m_ds->create(); + } else if (m_ds->pixelSize() != m_output->pixelSize()) { + // the size has changed, update the size and rebuild + m_ds->setPixelSize(m_output->pixelSize()); + m_ds->create(); + } + if (!m_rt) { + m_rt = m_rhi->newTextureRenderTarget({ { m_output }, m_ds }); + m_rp = m_rt->newCompatibleRenderPassDescriptor(); + m_rt->setRenderPassDescriptor(m_rp); + m_rt->create(); + } + \endcode + + The above snippet is also prepared for \a rhi and \a outputTexture changing + between invocations, via the checks at the beginning of the function. + + The created resources are expected to be released in the destructor + implementation of the subclass. \a rhi and \a outputTexture are not owned + by, and are guaranteed to outlive the QRhiWidget. + + \sa render() + */ +void QRhiWidget::initialize(QRhi *rhi, QRhiTexture *outputTexture) +{ + Q_UNUSED(rhi); + Q_UNUSED(outputTexture); +} + +/*! + Called when the widget contents (i.e. the contents of the texture) need + updating. + + There is always at least one call to initialize() before this function is + called. + + To request updates, call QWidget::update(). Calling update() from within + render() will lead to updating continuously, throttled by vsync. + + \a cb is the QRhiCommandBuffer for the current frame of the Qt Quick + scenegraph. The function is called with a frame being recorded, but without + an active render pass. + + \sa initialize() + */ +void QRhiWidget::render(QRhiCommandBuffer *cb) +{ + Q_UNUSED(cb); +} diff --git a/tests/manual/rhi/rhiwidgetproto/rhiwidget.h b/tests/manual/rhi/rhiwidgetproto/rhiwidget.h new file mode 100644 index 0000000000..0ac947b058 --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/rhiwidget.h @@ -0,0 +1,56 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef RHIWIDGET_H +#define RHIWIDGET_H + +#include <QWidget> +#include <rhi/qrhi.h> + +class QRhiWidgetPrivate; + +class QRhiWidget : public QWidget +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QRhiWidget) + Q_PROPERTY(QSize explicitSize READ explicitSize WRITE setExplicitSize NOTIFY explicitSizeChanged) + +public: + QRhiWidget(QWidget *parent = nullptr, Qt::WindowFlags f = {}); + ~QRhiWidget(); + + enum Api { + OpenGL, + Metal, + Vulkan, + D3D11, + Null + }; + + Api api() const; + void setApi(Api api); + + bool isDebugLayerEnabled() const; + void setDebugLayer(bool enable); + + QRhiTexture::Format textureFormat() const; + void setTextureFormat(QRhiTexture::Format format); + + QSize explicitSize() const; + void setExplicitSize(const QSize &pixelSize); + + virtual void initialize(QRhi *rhi, QRhiTexture *outputTexture); + virtual void render(QRhiCommandBuffer *cb); + + QImage grabTexture(); + +Q_SIGNALS: + void explicitSizeChanged(const QSize &pixelSize); + +protected: + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + bool event(QEvent *e) override; +}; + +#endif diff --git a/tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h b/tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h new file mode 100644 index 0000000000..da8ebc5c8f --- /dev/null +++ b/tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h @@ -0,0 +1,38 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef RHIWIDGET_P_H +#define RHIWIDGET_P_H + +#include "rhiwidget.h" + +#include <private/qwidget_p.h> +#include <private/qbackingstorerhisupport_p.h> + +class QRhiWidgetPrivate : public QWidgetPrivate +{ + Q_DECLARE_PUBLIC(QRhiWidget) +public: + TextureData texture() const override + { + TextureData td; + if (!textureInvalid) + td.textureLeft = t; + return td; + } + QPlatformBackingStoreRhiConfig rhiConfig() const override; + + void ensureRhi(); + void ensureTexture(); + + QRhi *rhi = nullptr; + QRhiTexture *t = nullptr; + bool noSize = false; + QPlatformBackingStoreRhiConfig config; + QRhiTexture::Format format = QRhiTexture::RGBA8; + QSize explicitSize; + QBackingStoreRhiSupport offscreenRenderer; + bool textureInvalid = false; +}; + +#endif |