// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "rhirenderingextensions.h" #include "cube.h" #include #include #include #include #include #include // The extension exposed to QML is the consumer, backed by an extension node // with Main mode that renders a textured cube within the main render pass. It // automatically creates a child extension, acting as the producer. This // backend node for this has the Standalone mode, as it creates and renders // into a QRhiTexture (doing its own render pass with a custom render target). // // The order is like this because extension backend nodes are invoked // (prepareData, prepareRender, render) in a bottom-up order based on the // frontend object relationships, i.e. the backend node of a child gets its // functions called before the backend node of its parent. For siblings the // order is undefined. class ProducerRenderer : public QSSGRenderExtension { public: ProducerRenderer(); ~ProducerRenderer(); bool prepareData(QSSGFrameData &data) override; void prepareRender(QSSGFrameData &data) override; void render(QSSGFrameData &data) override; void resetForFrame() override; RenderMode mode() const override { return RenderMode::Standalone; } RenderStage stage() const override { return RenderStage::PostColor; } std::unique_ptr texture; std::unique_ptr rt; std::unique_ptr rpDesc; std::unique_ptr vbuf; std::unique_ptr ubuf; std::unique_ptr srb; std::unique_ptr ps; float rotation = 0.0f; }; ProducerRenderer::ProducerRenderer() { qDebug() << "ProducerRenderer" << this << "ctor; current thread:" << QThread::currentThread() << "main thread:" << qGuiApp->thread(); } ProducerRenderer::~ProducerRenderer() { qDebug() << "ProducerRenderer" << this << "dtor; current thread:" << QThread::currentThread() << "main thread:" << qGuiApp->thread(); } bool ProducerRenderer::prepareData(QSSGFrameData &) { return true; } static QShader getShader(const QString &name) { QFile f(name); return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader(); } static float triangle[] = { 0.0f, 0.5f, 1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, }; void ProducerRenderer::prepareRender(QSSGFrameData &data) { const std::unique_ptr &rhiCtx = data.contextInterface()->rhiContext(); if (!rhiCtx) return; QRhi *rhi = rhiCtx->rhi(); if (!texture) { texture.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget)); texture->create(); rt.reset(rhi->newTextureRenderTarget({ texture.get() })); rpDesc.reset(rt->newCompatibleRenderPassDescriptor()); rt->setRenderPassDescriptor(rpDesc.get()); rt->create(); } if (!ps) { vbuf.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangle))); vbuf->create(); ubuf.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64)); ubuf->create(); srb.reset(rhi->newShaderResourceBindings()); srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()), }); srb->create(); ps.reset(rhi->newGraphicsPipeline()); ps->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shaders/color.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shaders/color.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 5 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) } }); ps->setVertexInputLayout(inputLayout); ps->setShaderResourceBindings(srb.get()); ps->setRenderPassDescriptor(rpDesc.get()); ps->create(); QRhiCommandBuffer *cb = rhiCtx->commandBuffer(); QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); resourceUpdates->uploadStaticBuffer(vbuf.get(), triangle); cb->resourceUpdate(resourceUpdates); } } void ProducerRenderer::render(QSSGFrameData &data) { const std::unique_ptr &rhiCtx = data.contextInterface()->rhiContext(); if (!rhiCtx) return; QRhi *rhi = rhiCtx->rhi(); QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); const QSize outputSize = rt->pixelSize(); QMatrix4x4 viewProjection = rhi->clipSpaceCorrMatrix(); viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f); viewProjection.translate(0, 0, -4); QMatrix4x4 modelViewProjection = viewProjection; modelViewProjection.rotate(rotation, 0, 1, 0); resourceUpdates->updateDynamicBuffer(ubuf.get(), 0, 64, modelViewProjection.constData()); QRhiCommandBuffer *cb = rhiCtx->commandBuffer(); const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f); cb->beginPass(rt.get(), clearColor, { 1.0f, 0}, resourceUpdates); cb->setGraphicsPipeline(ps.get()); cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height())); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(vbuf.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass(); } void ProducerRenderer::resetForFrame() { } class ProducerExtension : public QQuick3DRenderExtension { public: ProducerExtension(QQuick3DRenderExtension *parent = nullptr) : QQuick3DRenderExtension(parent) { } QSSGRenderGraphObject *updateSpatialNode(QSSGRenderGraphObject *node) override; ProducerRenderer *renderer = nullptr; float rotation = 0.0f; }; QSSGRenderGraphObject *ProducerExtension::updateSpatialNode(QSSGRenderGraphObject *node) { if (!renderer) renderer = new ProducerRenderer; if (node != renderer) { delete node; node = renderer; } renderer->rotation = rotation; return node; } class ConsumerRenderer : public QSSGRenderExtension { public: ConsumerRenderer(ProducerExtension *producer); ~ConsumerRenderer(); bool prepareData(QSSGFrameData &data) override; void prepareRender(QSSGFrameData &data) override; void render(QSSGFrameData &data) override; void resetForFrame() override; RenderMode mode() const override { return RenderMode::Main; } RenderStage stage() const override { return RenderStage::PostColor; } ProducerExtension *producer; bool canRender = false; QMatrix4x4 viewProjection; QRhiViewport mainViewport; std::unique_ptr vbuf; std::unique_ptr ubuf; std::unique_ptr sampler; std::unique_ptr srb; std::unique_ptr ps; QVector rpFormat; QMatrix4x4 modelMatrix; void updateModelMatrix(const QVector3D &pos) { modelMatrix.setToIdentity(); modelMatrix.translate(pos); modelMatrix.scale(10, 10, 10); } }; ConsumerRenderer::ConsumerRenderer(ProducerExtension *producer) : producer(producer) { qDebug() << "ConsumerRenderer" << this << "ctor; current thread:" << QThread::currentThread() << "main thread:" << qGuiApp->thread(); } ConsumerRenderer::~ConsumerRenderer() { qDebug() << "ConsumerRenderer" << this << "dtor; current thread:" << QThread::currentThread() << "main thread:" << qGuiApp->thread(); } bool ConsumerRenderer::prepareData(QSSGFrameData &data) { auto camera = data.activeCamera(); if (camera == QSSGCameraId::Invalid) { canRender = false; return false; } viewProjection = QSSGCameraHelpers::getViewProjectionMatrix(camera); canRender = true; return true; } void ConsumerRenderer::prepareRender(QSSGFrameData &data) { if (!canRender) return; // Due to lifetime guarantees (that the backend node of the child frontend // object is destroyed before the backend node of the parent frontend // object), accessing producer->m_renderer should always be safe in // ConsumerRenderer. QRhiTexture *texture = producer->renderer->texture.get(); if (!texture) qFatal("No texture from producer"); const std::unique_ptr &rhiCtx = data.contextInterface()->rhiContext(); if (!rhiCtx) return; QRhi *rhi = rhiCtx->rhi(); QRhiCommandBuffer *cb = rhiCtx->commandBuffer(); QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); // The SceneEnvironment changes the MSAA setting dynamically. To follow // this, we need this extra check so that we can recreate the graphics // pipeline with the correct sample count upon a change. if (ps && ps->sampleCount() != rhiCtx->mainPassSampleCount()) ps.reset(); // This is not strictly required for this application. However, if there is // a chance that the View3D moves to become a (texture-backed) layer, part // of a ShaderEffect, or gets moved to a new window, robust implementations // should prepare for that like this, since some render target parameters // may be different then, which may imply that the graphics pipeline needs // to be recreated. QVector currentRpFormat = rhiCtx->mainRenderPassDescriptor()->serializedFormat(); if (ps && rpFormat != currentRpFormat) ps.reset(); if (!ps) { vbuf.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube))); vbuf->create(); resourceUpdates->uploadStaticBuffer(vbuf.get(), cube); ubuf.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 16)); ubuf->create(); sampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge)); sampler->create(); srb.reset(rhi->newShaderResourceBindings()); srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, ubuf.get()), QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture, sampler.get()) }); srb->create(); ps.reset(rhi->newGraphicsPipeline()); // depth testing is important since we test against the objects already rendered by the View3D. (the mode is Overlay) ps->setDepthTest(true); ps->setDepthWrite(true); ps->setCullMode(QRhiGraphicsPipeline::Back); ps->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shaders/texture.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shaders/texture.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; // The cube is provided as non-interleaved sets of positions, UVs, normals. // Normals are not interesting here, only need the positions and UVs. inputLayout.setBindings({ { 3 * sizeof(float) }, { 2 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, { 1, 1, QRhiVertexInputAttribute::Float2, 0 } }); ps->setVertexInputLayout(inputLayout); ps->setShaderResourceBindings(srb.get()); // Must play nice with the View3D's render target. ps->setRenderPassDescriptor(rhiCtx->mainRenderPassDescriptor()); rpFormat = currentRpFormat; ps->setSampleCount(rhiCtx->mainPassSampleCount()); ps->create(); } // clipSpaceCorrMatrix has to be applied manually QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix() * viewProjection * modelMatrix; resourceUpdates->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData()); QVector4D color(0, 0.5f, 0, 1); resourceUpdates->updateDynamicBuffer(ubuf.get(), 64, 16, &color); cb->resourceUpdate(resourceUpdates); mainViewport = data.getPipelineState().viewport; } void ConsumerRenderer::render(QSSGFrameData &data) { if (!canRender) return; const std::unique_ptr &rhiCtx = data.contextInterface()->rhiContext(); if (!rhiCtx) return; QRhiCommandBuffer *cb = rhiCtx->commandBuffer(); cb->debugMarkBegin(QByteArrayLiteral("Custom rhi rendering (consumer)")); cb->setGraphicsPipeline(ps.get()); cb->setViewport(mainViewport); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBindings[] = { { vbuf.get(), 0 }, { vbuf.get(), quint32(36 * 3 * sizeof(float)) } }; cb->setVertexInput(0, 2, vbufBindings); cb->draw(36); cb->debugMarkEnd(); } void ConsumerRenderer::resetForFrame() { } MyExtension::MyExtension(QQuick3DObject *parent) : QQuick3DRenderExtension(parent) { m_producer = new ProducerExtension(this); // Calling update() on m_producer in updateSpatialNode() would be bad: that // would mean the producer only saw every *other* change. Instead, // trigger update() via the changed signals. QObject::connect(this, &MyExtension::subSceneRotationChanged, m_producer, &ProducerExtension::update); } QSSGRenderGraphObject *MyExtension::updateSpatialNode(QSSGRenderGraphObject *node) { if (!node) node = new ConsumerRenderer(m_producer); ConsumerRenderer *renderer = static_cast(node); if (m_dirtyFlag & PositionDirty) renderer->updateModelMatrix(QVector3D(m_x, m_y, m_z)); if (m_dirtyFlag & SubSceneRotationDirty) renderer->producer->rotation = m_subSceneRotation; m_dirtyFlag = {}; return node; } void MyExtension::markDirty(Dirty v) { m_dirtyFlag |= v; update(); }