/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ // This is a compact, minimal, single-file demo of deciding the backend at // runtime while using the exact same shaders and rendering code without any // branching whatsoever once the QWindow is up and the RHI is initialized. #include #include #include #include #include #include #include #include #ifndef QT_NO_OPENGL #include #include #endif #if QT_CONFIG(vulkan) #include #include #endif #ifdef Q_OS_WIN #include #endif #if defined(Q_OS_MACOS) || defined(Q_OS_IOS) #include #endif static float vertexData[] = { // Y up (note clipSpaceCorrMatrix in m_proj), CCW 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, }; static QShader getShader(const QString &name) { QFile f(name); if (f.open(QIODevice::ReadOnly)) return QShader::fromSerialized(f.readAll()); return QShader(); } enum GraphicsApi { OpenGL, Vulkan, D3D11, Metal, Null }; static GraphicsApi graphicsApi; static QString graphicsApiName() { switch (graphicsApi) { case OpenGL: return QLatin1String("OpenGL 2.x"); case Vulkan: return QLatin1String("Vulkan"); case D3D11: return QLatin1String("Direct3D 11"); case Metal: return QLatin1String("Metal"); case Null: return QLatin1String("Null"); default: break; } return QString(); } class Window : public QWindow { public: Window(); ~Window(); void init(); void releaseResources(); void resizeSwapChain(); void releaseSwapChain(); void render(); void exposeEvent(QExposeEvent *) override; bool event(QEvent *) override; private: bool m_running = false; bool m_notExposed = false; bool m_newlyExposed = false; QRhi *m_r = nullptr; bool m_hasSwapChain = false; QRhiSwapChain *m_sc = nullptr; QRhiRenderBuffer *m_ds = nullptr; QRhiRenderPassDescriptor *m_rp = nullptr; QRhiBuffer *m_vbuf = nullptr; bool m_vbufReady = false; QRhiBuffer *m_ubuf = nullptr; QRhiShaderResourceBindings *m_srb = nullptr; QRhiGraphicsPipeline *m_ps = nullptr; QVector releasePool; QMatrix4x4 m_proj; float m_rotation = 0; float m_opacity = 1; int m_opacityDir = -1; QElapsedTimer m_timer; qint64 m_elapsedMs; int m_elapsedCount; #ifndef QT_NO_OPENGL QOffscreenSurface *m_fallbackSurface = nullptr; #endif }; Window::Window() { // Tell the platform plugin what we want. switch (graphicsApi) { case OpenGL: #if QT_CONFIG(opengl) setSurfaceType(OpenGLSurface); setFormat(QRhiGles2InitParams::adjustedFormat()); #endif break; case Vulkan: setSurfaceType(VulkanSurface); break; case D3D11: setSurfaceType(OpenGLSurface); // not a typo break; case Metal: #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) setSurfaceType(MetalSurface); #endif break; default: break; } } Window::~Window() { releaseResources(); } void Window::exposeEvent(QExposeEvent *) { // initialize and start rendering when the window becomes usable for graphics purposes if (isExposed() && !m_running) { m_running = true; init(); resizeSwapChain(); } // stop pushing frames when not exposed (or size is 0) if ((!isExposed() || (m_hasSwapChain && m_sc->surfacePixelSize().isEmpty())) && m_running) m_notExposed = true; // continue when exposed again and the surface has a valid size. // note that the surface size can be (0, 0) even though size() reports a valid one... if (isExposed() && m_running && m_notExposed && !m_sc->surfacePixelSize().isEmpty()) { m_notExposed = false; m_newlyExposed = true; } // always render a frame on exposeEvent() (when exposed) in order to update // immediately on window resize. if (isExposed() && !m_sc->surfacePixelSize().isEmpty()) render(); } bool Window::event(QEvent *e) { switch (e->type()) { case QEvent::UpdateRequest: render(); break; case QEvent::PlatformSurface: // this is the proper time to tear down the swapchain (while the native window and surface are still around) if (static_cast(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) releaseSwapChain(); break; default: break; } return QWindow::event(e); } void Window::init() { if (graphicsApi == Null) { QRhiNullInitParams params; m_r = QRhi::create(QRhi::Null, ¶ms); } #ifndef QT_NO_OPENGL if (graphicsApi == OpenGL) { m_fallbackSurface = QRhiGles2InitParams::newFallbackSurface(); QRhiGles2InitParams params; params.fallbackSurface = m_fallbackSurface; params.window = this; m_r = QRhi::create(QRhi::OpenGLES2, ¶ms); } #endif #if QT_CONFIG(vulkan) if (graphicsApi == Vulkan) { QRhiVulkanInitParams params; params.inst = vulkanInstance(); params.window = this; m_r = QRhi::create(QRhi::Vulkan, ¶ms); } #endif #ifdef Q_OS_WIN if (graphicsApi == D3D11) { QRhiD3D11InitParams params; m_r = QRhi::create(QRhi::D3D11, ¶ms); } #endif #if defined(Q_OS_MACOS) || defined(Q_OS_IOS) if (graphicsApi == Metal) { QRhiMetalInitParams params; m_r = QRhi::create(QRhi::Metal, ¶ms); } #endif if (!m_r) qFatal("Failed to create RHI backend"); // now onto the backend-independent init m_sc = m_r->newSwapChain(); // allow depth-stencil, although we do not actually enable depth test/write for the triangle m_ds = m_r->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(), // no need to set the size here, due to UsedWithSwapChainOnly 1, QRhiRenderBuffer::UsedWithSwapChainOnly); releasePool << m_ds; m_sc->setWindow(this); m_sc->setDepthStencil(m_ds); m_rp = m_sc->newCompatibleRenderPassDescriptor(); releasePool << m_rp; m_sc->setRenderPassDescriptor(m_rp); m_vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)); releasePool << m_vbuf; m_vbuf->build(); m_vbufReady = false; m_ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68); releasePool << m_ubuf; m_ubuf->build(); m_srb = m_r->newShaderResourceBindings(); releasePool << m_srb; m_srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf) }); m_srb->build(); m_ps = m_r->newGraphicsPipeline(); releasePool << m_ps; QRhiGraphicsPipeline::TargetBlend premulAlphaBlend; premulAlphaBlend.enable = true; m_ps->setTargetBlends({ premulAlphaBlend }); const QShader vs = getShader(QLatin1String(":/color.vert.qsb")); if (!vs.isValid()) qFatal("Failed to load shader pack (vertex)"); const QShader fs = getShader(QLatin1String(":/color.frag.qsb")); if (!fs.isValid()) qFatal("Failed to load shader pack (fragment)"); m_ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 5 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) } }); m_ps->setVertexInputLayout(inputLayout); m_ps->setShaderResourceBindings(m_srb); m_ps->setRenderPassDescriptor(m_rp); m_ps->build(); } void Window::releaseResources() { qDeleteAll(releasePool); releasePool.clear(); delete m_sc; // native swapchain is likely released already m_sc = nullptr; delete m_r; #ifndef QT_NO_OPENGL delete m_fallbackSurface; #endif } void Window::resizeSwapChain() { m_hasSwapChain = m_sc->buildOrResize(); // also handles m_ds m_elapsedMs = 0; m_elapsedCount = 0; const QSize outputSize = m_sc->currentPixelSize(); m_proj = m_r->clipSpaceCorrMatrix(); m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 100.0f); m_proj.translate(0, 0, -4); } void Window::releaseSwapChain() { if (m_hasSwapChain) { m_hasSwapChain = false; m_sc->release(); } } void Window::render() { if (!m_hasSwapChain || m_notExposed) return; // If the window got resized or got newly exposed, resize the swapchain. // (the newly-exposed case is not actually required by some // platforms/backends, but f.ex. Vulkan on Windows seems to need it) if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) { resizeSwapChain(); if (!m_hasSwapChain) return; m_newlyExposed = false; } // Start a new frame. This is where we block when too far ahead of // GPU/present, and that's what throttles the thread to the refresh rate. // (except for OpenGL where it happens either in endFrame or somewhere else // depending on the GL implementation) QRhi::FrameOpResult r = m_r->beginFrame(m_sc); if (r == QRhi::FrameOpSwapChainOutOfDate) { resizeSwapChain(); if (!m_hasSwapChain) return; r = m_r->beginFrame(m_sc); } if (r != QRhi::FrameOpSuccess) { requestUpdate(); return; } if (m_elapsedCount) m_elapsedMs += m_timer.elapsed(); m_timer.restart(); m_elapsedCount += 1; if (m_elapsedMs >= 4000) { qDebug("%f", m_elapsedCount / 4.0f); m_elapsedMs = 0; m_elapsedCount = 0; } // Set up buffer updates. QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch(); if (!m_vbufReady) { m_vbufReady = true; u->uploadStaticBuffer(m_vbuf, vertexData); } m_rotation += 1.0f; QMatrix4x4 mvp = m_proj; mvp.rotate(m_rotation, 0, 1, 0); u->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData()); m_opacity += m_opacityDir * 0.005f; if (m_opacity < 0.0f || m_opacity > 1.0f) { m_opacityDir *= -1; m_opacity = qBound(0.0f, m_opacity, 1.0f); } u->updateDynamicBuffer(m_ubuf, 64, 4, &m_opacity); QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); const QSize outputSizeInPixels = m_sc->currentPixelSize(); // Apply buffer updates, clear, start the renderpass (where applicable). cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u); cb->setGraphicsPipeline(m_ps); cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) }); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf, 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass(); // Submit. m_r->endFrame(m_sc); requestUpdate(); // render continuously, throttled by the presentation rate (due to beginFrame above) } int main(int argc, char **argv) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); // Defaults. #if defined(Q_OS_WIN) graphicsApi = D3D11; #elif defined(Q_OS_MACOS) || defined(Q_OS_IOS) graphicsApi = Metal; #elif QT_CONFIG(vulkan) graphicsApi = Vulkan; #else graphicsApi = OpenGL; #endif // Allow overriding via the command line. QCommandLineParser cmdLineParser; cmdLineParser.addHelpOption(); QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)")); cmdLineParser.addOption(glOption); QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan")); cmdLineParser.addOption(vkOption); QCommandLineOption d3dOption({ "d", "d3d11" }, QLatin1String("Direct3D 11")); cmdLineParser.addOption(d3dOption); QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal")); cmdLineParser.addOption(mtlOption); QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null")); cmdLineParser.addOption(nullOption); cmdLineParser.process(app); if (cmdLineParser.isSet(glOption)) graphicsApi = OpenGL; if (cmdLineParser.isSet(vkOption)) graphicsApi = Vulkan; if (cmdLineParser.isSet(d3dOption)) graphicsApi = D3D11; if (cmdLineParser.isSet(mtlOption)) graphicsApi = Metal; if (cmdLineParser.isSet(nullOption)) graphicsApi = Null; // Vulkan setup. #if QT_CONFIG(vulkan) QVulkanInstance inst; if (graphicsApi == Vulkan) { if (!inst.create()) { qWarning("Failed to create Vulkan instance, switching to OpenGL"); graphicsApi = OpenGL; } } #endif // Create and show the window. Window w; #if QT_CONFIG(vulkan) if (graphicsApi == Vulkan) w.setVulkanInstance(&inst); #endif w.resize(1280, 720); w.setTitle(graphicsApiName()); w.show(); // Window::event() will not get invoked when the // PlatformSurfaceAboutToBeDestroyed event is sent during the QWindow // destruction. That happens only when exiting via app::quit() instead of // the more common QWindow::close(). Take care of it: if the // QPlatformWindow is still around (there was no close() yet), get rid of // the swapchain while it's not too late. if (w.handle()) w.releaseSwapChain(); return app.exec(); }