diff options
Diffstat (limited to 'tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp')
-rw-r--r-- | tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp | 538 |
1 files changed, 538 insertions, 0 deletions
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); +} |