From 8d72aba9e3358fdf51578533ab7f46602f7fd103 Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Sun, 12 Feb 2017 15:08:52 +0100 Subject: [PATCH] Introduce QVulkanWindow A convenience subclass of QWindow that provides a Vulkan-capable window with a double-buffered FIFO swapchain. While advanced use cases are better served by a custom QWindow subclass, many applications can benefit from having a convenient helper that makes getting started easier. Add also three examples of increasing complexity, and a variant that shows embeddeding into widgets via QWindowContainer. [ChangeLog][QtGui] Added QVulkanWindow, a convenience subclass of QWindow. Task-number: QTBUG-55981 Change-Id: I6cdc9ff1390ac6258e278377233fd369a0bfeddc Reviewed-by: Andy Nichols --- examples/examples.pro | 5 +- .../vulkan/doc/images/hellovulkantexture.png | Bin 0 -> 10259 bytes .../vulkan/doc/images/hellovulkantriangle.png | Bin 0 -> 30952 bytes .../vulkan/doc/images/hellovulkanwidget.png | Bin 0 -> 25256 bytes .../vulkan/doc/images/hellovulkanwindow.png | Bin 0 -> 2736 bytes .../vulkan/doc/src/hellovulkantexture.qdoc | 41 + .../vulkan/doc/src/hellovulkantriangle.qdoc | 49 + .../vulkan/doc/src/hellovulkanwidget.qdoc | 49 + .../vulkan/doc/src/hellovulkanwindow.qdoc | 101 + .../hellovulkantexture/hellovulkantexture.cpp | 828 +++++ .../hellovulkantexture/hellovulkantexture.h | 108 + .../hellovulkantexture/hellovulkantexture.pro | 7 + .../hellovulkantexture/hellovulkantexture.qrc | 7 + examples/vulkan/hellovulkantexture/main.cpp | 91 + examples/vulkan/hellovulkantexture/qt256.png | Bin 0 -> 6208 bytes .../vulkan/hellovulkantexture/texture.frag | 12 + .../vulkan/hellovulkantexture/texture.vert | 18 + .../hellovulkantexture/texture_frag.spv | Bin 0 -> 556 bytes .../hellovulkantexture/texture_vert.spv | Bin 0 -> 968 bytes .../hellovulkantriangle.pro | 12 + .../hellovulkantriangle.qrc | 6 + examples/vulkan/hellovulkantriangle/main.cpp | 100 + .../hellovulkanwidget/hellovulkanwidget.cpp | 182 ++ .../hellovulkanwidget/hellovulkanwidget.h | 98 + .../hellovulkanwidget/hellovulkanwidget.pro | 16 + .../hellovulkanwidget/hellovulkanwidget.qrc | 6 + examples/vulkan/hellovulkanwidget/main.cpp | 93 + .../hellovulkanwindow/hellovulkanwindow.cpp | 128 + .../hellovulkanwindow/hellovulkanwindow.h | 77 + .../hellovulkanwindow/hellovulkanwindow.pro | 6 + examples/vulkan/hellovulkanwindow/main.cpp | 93 + examples/vulkan/shared/color.frag | 10 + examples/vulkan/shared/color.vert | 18 + examples/vulkan/shared/color_frag.spv | Bin 0 -> 496 bytes examples/vulkan/shared/color_vert.spv | Bin 0 -> 960 bytes examples/vulkan/shared/trianglerenderer.cpp | 513 ++++ examples/vulkan/shared/trianglerenderer.h | 85 + examples/vulkan/vulkan.pro | 7 + src/gui/vulkan/qvulkanwindow.cpp | 2678 +++++++++++++++++ src/gui/vulkan/qvulkanwindow.h | 161 + src/gui/vulkan/qvulkanwindow_p.h | 188 ++ src/gui/vulkan/vulkan.pri | 7 +- tests/auto/gui/qvulkan/tst_qvulkan.cpp | 282 +- 43 files changed, 6076 insertions(+), 6 deletions(-) create mode 100644 examples/vulkan/doc/images/hellovulkantexture.png create mode 100644 examples/vulkan/doc/images/hellovulkantriangle.png create mode 100644 examples/vulkan/doc/images/hellovulkanwidget.png create mode 100644 examples/vulkan/doc/images/hellovulkanwindow.png create mode 100644 examples/vulkan/doc/src/hellovulkantexture.qdoc create mode 100644 examples/vulkan/doc/src/hellovulkantriangle.qdoc create mode 100644 examples/vulkan/doc/src/hellovulkanwidget.qdoc create mode 100644 examples/vulkan/doc/src/hellovulkanwindow.qdoc create mode 100644 examples/vulkan/hellovulkantexture/hellovulkantexture.cpp create mode 100644 examples/vulkan/hellovulkantexture/hellovulkantexture.h create mode 100644 examples/vulkan/hellovulkantexture/hellovulkantexture.pro create mode 100644 examples/vulkan/hellovulkantexture/hellovulkantexture.qrc create mode 100644 examples/vulkan/hellovulkantexture/main.cpp create mode 100644 examples/vulkan/hellovulkantexture/qt256.png create mode 100644 examples/vulkan/hellovulkantexture/texture.frag create mode 100644 examples/vulkan/hellovulkantexture/texture.vert create mode 100644 examples/vulkan/hellovulkantexture/texture_frag.spv create mode 100644 examples/vulkan/hellovulkantexture/texture_vert.spv create mode 100644 examples/vulkan/hellovulkantriangle/hellovulkantriangle.pro create mode 100644 examples/vulkan/hellovulkantriangle/hellovulkantriangle.qrc create mode 100644 examples/vulkan/hellovulkantriangle/main.cpp create mode 100644 examples/vulkan/hellovulkanwidget/hellovulkanwidget.cpp create mode 100644 examples/vulkan/hellovulkanwidget/hellovulkanwidget.h create mode 100644 examples/vulkan/hellovulkanwidget/hellovulkanwidget.pro create mode 100644 examples/vulkan/hellovulkanwidget/hellovulkanwidget.qrc create mode 100644 examples/vulkan/hellovulkanwidget/main.cpp create mode 100644 examples/vulkan/hellovulkanwindow/hellovulkanwindow.cpp create mode 100644 examples/vulkan/hellovulkanwindow/hellovulkanwindow.h create mode 100644 examples/vulkan/hellovulkanwindow/hellovulkanwindow.pro create mode 100644 examples/vulkan/hellovulkanwindow/main.cpp create mode 100644 examples/vulkan/shared/color.frag create mode 100644 examples/vulkan/shared/color.vert create mode 100644 examples/vulkan/shared/color_frag.spv create mode 100644 examples/vulkan/shared/color_vert.spv create mode 100644 examples/vulkan/shared/trianglerenderer.cpp create mode 100644 examples/vulkan/shared/trianglerenderer.h create mode 100644 examples/vulkan/vulkan.pro create mode 100644 src/gui/vulkan/qvulkanwindow.cpp create mode 100644 src/gui/vulkan/qvulkanwindow.h create mode 100644 src/gui/vulkan/qvulkanwindow_p.h diff --git a/examples/examples.pro b/examples/examples.pro index f66c5cbf220..d2ce1fd2948 100644 --- a/examples/examples.pro +++ b/examples/examples.pro @@ -15,7 +15,10 @@ SUBDIRS = \ widgets \ xml -qtHaveModule(gui):qtConfig(opengl): SUBDIRS += opengl +qtHaveModule(gui) { + qtConfig(opengl): SUBDIRS += opengl + qtConfig(vulkan): SUBDIRS += vulkan +} aggregate.files = aggregate/examples.pro aggregate.path = $$[QT_INSTALL_EXAMPLES] diff --git a/examples/vulkan/doc/images/hellovulkantexture.png b/examples/vulkan/doc/images/hellovulkantexture.png new file mode 100644 index 0000000000000000000000000000000000000000..0cb47a70be35f953072a5487be80a2731f35b93e GIT binary patch literal 10259 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 UWcbtuF7iP%RCQHqZvTV-Uw!7o*8l(j literal 0 HcmV?d00001 diff --git a/examples/vulkan/doc/images/hellovulkantriangle.png b/examples/vulkan/doc/images/hellovulkantriangle.png new file mode 100644 index 0000000000000000000000000000000000000000..f88b27a873a471cb6b6aba7f3e0b7be7607141d9 GIT binary patch literal 30952 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 literal 0 HcmV?d00001 diff --git a/examples/vulkan/doc/images/hellovulkanwidget.png b/examples/vulkan/doc/images/hellovulkanwidget.png new file mode 100644 index 0000000000000000000000000000000000000000..b85d4dc5966a9b4fe5fee96d63bf33c75c1adc1a GIT binary patch literal 25256 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 T1^ouRF37>o<#4gh#e4q+*UW_) literal 0 HcmV?d00001 diff --git a/examples/vulkan/doc/images/hellovulkanwindow.png b/examples/vulkan/doc/images/hellovulkanwindow.png new file mode 100644 index 0000000000000000000000000000000000000000..c55029312ce17d85cd862c43235ac14baaeb80f3 GIT binary patch literal 2736 zcmeHJYgEz)6#rAUSx!8zm6eh^Yt6Gm<&28N)E=6tQmyGD#sC1c?UO-xxRPH1V1ui>%Mri0;MtM$6h8ul_km-l{W@{C4!W8VXdW3G{GPZP6_^jrSFGD-H=AQz;JVYxWn;?N`Mh($o?Ls~ z=tWGyTf`fwF+dCkV~^j5Ky))7 zQ>lU8Fae(*Ex&@nWLlo&b!LIHLk(eepcTEDY#^;1SmlYE&GD}Y6YbN^bnAB%EE*2= z%0tD~%y)BiQtP;#Da;d$pZCN4#P;_ArbL%m{)-Q8WXVg^HD zdlqhEy_B9-BI{<5HB&A6kQH@0#jyP1Hl2smONWXb)5}}*9pZ-J7IdQ@U(n+e^Ij~4 z;13nF- zZmX?<*>RRtACSM*l6EkN=vabX32U@CjV6?{&!C6)^tRG!%;ZhiK~$Mi+<@^U);2V1 z9!)~&Q~p>QVyz^ArKxl%eabqT<8nL@n21D8CWjaYwF2+UFg7MSj!D zr~!3Z0%!XP;odjq7);p(cq!Pwkh%r!)OuX};f1JS0T+I-f*^Z7MgAzA6SNQ`y6@A= zo1@vHN*zr+Or=^P*+PN9(FInPtRL%1|d`gDc1X=Lpg zbj>a}g|^-AdG!2!=gt%qw?cWiQ*zWM*a?BxYIqE9BsNyLB+h{Q|Em#n2ZF~!m)Wfy z{(ImSgE#~NaZX@)(rr~`G!y?NPp3YW<)U(^)VFdJcx}sCpiyKFJV32E?CS=ckQ)Q; zFvz#e9l-Z^qnl4a;NIut=O%pKgwN;jf6@do-U!eRod0MW`6U-ECA|t;{W1Y}S8o^g I(F;j`0mUMNF8}}l literal 0 HcmV?d00001 diff --git a/examples/vulkan/doc/src/hellovulkantexture.qdoc b/examples/vulkan/doc/src/hellovulkantexture.qdoc new file mode 100644 index 00000000000..d0e0ca90a87 --- /dev/null +++ b/examples/vulkan/doc/src/hellovulkantexture.qdoc @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example hellovulkantexture + \ingroup examples-vulkan + \title Hello Vulkan Texture Vulkan Example + \brief Shows the basics of rendering with textures in a QVulkanWindow + + The \e{Hello Vulkan Texture Example} builds on \l hellovulkantriangle. Here + instead of drawing a single triangle, a triangle strip is drawn in order to + get a quad on the screen. This is then textured using a QImage loaded from + a .png image file. + + \image hellovulkantexture.png + \include examples-run.qdocinc +*/ diff --git a/examples/vulkan/doc/src/hellovulkantriangle.qdoc b/examples/vulkan/doc/src/hellovulkantriangle.qdoc new file mode 100644 index 00000000000..81af776ea1c --- /dev/null +++ b/examples/vulkan/doc/src/hellovulkantriangle.qdoc @@ -0,0 +1,49 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example hellovulkantriangle + \ingroup examples-vulkan + \title Hello Vulkan Triangle Example + \brief Shows the basics of rendering with QVulkanWindow and the Vulkan API + + The \e{Hello Vulkan Triangle Example} builds on \l hellovulkanwindow. This + time a full graphics pipeline is created, including a vertex and fragment + shader. This pipeline is then used to render a triangle. + + \image hellovulkantriangle.png + + The example also demonstrates multisample antialiasing. Based on the + supported sample counts reported by QVulkanWindow::supportedSampleCounts() + the example chooses between 8x, 4x, or no multisampling. Once configured + via QVulkanWindow::setSamples(), QVulkanWindow takes care of the rest: the + additional multisample color buffers are created automatically, and + resolving into the swapchain buffers is performed at the end of the default + render pass for each frame. + + \include examples-run.qdocinc +*/ diff --git a/examples/vulkan/doc/src/hellovulkanwidget.qdoc b/examples/vulkan/doc/src/hellovulkanwidget.qdoc new file mode 100644 index 00000000000..7987bdeff96 --- /dev/null +++ b/examples/vulkan/doc/src/hellovulkanwidget.qdoc @@ -0,0 +1,49 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example hellovulkanwidget + \ingroup examples-vulkan + \title Hello Vulkan Widget Example + \brief Shows the usage of QVulkanWindow in QWidget applications + + The \e{Hello Vulkan Widget Example} is a variant of \l hellovulkantriangle + that embeds the QVulkanWindow into a QWidget-based user interface using + QWidget::createWindowContainer(). + + \image hellovulkanwidget.png + + The code to set up the Vulkan pipeline and render the triangle is the same + as in \l hellovulkantriangle. In addition, this example demonstrates + another feature of QVulkanWindow: reading the image content back from the + color buffer into a QImage. By clicking the Grab button, the example + renders the next frame and follows it up with a transfer operation in order + to get the swapchain color buffer content copied into host accessible + memory. The image is then saved to disk via QImage::save(). + + \include examples-run.qdocinc +*/ diff --git a/examples/vulkan/doc/src/hellovulkanwindow.qdoc b/examples/vulkan/doc/src/hellovulkanwindow.qdoc new file mode 100644 index 00000000000..06cc9c1c288 --- /dev/null +++ b/examples/vulkan/doc/src/hellovulkanwindow.qdoc @@ -0,0 +1,101 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example hellovulkanwindow + \title Hello Vulkan Window Example + \ingroup examples-vulkan + \brief Shows the basics of using QVulkanWindow + + The \e{Hello Vulkan Window Example} shows the basics of using QVulkanWindow + in order to display rendering with the Vulkan graphics API on systems that + support this. + + \image hellovulkanwindow.png + + In this example there will be no actual rendering: it simply begins and + ends a render pass, which results in clearing the buffers to a fixed value. + The color buffer clear value changes on every frame. + + \section1 Startup + + Each Qt application using Vulkan will have to have a \c{Vulkan instance} + which encapsulates application-level state and initializes a Vulkan library. + + A QVulkanWindow must always be associated with a QVulkanInstance and hence + the example performs instance creation before the window. The + QVulkanInstance object must also outlive the window. + + \snippet hellovulkanwindow/main.cpp 0 + + The example enables validation layers, when supported. When the requested + layers are not present, the request will be ignored. Additional layers and + extensions can be enabled in a similar manner. + + \snippet hellovulkanwindow/main.cpp 1 + + Once the instance is ready, it is time to create a window. Note that \c w + lives on the stack and is declared after \c inst. + + \section1 The QVulkanWindow Subclass + + To add custom functionality to a QVulkanWindow, subclassing is used. This + follows the existing patterns from QOpenGLWindow and QOpenGLWidget. + However, QVulkanWindow utilizes a separate QVulkanWindowRenderer object. + This resembles QQuickFramebufferObject, and allows better separation of the + functions that are supposed to be reimplemented. + + \snippet hellovulkanwindow/hellovulkanwindow.h 0 + + The QVulkanWindow subclass reimplements the factory function + QVulkanWindow::createRenderer(). This simply returns a new instance of the + QVulkanWindowRenderer subclass. In order to be able to access various + Vulkan resources via the window object, a pointer to the window is passed + and stored via the constructor. + + \snippet hellovulkanwindow/hellovulkanwindow.cpp 0 + + Graphics resource creation and destruction is typically done in one of the + init - resource functions. + + \snippet hellovulkanwindow/hellovulkanwindow.cpp 1 + + \section1 The Actual Rendering + + QVulkanWindow subclasses queue their draw calls in their reimplementation + of QVulkanWindowRenderer::startNextFrame(). Once done, they are required to + call back QVulkanWindow::frameReady(). The example has no asynchronous + command generation, so the frameReady() call is made directly from + startNextFrame(). + + \snippet hellovulkanwindow/hellovulkanwindow.cpp 2 + + To get continuous updates, the example simply invokes + QWindow::requestUpdate() in order to schedule a repaint. + + \include examples-run.qdocinc +*/ diff --git a/examples/vulkan/hellovulkantexture/hellovulkantexture.cpp b/examples/vulkan/hellovulkantexture/hellovulkantexture.cpp new file mode 100644 index 00000000000..9953352e28c --- /dev/null +++ b/examples/vulkan/hellovulkantexture/hellovulkantexture.cpp @@ -0,0 +1,828 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include "hellovulkantexture.h" +#include +#include +#include + +// Use a triangle strip to get a quad. +// +// Note that the vertex data and the projection matrix assume OpenGL. With +// Vulkan Y is negated in clip space and the near/far plane is at 0/1 instead +// of -1/1. These will be corrected for by an extra transformation when +// calculating the modelview-projection matrix. +static float vertexData[] = { + // x, y, z, u, v + -1, -1, 0, 0, 1, + -1, 1, 0, 0, 0, + 1, -1, 0, 1, 1, + 1, 1, 0, 1, 0 +}; + +static const int UNIFORM_DATA_SIZE = 16 * sizeof(float); + +static inline VkDeviceSize aligned(VkDeviceSize v, VkDeviceSize byteAlign) +{ + return (v + byteAlign - 1) & ~(byteAlign - 1); +} + +QVulkanWindowRenderer *VulkanWindow::createRenderer() +{ + return new VulkanRenderer(this); +} + +VulkanRenderer::VulkanRenderer(QVulkanWindow *w) + : m_window(w) +{ +} + +VkShaderModule VulkanRenderer::createShader(const QString &name) +{ + QFile file(name); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("Failed to read shader %s", qPrintable(name)); + return VK_NULL_HANDLE; + } + QByteArray blob = file.readAll(); + file.close(); + + VkShaderModuleCreateInfo shaderInfo; + memset(&shaderInfo, 0, sizeof(shaderInfo)); + shaderInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + shaderInfo.codeSize = blob.size(); + shaderInfo.pCode = reinterpret_cast(blob.constData()); + VkShaderModule shaderModule; + VkResult err = m_devFuncs->vkCreateShaderModule(m_window->device(), &shaderInfo, nullptr, &shaderModule); + if (err != VK_SUCCESS) { + qWarning("Failed to create shader module: %d", err); + return VK_NULL_HANDLE; + } + + return shaderModule; +} + +bool VulkanRenderer::createTexture(const QString &name) +{ + QImage img(name); + if (img.isNull()) { + qWarning("Failed to load image %s", qPrintable(name)); + return false; + } + + // Convert to byte ordered RGBA8. Use premultiplied alpha, see pColorBlendState in the pipeline. + img = img.convertToFormat(QImage::Format_RGBA8888_Premultiplied); + + QVulkanFunctions *f = m_window->vulkanInstance()->functions(); + VkDevice dev = m_window->device(); + + const bool srgb = QCoreApplication::arguments().contains(QStringLiteral("--srgb")); + if (srgb) + qDebug("sRGB swapchain was requested, making texture sRGB too"); + + m_texFormat = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; + + // Now we can either map and copy the image data directly, or have to go + // through a staging buffer to copy and convert into the internal optimal + // tiling format. + VkFormatProperties props; + f->vkGetPhysicalDeviceFormatProperties(m_window->physicalDevice(), m_texFormat, &props); + const bool canSampleLinear = (props.linearTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT); + const bool canSampleOptimal = (props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT); + if (!canSampleLinear && !canSampleOptimal) { + qWarning("Neither linear nor optimal image sampling is supported for RGBA8"); + return false; + } + + static bool alwaysStage = qEnvironmentVariableIntValue("QT_VK_FORCE_STAGE_TEX"); + + if (canSampleLinear && !alwaysStage) { + if (!createTextureImage(img.size(), &m_texImage, &m_texMem, + VK_IMAGE_TILING_LINEAR, VK_IMAGE_USAGE_SAMPLED_BIT, + m_window->hostVisibleMemoryIndex())) + return false; + + if (!writeLinearImage(img, m_texImage, m_texMem)) + return false; + + m_texLayoutPending = true; + } else { + if (!createTextureImage(img.size(), &m_texStaging, &m_texStagingMem, + VK_IMAGE_TILING_LINEAR, VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + m_window->hostVisibleMemoryIndex())) + return false; + + if (!createTextureImage(img.size(), &m_texImage, &m_texMem, + VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + m_window->deviceLocalMemoryIndex())) + return false; + + if (!writeLinearImage(img, m_texStaging, m_texStagingMem)) + return false; + + m_texStagingPending = true; + } + + VkImageViewCreateInfo viewInfo; + memset(&viewInfo, 0, sizeof(viewInfo)); + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = m_texImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = m_texFormat; + viewInfo.components.r = VK_COMPONENT_SWIZZLE_R; + viewInfo.components.g = VK_COMPONENT_SWIZZLE_G; + viewInfo.components.b = VK_COMPONENT_SWIZZLE_B; + viewInfo.components.a = VK_COMPONENT_SWIZZLE_A; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.levelCount = viewInfo.subresourceRange.layerCount = 1; + + VkResult err = m_devFuncs->vkCreateImageView(dev, &viewInfo, nullptr, &m_texView); + if (err != VK_SUCCESS) { + qWarning("Failed to create image view for texture: %d", err); + return false; + } + + m_texSize = img.size(); + + return true; +} + +bool VulkanRenderer::createTextureImage(const QSize &size, VkImage *image, VkDeviceMemory *mem, + VkImageTiling tiling, VkImageUsageFlags usage, uint32_t memIndex) +{ + VkDevice dev = m_window->device(); + + VkImageCreateInfo imageInfo; + memset(&imageInfo, 0, sizeof(imageInfo)); + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = m_texFormat; + imageInfo.extent.width = size.width(); + imageInfo.extent.height = size.height(); + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = tiling; + imageInfo.usage = usage; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; + + VkResult err = m_devFuncs->vkCreateImage(dev, &imageInfo, nullptr, image); + if (err != VK_SUCCESS) { + qWarning("Failed to create linear image for texture: %d", err); + return false; + } + + VkMemoryRequirements memReq; + m_devFuncs->vkGetImageMemoryRequirements(dev, *image, &memReq); + + VkMemoryAllocateInfo allocInfo = { + VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + nullptr, + memReq.size, + memIndex + }; + qDebug("allocating %u bytes for texture image", uint32_t(memReq.size)); + + err = m_devFuncs->vkAllocateMemory(dev, &allocInfo, nullptr, mem); + if (err != VK_SUCCESS) { + qWarning("Failed to allocate memory for linear image: %d", err); + return false; + } + + err = m_devFuncs->vkBindImageMemory(dev, *image, *mem, 0); + if (err != VK_SUCCESS) { + qWarning("Failed to bind linear image memory: %d", err); + return false; + } + + return true; +} + +bool VulkanRenderer::writeLinearImage(const QImage &img, VkImage image, VkDeviceMemory memory) +{ + VkDevice dev = m_window->device(); + + VkImageSubresource subres = { + VK_IMAGE_ASPECT_COLOR_BIT, + 0, // mip level + 0 + }; + VkSubresourceLayout layout; + m_devFuncs->vkGetImageSubresourceLayout(dev, image, &subres, &layout); + + uchar *p; + VkResult err = m_devFuncs->vkMapMemory(dev, memory, layout.offset, layout.size, 0, reinterpret_cast(&p)); + if (err != VK_SUCCESS) { + qWarning("Failed to map memory for linear image: %d", err); + return false; + } + + for (int y = 0; y < img.height(); ++y) { + const uchar *line = img.constScanLine(y); + memcpy(p, line, img.width() * 4); + p += layout.rowPitch; + } + + m_devFuncs->vkUnmapMemory(dev, memory); + return true; +} + +void VulkanRenderer::ensureTexture() +{ + if (!m_texLayoutPending && !m_texStagingPending) + return; + + Q_ASSERT(m_texLayoutPending != m_texStagingPending); + VkCommandBuffer cb = m_window->currentCommandBuffer(); + + VkImageMemoryBarrier barrier; + memset(&barrier, 0, sizeof(barrier)); + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.levelCount = barrier.subresourceRange.layerCount = 1; + + if (m_texLayoutPending) { + m_texLayoutPending = false; + + barrier.oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.image = m_texImage; + + m_devFuncs->vkCmdPipelineBarrier(cb, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); + } else { + m_texStagingPending = false; + + barrier.oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.image = m_texStaging; + m_devFuncs->vkCmdPipelineBarrier(cb, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); + + barrier.oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.image = m_texImage; + m_devFuncs->vkCmdPipelineBarrier(cb, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); + + VkImageCopy copyInfo; + memset(©Info, 0, sizeof(copyInfo)); + copyInfo.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copyInfo.srcSubresource.layerCount = 1; + copyInfo.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copyInfo.dstSubresource.layerCount = 1; + copyInfo.extent.width = m_texSize.width(); + copyInfo.extent.height = m_texSize.height(); + copyInfo.extent.depth = 1; + m_devFuncs->vkCmdCopyImage(cb, m_texStaging, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_texImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Info); + + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.image = m_texImage; + m_devFuncs->vkCmdPipelineBarrier(cb, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); + } +} + +void VulkanRenderer::initResources() +{ + qDebug("initResources"); + + VkDevice dev = m_window->device(); + m_devFuncs = m_window->vulkanInstance()->deviceFunctions(dev); + + // The setup is similar to hellovulkantriangle. The difference is the + // presence of a second vertex attribute (texcoord), a sampler, and that we + // need blending. + + const int concurrentFrameCount = m_window->concurrentFrameCount(); + const VkPhysicalDeviceLimits *pdevLimits = &m_window->physicalDeviceProperties()->limits; + const VkDeviceSize uniAlign = pdevLimits->minUniformBufferOffsetAlignment; + qDebug("uniform buffer offset alignment is %u", (uint) uniAlign); + VkBufferCreateInfo bufInfo; + memset(&bufInfo, 0, sizeof(bufInfo)); + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + // Our internal layout is vertex, uniform, uniform, ... with each uniform buffer start offset aligned to uniAlign. + const VkDeviceSize vertexAllocSize = aligned(sizeof(vertexData), uniAlign); + const VkDeviceSize uniformAllocSize = aligned(UNIFORM_DATA_SIZE, uniAlign); + bufInfo.size = vertexAllocSize + concurrentFrameCount * uniformAllocSize; + bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VkResult err = m_devFuncs->vkCreateBuffer(dev, &bufInfo, nullptr, &m_buf); + if (err != VK_SUCCESS) + qFatal("Failed to create buffer: %d", err); + + VkMemoryRequirements memReq; + m_devFuncs->vkGetBufferMemoryRequirements(dev, m_buf, &memReq); + + VkMemoryAllocateInfo memAllocInfo = { + VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + nullptr, + memReq.size, + m_window->hostVisibleMemoryIndex() + }; + + err = m_devFuncs->vkAllocateMemory(dev, &memAllocInfo, nullptr, &m_bufMem); + if (err != VK_SUCCESS) + qFatal("Failed to allocate memory: %d", err); + + err = m_devFuncs->vkBindBufferMemory(dev, m_buf, m_bufMem, 0); + if (err != VK_SUCCESS) + qFatal("Failed to bind buffer memory: %d", err); + + quint8 *p; + err = m_devFuncs->vkMapMemory(dev, m_bufMem, 0, memReq.size, 0, reinterpret_cast(&p)); + if (err != VK_SUCCESS) + qFatal("Failed to map memory: %d", err); + memcpy(p, vertexData, sizeof(vertexData)); + QMatrix4x4 ident; + memset(m_uniformBufInfo, 0, sizeof(m_uniformBufInfo)); + for (int i = 0; i < concurrentFrameCount; ++i) { + const VkDeviceSize offset = vertexAllocSize + i * uniformAllocSize; + memcpy(p + offset, ident.constData(), 16 * sizeof(float)); + m_uniformBufInfo[i].buffer = m_buf; + m_uniformBufInfo[i].offset = offset; + m_uniformBufInfo[i].range = uniformAllocSize; + } + m_devFuncs->vkUnmapMemory(dev, m_bufMem); + + VkVertexInputBindingDescription vertexBindingDesc = { + 0, // binding + 5 * sizeof(float), + VK_VERTEX_INPUT_RATE_VERTEX + }; + VkVertexInputAttributeDescription vertexAttrDesc[] = { + { // position + 0, // location + 0, // binding + VK_FORMAT_R32G32B32_SFLOAT, + 0 + }, + { // texcoord + 1, + 0, + VK_FORMAT_R32G32_SFLOAT, + 3 * sizeof(float) + } + }; + + VkPipelineVertexInputStateCreateInfo vertexInputInfo; + vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertexInputInfo.pNext = nullptr; + vertexInputInfo.flags = 0; + vertexInputInfo.vertexBindingDescriptionCount = 1; + vertexInputInfo.pVertexBindingDescriptions = &vertexBindingDesc; + vertexInputInfo.vertexAttributeDescriptionCount = 2; + vertexInputInfo.pVertexAttributeDescriptions = vertexAttrDesc; + + // Sampler. + VkSamplerCreateInfo samplerInfo; + memset(&samplerInfo, 0, sizeof(samplerInfo)); + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + err = m_devFuncs->vkCreateSampler(dev, &samplerInfo, nullptr, &m_sampler); + if (err != VK_SUCCESS) + qFatal("Failed to create sampler: %d", err); + + // Texture. + if (!createTexture(QStringLiteral(":/qt256.png"))) + qFatal("Failed to create texture"); + + // Set up descriptor set and its layout. + VkDescriptorPoolSize descPoolSizes[2] = { + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, uint32_t(concurrentFrameCount) }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, uint32_t(concurrentFrameCount) } + }; + VkDescriptorPoolCreateInfo descPoolInfo; + memset(&descPoolInfo, 0, sizeof(descPoolInfo)); + descPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + descPoolInfo.maxSets = concurrentFrameCount; + descPoolInfo.poolSizeCount = 2; + descPoolInfo.pPoolSizes = descPoolSizes; + err = m_devFuncs->vkCreateDescriptorPool(dev, &descPoolInfo, nullptr, &m_descPool); + if (err != VK_SUCCESS) + qFatal("Failed to create descriptor pool: %d", err); + + VkDescriptorSetLayoutBinding layoutBinding[2] = + { + { + 0, // binding + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + 1, // descriptorCount + VK_SHADER_STAGE_VERTEX_BIT, + nullptr + }, + { + 1, // binding + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + 1, // descriptorCount + VK_SHADER_STAGE_FRAGMENT_BIT, + nullptr + } + }; + VkDescriptorSetLayoutCreateInfo descLayoutInfo = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + nullptr, + 0, + 2, // bindingCount + layoutBinding + }; + err = m_devFuncs->vkCreateDescriptorSetLayout(dev, &descLayoutInfo, nullptr, &m_descSetLayout); + if (err != VK_SUCCESS) + qFatal("Failed to create descriptor set layout: %d", err); + + for (int i = 0; i < concurrentFrameCount; ++i) { + VkDescriptorSetAllocateInfo descSetAllocInfo = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + nullptr, + m_descPool, + 1, + &m_descSetLayout + }; + err = m_devFuncs->vkAllocateDescriptorSets(dev, &descSetAllocInfo, &m_descSet[i]); + if (err != VK_SUCCESS) + qFatal("Failed to allocate descriptor set: %d", err); + + VkWriteDescriptorSet descWrite[2]; + memset(descWrite, 0, sizeof(descWrite)); + descWrite[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descWrite[0].dstSet = m_descSet[i]; + descWrite[0].dstBinding = 0; + descWrite[0].descriptorCount = 1; + descWrite[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descWrite[0].pBufferInfo = &m_uniformBufInfo[i]; + + VkDescriptorImageInfo descImageInfo = { + m_sampler, + m_texView, + VK_IMAGE_LAYOUT_GENERAL + }; + + descWrite[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descWrite[1].dstSet = m_descSet[i]; + descWrite[1].dstBinding = 1; + descWrite[1].descriptorCount = 1; + descWrite[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + descWrite[1].pImageInfo = &descImageInfo; + + m_devFuncs->vkUpdateDescriptorSets(dev, 2, descWrite, 0, nullptr); + } + + // Pipeline cache + VkPipelineCacheCreateInfo pipelineCacheInfo; + memset(&pipelineCacheInfo, 0, sizeof(pipelineCacheInfo)); + pipelineCacheInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + err = m_devFuncs->vkCreatePipelineCache(dev, &pipelineCacheInfo, nullptr, &m_pipelineCache); + if (err != VK_SUCCESS) + qFatal("Failed to create pipeline cache: %d", err); + + // Pipeline layout + VkPipelineLayoutCreateInfo pipelineLayoutInfo; + memset(&pipelineLayoutInfo, 0, sizeof(pipelineLayoutInfo)); + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &m_descSetLayout; + err = m_devFuncs->vkCreatePipelineLayout(dev, &pipelineLayoutInfo, nullptr, &m_pipelineLayout); + if (err != VK_SUCCESS) + qFatal("Failed to create pipeline layout: %d", err); + + // Shaders + VkShaderModule vertShaderModule = createShader(QStringLiteral(":/texture_vert.spv")); + VkShaderModule fragShaderModule = createShader(QStringLiteral(":/texture_frag.spv")); + + // Graphics pipeline + VkGraphicsPipelineCreateInfo pipelineInfo; + memset(&pipelineInfo, 0, sizeof(pipelineInfo)); + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + + VkPipelineShaderStageCreateInfo shaderStages[2] = { + { + VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + nullptr, + 0, + VK_SHADER_STAGE_VERTEX_BIT, + vertShaderModule, + "main", + nullptr + }, + { + VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + nullptr, + 0, + VK_SHADER_STAGE_FRAGMENT_BIT, + fragShaderModule, + "main", + nullptr + } + }; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = shaderStages; + + pipelineInfo.pVertexInputState = &vertexInputInfo; + + VkPipelineInputAssemblyStateCreateInfo ia; + memset(&ia, 0, sizeof(ia)); + ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + pipelineInfo.pInputAssemblyState = &ia; + + // The viewport and scissor will be set dynamically via vkCmdSetViewport/Scissor. + // This way the pipeline does not need to be touched when resizing the window. + VkPipelineViewportStateCreateInfo vp; + memset(&vp, 0, sizeof(vp)); + vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + vp.viewportCount = 1; + vp.scissorCount = 1; + pipelineInfo.pViewportState = &vp; + + VkPipelineRasterizationStateCreateInfo rs; + memset(&rs, 0, sizeof(rs)); + rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rs.polygonMode = VK_POLYGON_MODE_FILL; + rs.cullMode = VK_CULL_MODE_NONE; // we want the back face as well + rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rs.lineWidth = 1.0f; + pipelineInfo.pRasterizationState = &rs; + + VkPipelineMultisampleStateCreateInfo ms; + memset(&ms, 0, sizeof(ms)); + ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + pipelineInfo.pMultisampleState = &ms; + + VkPipelineDepthStencilStateCreateInfo ds; + memset(&ds, 0, sizeof(ds)); + ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + ds.depthTestEnable = VK_TRUE; + ds.depthWriteEnable = VK_TRUE; + ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; + pipelineInfo.pDepthStencilState = &ds; + + VkPipelineColorBlendStateCreateInfo cb; + memset(&cb, 0, sizeof(cb)); + cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + // assume pre-multiplied alpha, blend, write out all of rgba + VkPipelineColorBlendAttachmentState att; + memset(&att, 0, sizeof(att)); + att.colorWriteMask = 0xF; + att.blendEnable = VK_TRUE; + att.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + att.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + att.colorBlendOp = VK_BLEND_OP_ADD; + att.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + att.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + att.alphaBlendOp = VK_BLEND_OP_ADD; + cb.attachmentCount = 1; + cb.pAttachments = &att; + pipelineInfo.pColorBlendState = &cb; + + VkDynamicState dynEnable[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dyn; + memset(&dyn, 0, sizeof(dyn)); + dyn.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dyn.dynamicStateCount = sizeof(dynEnable) / sizeof(VkDynamicState); + dyn.pDynamicStates = dynEnable; + pipelineInfo.pDynamicState = &dyn; + + pipelineInfo.layout = m_pipelineLayout; + pipelineInfo.renderPass = m_window->defaultRenderPass(); + + err = m_devFuncs->vkCreateGraphicsPipelines(dev, m_pipelineCache, 1, &pipelineInfo, nullptr, &m_pipeline); + if (err != VK_SUCCESS) + qFatal("Failed to create graphics pipeline: %d", err); + + if (vertShaderModule) + m_devFuncs->vkDestroyShaderModule(dev, vertShaderModule, nullptr); + if (fragShaderModule) + m_devFuncs->vkDestroyShaderModule(dev, fragShaderModule, nullptr); +} + +void VulkanRenderer::initSwapChainResources() +{ + qDebug("initSwapChainResources"); + + // Projection matrix + m_proj = *m_window->clipCorrectionMatrix(); // adjust for Vulkan-OpenGL clip space differences + const QSize sz = m_window->swapChainImageSize(); + m_proj.perspective(45.0f, sz.width() / (float) sz.height(), 0.01f, 100.0f); + m_proj.translate(0, 0, -4); +} + +void VulkanRenderer::releaseSwapChainResources() +{ + qDebug("releaseSwapChainResources"); +} + +void VulkanRenderer::releaseResources() +{ + qDebug("releaseResources"); + + VkDevice dev = m_window->device(); + + if (m_sampler) { + m_devFuncs->vkDestroySampler(dev, m_sampler, nullptr); + m_sampler = VK_NULL_HANDLE; + } + + if (m_texStaging) { + m_devFuncs->vkDestroyImage(dev, m_texStaging, nullptr); + m_texStaging = VK_NULL_HANDLE; + } + + if (m_texStagingMem) { + m_devFuncs->vkFreeMemory(dev, m_texStagingMem, nullptr); + m_texStagingMem = VK_NULL_HANDLE; + } + + if (m_texView) { + m_devFuncs->vkDestroyImageView(dev, m_texView, nullptr); + m_texView = VK_NULL_HANDLE; + } + + if (m_texImage) { + m_devFuncs->vkDestroyImage(dev, m_texImage, nullptr); + m_texImage = VK_NULL_HANDLE; + } + + if (m_texMem) { + m_devFuncs->vkFreeMemory(dev, m_texMem, nullptr); + m_texMem = VK_NULL_HANDLE; + } + + if (m_pipeline) { + m_devFuncs->vkDestroyPipeline(dev, m_pipeline, nullptr); + m_pipeline = VK_NULL_HANDLE; + } + + if (m_pipelineLayout) { + m_devFuncs->vkDestroyPipelineLayout(dev, m_pipelineLayout, nullptr); + m_pipelineLayout = VK_NULL_HANDLE; + } + + if (m_pipelineCache) { + m_devFuncs->vkDestroyPipelineCache(dev, m_pipelineCache, nullptr); + m_pipelineCache = VK_NULL_HANDLE; + } + + if (m_descSetLayout) { + m_devFuncs->vkDestroyDescriptorSetLayout(dev, m_descSetLayout, nullptr); + m_descSetLayout = VK_NULL_HANDLE; + } + + if (m_descPool) { + m_devFuncs->vkDestroyDescriptorPool(dev, m_descPool, nullptr); + m_descPool = VK_NULL_HANDLE; + } + + if (m_buf) { + m_devFuncs->vkDestroyBuffer(dev, m_buf, nullptr); + m_buf = VK_NULL_HANDLE; + } + + if (m_bufMem) { + m_devFuncs->vkFreeMemory(dev, m_bufMem, nullptr); + m_bufMem = VK_NULL_HANDLE; + } +} + +void VulkanRenderer::startNextFrame() +{ + VkDevice dev = m_window->device(); + VkCommandBuffer cb = m_window->currentCommandBuffer(); + const QSize sz = m_window->swapChainImageSize(); + + // Add the necessary barriers and do the host-linear -> device-optimal copy, if not yet done. + ensureTexture(); + + VkClearColorValue clearColor = { 0, 0, 0, 1 }; + VkClearDepthStencilValue clearDS = { 1, 0 }; + VkClearValue clearValues[2]; + memset(clearValues, 0, sizeof(clearValues)); + clearValues[0].color = clearColor; + clearValues[1].depthStencil = clearDS; + + VkRenderPassBeginInfo rpBeginInfo; + memset(&rpBeginInfo, 0, sizeof(rpBeginInfo)); + rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBeginInfo.renderPass = m_window->defaultRenderPass(); + rpBeginInfo.framebuffer = m_window->currentFramebuffer(); + rpBeginInfo.renderArea.extent.width = sz.width(); + rpBeginInfo.renderArea.extent.height = sz.height(); + rpBeginInfo.clearValueCount = 2; + rpBeginInfo.pClearValues = clearValues; + VkCommandBuffer cmdBuf = m_window->currentCommandBuffer(); + m_devFuncs->vkCmdBeginRenderPass(cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + + quint8 *p; + VkResult err = m_devFuncs->vkMapMemory(dev, m_bufMem, m_uniformBufInfo[m_window->currentFrame()].offset, + UNIFORM_DATA_SIZE, 0, reinterpret_cast(&p)); + if (err != VK_SUCCESS) + qFatal("Failed to map memory: %d", err); + QMatrix4x4 m = m_proj; + m.rotate(m_rotation, 0, 0, 1); + memcpy(p, m.constData(), 16 * sizeof(float)); + m_devFuncs->vkUnmapMemory(dev, m_bufMem); + + // Not exactly a real animation system, just advance on every frame for now. + m_rotation += 1.0f; + + m_devFuncs->vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline); + m_devFuncs->vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, + &m_descSet[m_window->currentFrame()], 0, nullptr); + VkDeviceSize vbOffset = 0; + m_devFuncs->vkCmdBindVertexBuffers(cb, 0, 1, &m_buf, &vbOffset); + + VkViewport viewport; + viewport.x = viewport.y = 0; + viewport.width = sz.width(); + viewport.height = sz.height(); + viewport.minDepth = 0; + viewport.maxDepth = 1; + m_devFuncs->vkCmdSetViewport(cb, 0, 1, &viewport); + + VkRect2D scissor; + scissor.offset.x = scissor.offset.y = 0; + scissor.extent.width = viewport.width; + scissor.extent.height = viewport.height; + m_devFuncs->vkCmdSetScissor(cb, 0, 1, &scissor); + + m_devFuncs->vkCmdDraw(cb, 4, 1, 0, 0); + + m_devFuncs->vkCmdEndRenderPass(cmdBuf); + + m_window->frameReady(); + m_window->requestUpdate(); // render continuously, throttled by the presentation rate +} diff --git a/examples/vulkan/hellovulkantexture/hellovulkantexture.h b/examples/vulkan/hellovulkantexture/hellovulkantexture.h new file mode 100644 index 00000000000..a8c96d19870 --- /dev/null +++ b/examples/vulkan/hellovulkantexture/hellovulkantexture.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include +#include + +class VulkanRenderer : public QVulkanWindowRenderer +{ +public: + VulkanRenderer(QVulkanWindow *w); + + void initResources() override; + void initSwapChainResources() override; + void releaseSwapChainResources() override; + void releaseResources() override; + + void startNextFrame() override; + +private: + VkShaderModule createShader(const QString &name); + bool createTexture(const QString &name); + bool createTextureImage(const QSize &size, VkImage *image, VkDeviceMemory *mem, + VkImageTiling tiling, VkImageUsageFlags usage, uint32_t memIndex); + bool writeLinearImage(const QImage &img, VkImage image, VkDeviceMemory memory); + void ensureTexture(); + + QVulkanWindow *m_window; + QVulkanDeviceFunctions *m_devFuncs; + + VkDeviceMemory m_bufMem = VK_NULL_HANDLE; + VkBuffer m_buf = VK_NULL_HANDLE; + VkDescriptorBufferInfo m_uniformBufInfo[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT]; + + VkDescriptorPool m_descPool = VK_NULL_HANDLE; + VkDescriptorSetLayout m_descSetLayout = VK_NULL_HANDLE; + VkDescriptorSet m_descSet[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT]; + + VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; + VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; + VkPipeline m_pipeline = VK_NULL_HANDLE; + + VkSampler m_sampler = VK_NULL_HANDLE; + VkImage m_texImage = VK_NULL_HANDLE; + VkDeviceMemory m_texMem = VK_NULL_HANDLE; + bool m_texLayoutPending = false; + VkImageView m_texView = VK_NULL_HANDLE; + VkImage m_texStaging = VK_NULL_HANDLE; + VkDeviceMemory m_texStagingMem = VK_NULL_HANDLE; + bool m_texStagingPending = false; + QSize m_texSize; + VkFormat m_texFormat; + + QMatrix4x4 m_proj; + float m_rotation = 0.0f; +}; + +class VulkanWindow : public QVulkanWindow +{ +public: + QVulkanWindowRenderer *createRenderer() override; +}; diff --git a/examples/vulkan/hellovulkantexture/hellovulkantexture.pro b/examples/vulkan/hellovulkantexture/hellovulkantexture.pro new file mode 100644 index 00000000000..59bfcda7157 --- /dev/null +++ b/examples/vulkan/hellovulkantexture/hellovulkantexture.pro @@ -0,0 +1,7 @@ +HEADERS += hellovulkantexture.h +SOURCES += hellovulkantexture.cpp main.cpp +RESOURCES += hellovulkantexture.qrc + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/vulkan/hellovulkantexture +INSTALLS += target diff --git a/examples/vulkan/hellovulkantexture/hellovulkantexture.qrc b/examples/vulkan/hellovulkantexture/hellovulkantexture.qrc new file mode 100644 index 00000000000..04e7cda8599 --- /dev/null +++ b/examples/vulkan/hellovulkantexture/hellovulkantexture.qrc @@ -0,0 +1,7 @@ + + + texture_vert.spv + texture_frag.spv + qt256.png + + diff --git a/examples/vulkan/hellovulkantexture/main.cpp b/examples/vulkan/hellovulkantexture/main.cpp new file mode 100644 index 00000000000..1144463b70d --- /dev/null +++ b/examples/vulkan/hellovulkantexture/main.cpp @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include +#include +#include +#include "hellovulkantexture.h" + +Q_LOGGING_CATEGORY(lcVk, "qt.vulkan") + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true")); + + QVulkanInstance inst; + +#ifndef Q_OS_ANDROID + inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation"); +#else + inst.setLayers(QByteArrayList() + << "VK_LAYER_GOOGLE_threading" + << "VK_LAYER_LUNARG_parameter_validation" + << "VK_LAYER_LUNARG_object_tracker" + << "VK_LAYER_LUNARG_core_validation" + << "VK_LAYER_LUNARG_image" + << "VK_LAYER_LUNARG_swapchain" + << "VK_LAYER_GOOGLE_unique_objects"); +#endif + + if (!inst.create()) + qFatal("Failed to create Vulkan instance: %d", inst.errorCode()); + + VulkanWindow w; + w.setVulkanInstance(&inst); + if (QCoreApplication::arguments().contains(QStringLiteral("--srgb"))) + w.setPreferredColorFormats(QVector() << VK_FORMAT_B8G8R8A8_SRGB); + + w.resize(1024, 768); + w.show(); + + return app.exec(); +} diff --git a/examples/vulkan/hellovulkantexture/qt256.png b/examples/vulkan/hellovulkantexture/qt256.png new file mode 100644 index 0000000000000000000000000000000000000000..30c621c9c669457e8fb338a79d25a86501916de1 GIT binary patch literal 6208 zcmcIoc|4Tu*S`tb5sqpy7(Sg#a0NJG&vw5YDbd-%f-Y z0GzmjvarTm!;Ms2Fy2zm$2?L2-ab@m08oPl_&B?GBJiMF2sacOBDhxHEC@omLIkbA za2dFd7Q!8+e;bQ1zin*ca@*5I*;Np#4pIwHp#pd#@XnwBZ!a`XB>*D$2d)a0J|319 z1pUE+_k;-kODSvkb&wVYivWS86eL|_ugZZGm8E3C%5qBb5+FGlSp{hsd1+Z?N!hC^ z)LUK#^yei=oek@XR58=m{c|qr8${3@kM~iLmL?DgQiQ8g7_1vrR#{nEMowBzPLj$Y zi6f%%&H<8WoX}qg+6bHr7UhFSVbGvsMCV%=KRiT`s_DN>@b>u|7LEJUO;m?T2RQpk z%Sy={oAehW9R9zvdVBxPjl-KE{#)OF3yiZM`XHpu5IBq<)`hxoNTK7Yd{nfs2xmM7 zYk|Rd{pH2$?if4<=Z^6KX=xpM4J-u)U4}cmpwP#|Vt>fN;VK4b9Nro2f-umA2vU)x zP$*Xw9bI`HSzWL$SVl`hR#rzrNm*M-Q%PP%39O;5ET=2~m#j9%#m^go#{VVj`nRlt z%s34lOTqaI5QZ;vxWLjb^8W}vNM5iq`z#gt|_Oe8ml{0a^`m*Sl2 z&93pd24L`c9><{hmx&3pN>U2n)k#M?YoZ8*sz(l-@!nITos)>r4cDIF)f zYs8paT5!%zVDlpnCRta8rjR+QHP!y3`(N|sn<*1}jg(AW|BkURZ!9x#H&LnNk5s1x z06G9*1ORpb0096D0LavgrPhG|(1&T0-+px6k@)D-$I1KIhWSE{At&W%bL5>GKLJGmRyrrir6`ixaqWZnwxi z=`+C*V&w5)LkFuy3&Lu0Nu0;n&`qiQzWL1&>C6zTYlyHcfblR)=&HI_ftzuB@uCh^ zTQ~mxHGauXUjx~r&voStJ7tJ_ULNCu=N~C_r_A+dj%X?J(6QM9TXZ37XTPa+&d>M7 zE05`Q%Zn~-6+_><$9$QLpv?Bx`E4K%R2CN&ziDdZ(6Q(9vVG8t?(iK85pbK6PO`Dl zs|^X{;x`FEtl8R_ZiTLRP4>P$LFeYL_{8_EmX+c@;_r?Cb*i*3=I}-*)5xC7l zdm|VS?Ck$bqSmABZ4>MM0kM!2HsM&IZ8+Xt^MVYpwN1CF)sj9WqZ6U}cwN=m9~-k9 z);k!8s#6h0|H75Jy@%a9-m{w(Jv*>9{aj{J|3s$CH@&>__@2A>Te9qK37bk?@NeRK z1kqakNUYwo9q?>ZhH52joUYB3p0|NvmLV%}(ENO0t8`KbAHf_H7UFm*KQ{H`rK6ah zp3L^F(~Ka21W@&Zdeg((3(Aj`-GVg_z4o6~$Y9h7(7RRu?>>~DBBZQ7|8d>nr+(&s zK+jZX`c5G`4I?PC_t380{(Q}wrY7w7j&8)wRj(%i&D|Jr=A(%7N8%heMa>6A&oD89 z_}mkZK56yMbcGCaMW=phpS(QA)We_$(CAHVPRZ}?*>4?mnD7ZJ#x-R`Gk!e*G7=4J zEh-g(sC&bj?06QF{48p&1%C=uc?ua{Jsjb>yXcEb__p77t;csN{B+yx6AsgOv(JjI ze&3h1>1i1+h<@u!cBydt*-bz>HBW8F2TfG!R8@H;CRE84FHS6%Q;s&_8+7MV>_K_v zx=O*D26951jOl6q*3Enru#i-I_3~)FI4cbeUBtH<*II0wj_gr3g;zc42Kaf0sxf;p z9UDV0tHp%n66tf19p%R8Y+J+E*`7CrO4ze!UVR^WA?lesAbx&We-}1KqTp@%f+x6j4R)`SN1m6G*m7|D<=|w>}3-T1tBI zLopmK?vf@u89IQ!IdqjxQQRc#FwTK+&C}$OXup+t2*hsd$&NL((1s`+ZO&z+R5y4e zdwK3m49)L5ExJ@xzv?S%NVlOnh=vRVz1!!jA12iyM)PL7)k6AI*j`j(^KFPKBFgu7 z&alIPj3XCwy>#`1<$B&A-_`L`@3$B%dpgr~lBn>$M!$?+Rn+B8ZWzs8naZq>GcA+h zV^O+-D^X464K)B2(q8nu3{3i$w!59jHd*YI*vqe|C06{rkt?M?IGPizk$9hTOJE>! zxG8$Kah^L9eX~%~n6ea}0cLF36y-E{SiG3n+Z8x)kEgM$@pp%)qDLvVxft`^E5M{K z+&rG5_^Kif4;`_z|MAN3L?Xzl~47$0EZp%~F6ck`?z1 zZq=PaH+qYe|0s453HJr)rc=4eT+6)b>)wzB?9D6zcWk2ejH~9j(i3``fSgX0KkeDb zWKxRtg%FPPFb?wQKsYqRs-Jce+Kh_-+eK#`a zjYPy=Ewh6GFF9acms_WQGy^|}lZRQ`2gHO#)X$i-fq*r+D+oGC^1*hGJ5Ayxrk5X^ z&a=aME*o7yT8{8|i`*$noQ#;4{rv@rh9>Q*+}k;ZVGG9;77qqXAU~T^0tA@jnP1!x z?4J2G*kV5oiI|rYM#*r50SyLC&fK38U`)9a<#8MU?fMn^h!Ymc{NOvaEjO~j$i0)C zAaV;w+Az5`%D=dDU!BI?Q9jk#!L898dQV-WMA|Nrr~11u!D#6qHqEJmkar0^If;gn zOZ_4NH6vtDfsM`U2;x=)%-bp}3aFU5di!8%iA{HRQL>KMjT;KVb-XK@+VL4PR~v5~ zq3yqU5)dHBqH-=Wp zo{E`cwL!t>a(8i+{?!aig4Mr1W|<&0FPT!!)3XgKF;M1*WHt5D{w#Et<+Mez`r9(l z`E*!&(|jHr;;o7CA{txnaMREPB>c+$)skhdXL6E~Bp=$sPDJHUPGDcC{G?g0C-gMFXHRc|!GdCdQUPYxtB11p)#t4vq{V&5S+l z4cF@cj)FB>2lD$WK;j*cu|Nv~qz6$yccmoBYqdi&Lp@!$9B63Qg)PI(EzP4LB0wt| zuE$9O0!|M)rM(q(Kcf#;%PEkm0~lMv_ps7tRndddB>|pqO2TCaKygBl$*}nuH8bjSR6U%T={B z>MY$f+%|B$r|Ktfx||w}_C=!2jgz!6;6lNZ)&edWVKJzTSUaNs$`B=h>L5Tdyjj2SvA= z)Xz|>vh|XCVugnaMUIJ)Cg4%O9bt&}M{70heC4EK{lsF=PqC@M^45*isYo5KJB5p{IsTbHF!($WY%=O%KO6U(1}{)thZNm zBQSoy3Q=uD3I&3?>LO0}bY)mU-CT^9{n6WHrdG6la9N%|k10gaEY%fWA zcwEs1jQAL0WGft!9I`})bB$2;74O#>9=;)Nh@Qou%0HDoE4y7h)!%vL>qDgGVduwX zc(t|`pgtS5!G*kbmm;DpWk8bI2*e(J)4{sVeL3(~|6%-Md8O@M;lAb}cD++}C+=Q% zj(=j=FGW>bLV9w(c?}QGf&JWUpMN;uXyW=N0{27%YH-RT*!EpQZ*PvWL)=lyYrnNL z?;B0h^af$G-x`S6_UZ6d=)35IUuL!VO+VRq{0I zVKIFr0$(V?b;zcW|_@Uk8z3Wl^SuMmmJZr6Pv~CmVPC> zD5tv_?rKvuTTQLH+S^SgI`toARM%WxWJ6}xug+Z+2~IGA=SU1?akoDubzR3}euSOi z-j6Py<4bT82eNvJu5%U?+zUGI){objV1&wp8;if8uYu;cmeiDyNH_ty7pFt56gAI9 z{OU@XANQhedFC$m79Tz65)&6_4c=WW)h(^v5B@S%!h_I)_Uu8~>oPW}T_1#S8s!Pl zY@cnc^~+d+Nv!qyW?$G%U-_b)H?DTGVLqM)8b>*=zTtX6cLVTs<{!rWo_*$Ru~Am+^$Oa{PmK>7IUemBgG!F*pln`N=two z9Po;iwO{hnSMca)n=;v)RJ}B{`9bzsxr1j4=&mI|yDfA<((zLFm7LST>r>_p)+DRO zaooH7=SwM8&o(A}cO+$5X_Wv<>b`AunKefjZnXOJQN!|0A{s#MZE-g$Z0XiRhNy}- zexeMUt~qi-g&_mk?=uNnR(TVZ2Zh-yZfh~ddWSo!C)okoNXhXJ4FgP(hlYXRhYMa+ z-PIGIiwdNfn6JTk=qxq+U-HK$>HOwsUp0HL0=ahn4n!~bZK1hMsfuTi-^A%uh{)*_ z^$6gW_Wnol7_=UZ;CBfB#_dVfRf`V>i(eTFekDks+-$pOIzNm0w2Xe_TNfn}ac_C#UGyFVtyj*39KM)mITUJII<9dzU zAJ9=fka{vFS)gh{ymP&bL2aSSmxK?L={;GrN5|kS7^(iId~DZ+!2aR6`$i&B`4!?z z?v9*f-eqKWaJTA}Ns%s9J3@C{ibDmpd3q$qM059{MiddNxynb~8d)o=xgwEdR2lBaoK!8bL_)ynn93gqokE2dP zg>|T%qeUb4kqLD}Hm4R`P!ki=uHrxsWzxW`#3&wt-ec4)Sa;$o{Uk%|h5H=Vrue_d zkE|-7zN33j__XWFJ#+Di6wFFr)p-Htn;hn7$A11UCS%jr!t+2mUOungZ$~7r4 zn~$=XXnM!LpOK=j6W5vTe7i?wnz%QA)b&;iU<8dUjFYl-OIQpmn=_WwVrPVoju--8)tjwo6+C6R^@FON$WyD&a0;@Oq;`~x`%T!&hzHA0*oZOX&m0P z|3KtgPdDU2EzM)<2s?0da<9P4y>im{S~X}CZk&FeePx=unf>y4GG>zNZpuPmTjA-O z`m+p5+Z-_1dphn1snUwll<42rYCa`t$%I{`p^JPco?>7V7{0h2I;YDzl+EPKLK3-C z$IL)&5V4paA)QZtK$V|jslQ=Yo8irhiX{YU30-jO1m8X*$f?Y88+KT@>Ybj7o6ztR z$}SJEyjenotK?-_AQ?lc=nwZ|Qac80^tMz2A0nAZ6+z*T1bh0^Lm#ibc|c9u)&0Es zMf)28KWL8Xw@#g(jf$S#(D?e!!dCxSwIoDxb7DAg^T=K90oy{OB_(CAZM3+Jef8MH z+3pn$3cO+H!0sfY)o@u#K3Bhijh+Y>Ctpccf0Xh1#h*qpbnMshET#F$;`z_&;n~Hu zvvVBe7WyM5>Q}Riab+!lz$sFA6t%B9MWO@PXQ;4%0Na0-6gd-{z|IINdr$kHKeGEb fr+-Sok$4Nh7btU1@5BdBfO;9|7;BenI)(lhGcL*Y literal 0 HcmV?d00001 diff --git a/examples/vulkan/hellovulkantexture/texture.frag b/examples/vulkan/hellovulkantexture/texture.frag new file mode 100644 index 00000000000..e6021fe9054 --- /dev/null +++ b/examples/vulkan/hellovulkantexture/texture.frag @@ -0,0 +1,12 @@ +#version 440 + +layout(location = 0) in vec2 v_texcoord; + +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D tex; + +void main() +{ + fragColor = texture(tex, v_texcoord); +} diff --git a/examples/vulkan/hellovulkantexture/texture.vert b/examples/vulkan/hellovulkantexture/texture.vert new file mode 100644 index 00000000000..de486cb7720 --- /dev/null +++ b/examples/vulkan/hellovulkantexture/texture.vert @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec4 position; +layout(location = 1) in vec2 texcoord; + +layout(location = 0) out vec2 v_texcoord; + +layout(std140, binding = 0) uniform buf { + mat4 mvp; +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() +{ + v_texcoord = texcoord; + gl_Position = ubuf.mvp * position; +} diff --git a/examples/vulkan/hellovulkantexture/texture_frag.spv b/examples/vulkan/hellovulkantexture/texture_frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..7521ef6eef30b34c59cc2126de9f3c3246fc7a82 GIT binary patch literal 556 zcmYk2&r3p45QQgSPc6$I8ASx4wU-tpLe!>(8+R>QhGa!hJ$OmAH9+O8OlAHg*2+NuQ^AA9tYpq9|t(V+89^?*ivq@F& zd#ka;hbS8ysB_-dLL}!ru{#*P-=aECpX%FGXUVGfi7H@P2P7?Hmn9UB4w literal 0 HcmV?d00001 diff --git a/examples/vulkan/hellovulkantexture/texture_vert.spv b/examples/vulkan/hellovulkantexture/texture_vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..6292c0de310a9e457964a205047a2ea36f2e5125 GIT binary patch literal 968 zcmYk4$x1^(5Jfw~i$k2^Je!!M5L~DTqTnhnTnK{O;1DICMl#`V_&0u<8^Lord1^f> zRrhxFt?KUNvemH=GJNBq9EMg3S#Su&P>8X9d3{;y_8w{nhx=B{gj_7NW;RU3JkRgT zZR=GaQ^e=+4Lrw}C4RCY33L8I4vzi+oY57s1c{~se)pl=9I&yA~Q=cd_V z5ePfv&0TAETfJ8MmHJ%lFK@m`yz4(wosYTv%ixW*d{`#$yNKD_ncq?&@;WunxQ%OK z&nNZ{F4*I2n!Hx0r<~`$_}4RuIQgBS3f9zcMvVuFv#TMehCQ8g7uQl;8N!I2JDqWX zUGA%(;cJ;MlNE?%d<8xGh+Qj#$BEX;pNz2QmUDfQn8DL^vFDXjZUoFG<@yotqKGgiGX@i`UtUB#Wnvsmv8Yq-4F9-FxR bKiDJQ!mW{4$9Z;e?@Jx~h^_l(>SO!|V5~F~ literal 0 HcmV?d00001 diff --git a/examples/vulkan/hellovulkantriangle/hellovulkantriangle.pro b/examples/vulkan/hellovulkantriangle/hellovulkantriangle.pro new file mode 100644 index 00000000000..db016da3aca --- /dev/null +++ b/examples/vulkan/hellovulkantriangle/hellovulkantriangle.pro @@ -0,0 +1,12 @@ +HEADERS += \ + ../shared/trianglerenderer.h + +SOURCES += \ + main.cpp \ + ../shared/trianglerenderer.cpp + +RESOURCES += hellovulkantriangle.qrc + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/vulkan/hellovulkantriangle +INSTALLS += target diff --git a/examples/vulkan/hellovulkantriangle/hellovulkantriangle.qrc b/examples/vulkan/hellovulkantriangle/hellovulkantriangle.qrc new file mode 100644 index 00000000000..489fc7295a2 --- /dev/null +++ b/examples/vulkan/hellovulkantriangle/hellovulkantriangle.qrc @@ -0,0 +1,6 @@ + + + ../shared/color_vert.spv + ../shared/color_frag.spv + + diff --git a/examples/vulkan/hellovulkantriangle/main.cpp b/examples/vulkan/hellovulkantriangle/main.cpp new file mode 100644 index 00000000000..d3eef2e14a2 --- /dev/null +++ b/examples/vulkan/hellovulkantriangle/main.cpp @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include +#include +#include +#include "../shared/trianglerenderer.h" + +Q_LOGGING_CATEGORY(lcVk, "qt.vulkan") + +class VulkanWindow : public QVulkanWindow +{ +public: + QVulkanWindowRenderer *createRenderer() override; +}; + +QVulkanWindowRenderer *VulkanWindow::createRenderer() +{ + return new TriangleRenderer(this, true); // try MSAA, when available +} + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true")); + + QVulkanInstance inst; + +#ifndef Q_OS_ANDROID + inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation"); +#else + inst.setLayers(QByteArrayList() + << "VK_LAYER_GOOGLE_threading" + << "VK_LAYER_LUNARG_parameter_validation" + << "VK_LAYER_LUNARG_object_tracker" + << "VK_LAYER_LUNARG_core_validation" + << "VK_LAYER_LUNARG_image" + << "VK_LAYER_LUNARG_swapchain" + << "VK_LAYER_GOOGLE_unique_objects"); +#endif + + if (!inst.create()) + qFatal("Failed to create Vulkan instance: %d", inst.errorCode()); + + VulkanWindow w; + w.setVulkanInstance(&inst); + + w.resize(1024, 768); + w.show(); + + return app.exec(); +} diff --git a/examples/vulkan/hellovulkanwidget/hellovulkanwidget.cpp b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.cpp new file mode 100644 index 00000000000..ecab1043993 --- /dev/null +++ b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.cpp @@ -0,0 +1,182 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include "hellovulkanwidget.h" +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(VulkanWindow *w) + : m_window(w) +{ + QWidget *wrapper = QWidget::createWindowContainer(w); + + m_info = new QTextEdit; + m_info->setReadOnly(true); + + m_number = new QLCDNumber(3); + m_number->setSegmentStyle(QLCDNumber::Filled); + + QPushButton *grabButton = new QPushButton(tr("&Grab")); + grabButton->setFocusPolicy(Qt::NoFocus); + + connect(grabButton, &QPushButton::clicked, this, &MainWindow::onGrabRequested); + + QPushButton *quitButton = new QPushButton(tr("&Quit")); + quitButton->setFocusPolicy(Qt::NoFocus); + + connect(quitButton, &QPushButton::clicked, qApp, &QCoreApplication::quit); + + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget(m_info, 2); + layout->addWidget(m_number, 1); + layout->addWidget(wrapper, 5); + layout->addWidget(grabButton, 1); + layout->addWidget(quitButton, 1); + setLayout(layout); +} + +void MainWindow::onVulkanInfoReceived(const QString &text) +{ + m_info->setText(text); +} + +void MainWindow::onFrameQueued(int colorValue) +{ + m_number->display(colorValue); +} + +void MainWindow::onGrabRequested() +{ + if (!m_window->supportsGrab()) { + QMessageBox::warning(this, tr("Cannot grab"), tr("This swapchain does not support readbacks.")); + return; + } + + QImage img = m_window->grab(); + + // Our startNextFrame() implementation is synchronous so img is ready to be + // used right here. + + QFileDialog fd(this); + fd.setAcceptMode(QFileDialog::AcceptSave); + fd.setDefaultSuffix("png"); + fd.selectFile("test.png"); + if (fd.exec() == QDialog::Accepted) + img.save(fd.selectedFiles().first()); +} + +QVulkanWindowRenderer *VulkanWindow::createRenderer() +{ + return new VulkanRenderer(this); +} + +VulkanRenderer::VulkanRenderer(VulkanWindow *w) + : TriangleRenderer(w) +{ +} + +void VulkanRenderer::initResources() +{ + TriangleRenderer::initResources(); + + QVulkanInstance *inst = m_window->vulkanInstance(); + m_devFuncs = inst->deviceFunctions(m_window->device()); + + QString info; + info += QString().sprintf("Number of physical devices: %d\n", m_window->availablePhysicalDevices().count()); + + QVulkanFunctions *f = inst->functions(); + VkPhysicalDeviceProperties props; + f->vkGetPhysicalDeviceProperties(m_window->physicalDevice(), &props); + info += QString().sprintf("Active physical device name: '%s' version %d.%d.%d\nAPI version %d.%d.%d\n", + props.deviceName, + VK_VERSION_MAJOR(props.driverVersion), VK_VERSION_MINOR(props.driverVersion), + VK_VERSION_PATCH(props.driverVersion), + VK_VERSION_MAJOR(props.apiVersion), VK_VERSION_MINOR(props.apiVersion), + VK_VERSION_PATCH(props.apiVersion)); + + info += QStringLiteral("Supported instance layers:\n"); + for (const QVulkanLayer &layer : inst->supportedLayers()) + info += QString().sprintf(" %s v%u\n", layer.name.constData(), layer.version); + info += QStringLiteral("Enabled instance layers:\n"); + for (const QByteArray &layer : inst->layers()) + info += QString().sprintf(" %s\n", layer.constData()); + + info += QStringLiteral("Supported instance extensions:\n"); + for (const QVulkanExtension &ext : inst->supportedExtensions()) + info += QString().sprintf(" %s v%u\n", ext.name.constData(), ext.version); + info += QStringLiteral("Enabled instance extensions:\n"); + for (const QByteArray &ext : inst->extensions()) + info += QString().sprintf(" %s\n", ext.constData()); + + info += QString().sprintf("Color format: %u\nDepth-stencil format: %u\n", + m_window->colorFormat(), m_window->depthStencilFormat()); + + info += QStringLiteral("Supported sample counts:"); + QList sampleCounts = m_window->supportedSampleCounts().toList(); + std::sort(sampleCounts.begin(), sampleCounts.end()); + for (int count : sampleCounts) + info += QLatin1Char(' ') + QString::number(count); + info += QLatin1Char('\n'); + + emit static_cast(m_window)->vulkanInfoReceived(info); +} + +void VulkanRenderer::startNextFrame() +{ + TriangleRenderer::startNextFrame(); + emit static_cast(m_window)->frameQueued(int(m_rotation) % 360); +} diff --git a/examples/vulkan/hellovulkanwidget/hellovulkanwidget.h b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.h new file mode 100644 index 00000000000..b1f4824006a --- /dev/null +++ b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include "../shared/trianglerenderer.h" +#include + +class VulkanWindow; + +QT_BEGIN_NAMESPACE +class QTextEdit; +class QLCDNumber; +QT_END_NAMESPACE + +class MainWindow : public QWidget +{ + Q_OBJECT + +public: + MainWindow(VulkanWindow *w); + +public slots: + void onVulkanInfoReceived(const QString &text); + void onFrameQueued(int colorValue); + void onGrabRequested(); + +private: + VulkanWindow *m_window; + QTextEdit *m_info; + QLCDNumber *m_number; +}; + +class VulkanRenderer : public TriangleRenderer +{ +public: + VulkanRenderer(VulkanWindow *w); + + void initResources() override; + void startNextFrame() override; +}; + +class VulkanWindow : public QVulkanWindow +{ + Q_OBJECT + +public: + QVulkanWindowRenderer *createRenderer() override; + +signals: + void vulkanInfoReceived(const QString &text); + void frameQueued(int colorValue); +}; diff --git a/examples/vulkan/hellovulkanwidget/hellovulkanwidget.pro b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.pro new file mode 100644 index 00000000000..7b87d7f210e --- /dev/null +++ b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.pro @@ -0,0 +1,16 @@ +QT += widgets + +HEADERS += \ + hellovulkanwidget.h \ + ../shared/trianglerenderer.h + +SOURCES += \ + hellovulkanwidget.cpp \ + main.cpp \ + ../shared/trianglerenderer.cpp + +RESOURCES += hellovulkanwidget.qrc + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/vulkan/hellovulkanwidget +INSTALLS += target diff --git a/examples/vulkan/hellovulkanwidget/hellovulkanwidget.qrc b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.qrc new file mode 100644 index 00000000000..489fc7295a2 --- /dev/null +++ b/examples/vulkan/hellovulkanwidget/hellovulkanwidget.qrc @@ -0,0 +1,6 @@ + + + ../shared/color_vert.spv + ../shared/color_frag.spv + + diff --git a/examples/vulkan/hellovulkanwidget/main.cpp b/examples/vulkan/hellovulkanwidget/main.cpp new file mode 100644 index 00000000000..320e015e674 --- /dev/null +++ b/examples/vulkan/hellovulkanwidget/main.cpp @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include +#include +#include +#include "hellovulkanwidget.h" + +Q_LOGGING_CATEGORY(lcVk, "qt.vulkan") + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true")); + + QVulkanInstance inst; + +#ifndef Q_OS_ANDROID + inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation"); +#else + inst.setLayers(QByteArrayList() + << "VK_LAYER_GOOGLE_threading" + << "VK_LAYER_LUNARG_parameter_validation" + << "VK_LAYER_LUNARG_object_tracker" + << "VK_LAYER_LUNARG_core_validation" + << "VK_LAYER_LUNARG_image" + << "VK_LAYER_LUNARG_swapchain" + << "VK_LAYER_GOOGLE_unique_objects"); +#endif + + if (!inst.create()) + qFatal("Failed to create Vulkan instance: %d", inst.errorCode()); + + VulkanWindow *vulkanWindow = new VulkanWindow; + vulkanWindow->setVulkanInstance(&inst); + + MainWindow mainWindow(vulkanWindow); + QObject::connect(vulkanWindow, &VulkanWindow::vulkanInfoReceived, &mainWindow, &MainWindow::onVulkanInfoReceived); + QObject::connect(vulkanWindow, &VulkanWindow::frameQueued, &mainWindow, &MainWindow::onFrameQueued); + + mainWindow.resize(1024, 768); + mainWindow.show(); + + return app.exec(); +} diff --git a/examples/vulkan/hellovulkanwindow/hellovulkanwindow.cpp b/examples/vulkan/hellovulkanwindow/hellovulkanwindow.cpp new file mode 100644 index 00000000000..0a7d1d41741 --- /dev/null +++ b/examples/vulkan/hellovulkanwindow/hellovulkanwindow.cpp @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include "hellovulkanwindow.h" +#include + +//! [0] +QVulkanWindowRenderer *VulkanWindow::createRenderer() +{ + return new VulkanRenderer(this); +} + +VulkanRenderer::VulkanRenderer(QVulkanWindow *w) + : m_window(w) +{ +} +//! [0] + +//! [1] +void VulkanRenderer::initResources() +{ + qDebug("initResources"); + + m_devFuncs = m_window->vulkanInstance()->deviceFunctions(m_window->device()); +} +//! [1] + +void VulkanRenderer::initSwapChainResources() +{ + qDebug("initSwapChainResources"); +} + +void VulkanRenderer::releaseSwapChainResources() +{ + qDebug("releaseSwapChainResources"); +} + +void VulkanRenderer::releaseResources() +{ + qDebug("releaseResources"); +} + +//! [2] +void VulkanRenderer::startNextFrame() +{ + m_green += 0.005f; + if (m_green > 1.0f) + m_green = 0.0f; + + VkClearColorValue clearColor = { 0.0f, m_green, 0.0f, 1.0f }; + VkClearDepthStencilValue clearDS = { 1.0f, 0 }; + VkClearValue clearValues[2]; + memset(clearValues, 0, sizeof(clearValues)); + clearValues[0].color = clearColor; + clearValues[1].depthStencil = clearDS; + + VkRenderPassBeginInfo rpBeginInfo; + memset(&rpBeginInfo, 0, sizeof(rpBeginInfo)); + rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBeginInfo.renderPass = m_window->defaultRenderPass(); + rpBeginInfo.framebuffer = m_window->currentFramebuffer(); + const QSize sz = m_window->swapChainImageSize(); + rpBeginInfo.renderArea.extent.width = sz.width(); + rpBeginInfo.renderArea.extent.height = sz.height(); + rpBeginInfo.clearValueCount = 2; + rpBeginInfo.pClearValues = clearValues; + VkCommandBuffer cmdBuf = m_window->currentCommandBuffer(); + m_devFuncs->vkCmdBeginRenderPass(cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + + // Do nothing else. We will just clear to green, changing the component on + // every invocation. This also helps verifying the rate to which the thread + // is throttled to. (The elapsed time between startNextFrame calls should + // typically be around 16 ms. Note that rendering is 2 frames ahead of what + // is displayed.) + + m_devFuncs->vkCmdEndRenderPass(cmdBuf); + + m_window->frameReady(); + m_window->requestUpdate(); // render continuously, throttled by the presentation rate +} +//! [2] diff --git a/examples/vulkan/hellovulkanwindow/hellovulkanwindow.h b/examples/vulkan/hellovulkanwindow/hellovulkanwindow.h new file mode 100644 index 00000000000..5f52e402cad --- /dev/null +++ b/examples/vulkan/hellovulkanwindow/hellovulkanwindow.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include + +//! [0] +class VulkanRenderer : public QVulkanWindowRenderer +{ +public: + VulkanRenderer(QVulkanWindow *w); + + void initResources() override; + void initSwapChainResources() override; + void releaseSwapChainResources() override; + void releaseResources() override; + + void startNextFrame() override; + +private: + QVulkanWindow *m_window; + QVulkanDeviceFunctions *m_devFuncs; + float m_green = 0; +}; + +class VulkanWindow : public QVulkanWindow +{ +public: + QVulkanWindowRenderer *createRenderer() override; +}; +//! [0] diff --git a/examples/vulkan/hellovulkanwindow/hellovulkanwindow.pro b/examples/vulkan/hellovulkanwindow/hellovulkanwindow.pro new file mode 100644 index 00000000000..8f7d9494e2f --- /dev/null +++ b/examples/vulkan/hellovulkanwindow/hellovulkanwindow.pro @@ -0,0 +1,6 @@ +HEADERS += hellovulkanwindow.h +SOURCES += hellovulkanwindow.cpp main.cpp + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/vulkan/hellovulkanwindow +INSTALLS += target diff --git a/examples/vulkan/hellovulkanwindow/main.cpp b/examples/vulkan/hellovulkanwindow/main.cpp new file mode 100644 index 00000000000..313c28f9e02 --- /dev/null +++ b/examples/vulkan/hellovulkanwindow/main.cpp @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include +#include +#include +#include "hellovulkanwindow.h" + +Q_LOGGING_CATEGORY(lcVk, "qt.vulkan") + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true")); + +//! [0] + QVulkanInstance inst; + +#ifndef Q_OS_ANDROID + inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation"); +#else + inst.setLayers(QByteArrayList() + << "VK_LAYER_GOOGLE_threading" + << "VK_LAYER_LUNARG_parameter_validation" + << "VK_LAYER_LUNARG_object_tracker" + << "VK_LAYER_LUNARG_core_validation" + << "VK_LAYER_LUNARG_image" + << "VK_LAYER_LUNARG_swapchain" + << "VK_LAYER_GOOGLE_unique_objects"); +#endif + + if (!inst.create()) + qFatal("Failed to create Vulkan instance: %d", inst.errorCode()); +//! [0] + +//! [1] + VulkanWindow w; + w.setVulkanInstance(&inst); + + w.resize(1024, 768); + w.show(); +//! [1] + + return app.exec(); +} diff --git a/examples/vulkan/shared/color.frag b/examples/vulkan/shared/color.frag new file mode 100644 index 00000000000..375587662f3 --- /dev/null +++ b/examples/vulkan/shared/color.frag @@ -0,0 +1,10 @@ +#version 440 + +layout(location = 0) in vec3 v_color; + +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = vec4(v_color, 1.0); +} diff --git a/examples/vulkan/shared/color.vert b/examples/vulkan/shared/color.vert new file mode 100644 index 00000000000..02492c0e650 --- /dev/null +++ b/examples/vulkan/shared/color.vert @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec4 position; +layout(location = 1) in vec3 color; + +layout(location = 0) out vec3 v_color; + +layout(std140, binding = 0) uniform buf { + mat4 mvp; +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() +{ + v_color = color; + gl_Position = ubuf.mvp * position; +} diff --git a/examples/vulkan/shared/color_frag.spv b/examples/vulkan/shared/color_frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..30e33b76caec67e263f8b4a7ba8c64bdafd42048 GIT binary patch literal 496 zcmYk1%}PR16oq%*u3F|_Ld`;H7>I+6AgVzFCk~1pAVevGOt5I&WA$iF0>AHc70+ee zeb(A*|D3FJT8Y@* z0|%gmPn`kWGP*|mP?V!?`*Rd)o|luCyT#jL$z6{3+n$jMfFm_}xS9>@NQ*(U+)S72FwW`uBAxjU$x!Syyk#P{t5FK{1c zdF=6vk`u2{?Og}fKcHIMEHuJKYZaO^*1mS3+5evH-Tx3+uiw0T+%VPmnEPYPORxg& CPZ&)A literal 0 HcmV?d00001 diff --git a/examples/vulkan/shared/color_vert.spv b/examples/vulkan/shared/color_vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..a1f42e3119da21c1590a3dc0d20bf4d3e3b0fcbc GIT binary patch literal 960 zcmYk4%T5A85JlS=22oT*9Ji zrq!AhUH4Y?t?F)kueEB-ioTo)O=3mk2^*6)S-VEv`)+$Se`%kcpHi`9d|OCO!K~Rh zquuGxTrOhqji$v9mQBpxTF;=r_H^`QXq7b60TWt7+`&_l4<$9`#`E z4vu>6kh#cT=v~mEF9!c5hIhOc%Ux{9xrZBNxi9doD(G92GYe;-o*DM!@L+lz%IW{1 aJ>VlbHSp*#Peaaqp+g@qb>Eu$QvL(SWifyN literal 0 HcmV?d00001 diff --git a/examples/vulkan/shared/trianglerenderer.cpp b/examples/vulkan/shared/trianglerenderer.cpp new file mode 100644 index 00000000000..f2e636bbe61 --- /dev/null +++ b/examples/vulkan/shared/trianglerenderer.cpp @@ -0,0 +1,513 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include "trianglerenderer.h" +#include +#include + +// Note that the vertex data and the projection matrix assume OpenGL. With +// Vulkan Y is negated in clip space and the near/far plane is at 0/1 instead +// of -1/1. These will be corrected for by an extra transformation when +// calculating the modelview-projection matrix. +static float vertexData[] = { + 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 const int UNIFORM_DATA_SIZE = 16 * sizeof(float); + +static inline VkDeviceSize aligned(VkDeviceSize v, VkDeviceSize byteAlign) +{ + return (v + byteAlign - 1) & ~(byteAlign - 1); +} + +TriangleRenderer::TriangleRenderer(QVulkanWindow *w, bool msaa) + : m_window(w) +{ + if (msaa) { + QSet counts = w->supportedSampleCounts(); + qDebug() << "Supported sample counts:" << counts; + for (int s = 16; s >= 4; s /= 2) { + if (counts.contains(s)) { + qDebug("Requesting sample count %d", s); + m_window->setSampleCount(s); + break; + } + } + } +} + +VkShaderModule TriangleRenderer::createShader(const QString &name) +{ + QFile file(name); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("Failed to read shader %s", qPrintable(name)); + return VK_NULL_HANDLE; + } + QByteArray blob = file.readAll(); + file.close(); + + VkShaderModuleCreateInfo shaderInfo; + memset(&shaderInfo, 0, sizeof(shaderInfo)); + shaderInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + shaderInfo.codeSize = blob.size(); + shaderInfo.pCode = reinterpret_cast(blob.constData()); + VkShaderModule shaderModule; + VkResult err = m_devFuncs->vkCreateShaderModule(m_window->device(), &shaderInfo, nullptr, &shaderModule); + if (err != VK_SUCCESS) { + qWarning("Failed to create shader module: %d", err); + return VK_NULL_HANDLE; + } + + return shaderModule; +} + +void TriangleRenderer::initResources() +{ + qDebug("initResources"); + + VkDevice dev = m_window->device(); + m_devFuncs = m_window->vulkanInstance()->deviceFunctions(dev); + + // Prepare the vertex and uniform data. The vertex data will never + // change so one buffer is sufficient regardless of the value of + // QVulkanWindow::CONCURRENT_FRAME_COUNT. Uniform data is changing per + // frame however so active frames have to have a dedicated copy. + + // Use just one memory allocation and one buffer. We will then specify the + // appropriate offsets for uniform buffers in the VkDescriptorBufferInfo. + // Have to watch out for + // VkPhysicalDeviceLimits::minUniformBufferOffsetAlignment, though. + + // The uniform buffer is not strictly required in this example, we could + // have used push constants as well since our single matrix (64 bytes) fits + // into the spec mandated minimum limit of 128 bytes. However, once that + // limit is not sufficient, the per-frame buffers, as shown below, will + // become necessary. + + const int concurrentFrameCount = m_window->concurrentFrameCount(); + const VkPhysicalDeviceLimits *pdevLimits = &m_window->physicalDeviceProperties()->limits; + const VkDeviceSize uniAlign = pdevLimits->minUniformBufferOffsetAlignment; + qDebug("uniform buffer offset alignment is %u", (uint) uniAlign); + VkBufferCreateInfo bufInfo; + memset(&bufInfo, 0, sizeof(bufInfo)); + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + // Our internal layout is vertex, uniform, uniform, ... with each uniform buffer start offset aligned to uniAlign. + const VkDeviceSize vertexAllocSize = aligned(sizeof(vertexData), uniAlign); + const VkDeviceSize uniformAllocSize = aligned(UNIFORM_DATA_SIZE, uniAlign); + bufInfo.size = vertexAllocSize + concurrentFrameCount * uniformAllocSize; + bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VkResult err = m_devFuncs->vkCreateBuffer(dev, &bufInfo, nullptr, &m_buf); + if (err != VK_SUCCESS) + qFatal("Failed to create buffer: %d", err); + + VkMemoryRequirements memReq; + m_devFuncs->vkGetBufferMemoryRequirements(dev, m_buf, &memReq); + + VkMemoryAllocateInfo memAllocInfo = { + VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + nullptr, + memReq.size, + m_window->hostVisibleMemoryIndex() + }; + + err = m_devFuncs->vkAllocateMemory(dev, &memAllocInfo, nullptr, &m_bufMem); + if (err != VK_SUCCESS) + qFatal("Failed to allocate memory: %d", err); + + err = m_devFuncs->vkBindBufferMemory(dev, m_buf, m_bufMem, 0); + if (err != VK_SUCCESS) + qFatal("Failed to bind buffer memory: %d", err); + + quint8 *p; + err = m_devFuncs->vkMapMemory(dev, m_bufMem, 0, memReq.size, 0, reinterpret_cast(&p)); + if (err != VK_SUCCESS) + qFatal("Failed to map memory: %d", err); + memcpy(p, vertexData, sizeof(vertexData)); + QMatrix4x4 ident; + memset(m_uniformBufInfo, 0, sizeof(m_uniformBufInfo)); + for (int i = 0; i < concurrentFrameCount; ++i) { + const VkDeviceSize offset = vertexAllocSize + i * uniformAllocSize; + memcpy(p + offset, ident.constData(), 16 * sizeof(float)); + m_uniformBufInfo[i].buffer = m_buf; + m_uniformBufInfo[i].offset = offset; + m_uniformBufInfo[i].range = uniformAllocSize; + } + m_devFuncs->vkUnmapMemory(dev, m_bufMem); + + VkVertexInputBindingDescription vertexBindingDesc = { + 0, // binding + 5 * sizeof(float), + VK_VERTEX_INPUT_RATE_VERTEX + }; + VkVertexInputAttributeDescription vertexAttrDesc[] = { + { // position + 0, // location + 0, // binding + VK_FORMAT_R32G32_SFLOAT, + 0 + }, + { // color + 1, + 0, + VK_FORMAT_R32G32B32_SFLOAT, + 2 * sizeof(float) + } + }; + + VkPipelineVertexInputStateCreateInfo vertexInputInfo; + vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertexInputInfo.pNext = nullptr; + vertexInputInfo.flags = 0; + vertexInputInfo.vertexBindingDescriptionCount = 1; + vertexInputInfo.pVertexBindingDescriptions = &vertexBindingDesc; + vertexInputInfo.vertexAttributeDescriptionCount = 2; + vertexInputInfo.pVertexAttributeDescriptions = vertexAttrDesc; + + // Set up descriptor set and its layout. + VkDescriptorPoolSize descPoolSizes = { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, uint32_t(concurrentFrameCount) }; + VkDescriptorPoolCreateInfo descPoolInfo; + memset(&descPoolInfo, 0, sizeof(descPoolInfo)); + descPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + descPoolInfo.maxSets = concurrentFrameCount; + descPoolInfo.poolSizeCount = 1; + descPoolInfo.pPoolSizes = &descPoolSizes; + err = m_devFuncs->vkCreateDescriptorPool(dev, &descPoolInfo, nullptr, &m_descPool); + if (err != VK_SUCCESS) + qFatal("Failed to create descriptor pool: %d", err); + + VkDescriptorSetLayoutBinding layoutBinding = { + 0, // binding + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + 1, + VK_SHADER_STAGE_VERTEX_BIT, + nullptr + }; + VkDescriptorSetLayoutCreateInfo descLayoutInfo = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + nullptr, + 0, + 1, + &layoutBinding + }; + err = m_devFuncs->vkCreateDescriptorSetLayout(dev, &descLayoutInfo, nullptr, &m_descSetLayout); + if (err != VK_SUCCESS) + qFatal("Failed to create descriptor set layout: %d", err); + + for (int i = 0; i < concurrentFrameCount; ++i) { + VkDescriptorSetAllocateInfo descSetAllocInfo = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + nullptr, + m_descPool, + 1, + &m_descSetLayout + }; + err = m_devFuncs->vkAllocateDescriptorSets(dev, &descSetAllocInfo, &m_descSet[i]); + if (err != VK_SUCCESS) + qFatal("Failed to allocate descriptor set: %d", err); + + VkWriteDescriptorSet descWrite; + memset(&descWrite, 0, sizeof(descWrite)); + descWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descWrite.dstSet = m_descSet[i]; + descWrite.descriptorCount = 1; + descWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descWrite.pBufferInfo = &m_uniformBufInfo[i]; + m_devFuncs->vkUpdateDescriptorSets(dev, 1, &descWrite, 0, nullptr); + } + + // Pipeline cache + VkPipelineCacheCreateInfo pipelineCacheInfo; + memset(&pipelineCacheInfo, 0, sizeof(pipelineCacheInfo)); + pipelineCacheInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + err = m_devFuncs->vkCreatePipelineCache(dev, &pipelineCacheInfo, nullptr, &m_pipelineCache); + if (err != VK_SUCCESS) + qFatal("Failed to create pipeline cache: %d", err); + + // Pipeline layout + VkPipelineLayoutCreateInfo pipelineLayoutInfo; + memset(&pipelineLayoutInfo, 0, sizeof(pipelineLayoutInfo)); + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &m_descSetLayout; + err = m_devFuncs->vkCreatePipelineLayout(dev, &pipelineLayoutInfo, nullptr, &m_pipelineLayout); + if (err != VK_SUCCESS) + qFatal("Failed to create pipeline layout: %d", err); + + // Shaders + VkShaderModule vertShaderModule = createShader(QStringLiteral(":/color_vert.spv")); + VkShaderModule fragShaderModule = createShader(QStringLiteral(":/color_frag.spv")); + + // Graphics pipeline + VkGraphicsPipelineCreateInfo pipelineInfo; + memset(&pipelineInfo, 0, sizeof(pipelineInfo)); + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + + VkPipelineShaderStageCreateInfo shaderStages[2] = { + { + VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + nullptr, + 0, + VK_SHADER_STAGE_VERTEX_BIT, + vertShaderModule, + "main", + nullptr + }, + { + VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + nullptr, + 0, + VK_SHADER_STAGE_FRAGMENT_BIT, + fragShaderModule, + "main", + nullptr + } + }; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = shaderStages; + + pipelineInfo.pVertexInputState = &vertexInputInfo; + + VkPipelineInputAssemblyStateCreateInfo ia; + memset(&ia, 0, sizeof(ia)); + ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + pipelineInfo.pInputAssemblyState = &ia; + + // The viewport and scissor will be set dynamically via vkCmdSetViewport/Scissor. + // This way the pipeline does not need to be touched when resizing the window. + VkPipelineViewportStateCreateInfo vp; + memset(&vp, 0, sizeof(vp)); + vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + vp.viewportCount = 1; + vp.scissorCount = 1; + pipelineInfo.pViewportState = &vp; + + VkPipelineRasterizationStateCreateInfo rs; + memset(&rs, 0, sizeof(rs)); + rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rs.polygonMode = VK_POLYGON_MODE_FILL; + rs.cullMode = VK_CULL_MODE_NONE; // we want the back face as well + rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rs.lineWidth = 1.0f; + pipelineInfo.pRasterizationState = &rs; + + VkPipelineMultisampleStateCreateInfo ms; + memset(&ms, 0, sizeof(ms)); + ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + // Enable multisampling. + ms.rasterizationSamples = m_window->sampleCountFlagBits(); + pipelineInfo.pMultisampleState = &ms; + + VkPipelineDepthStencilStateCreateInfo ds; + memset(&ds, 0, sizeof(ds)); + ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + ds.depthTestEnable = VK_TRUE; + ds.depthWriteEnable = VK_TRUE; + ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; + pipelineInfo.pDepthStencilState = &ds; + + VkPipelineColorBlendStateCreateInfo cb; + memset(&cb, 0, sizeof(cb)); + cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + // no blend, write out all of rgba + VkPipelineColorBlendAttachmentState att; + memset(&att, 0, sizeof(att)); + att.colorWriteMask = 0xF; + cb.attachmentCount = 1; + cb.pAttachments = &att; + pipelineInfo.pColorBlendState = &cb; + + VkDynamicState dynEnable[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dyn; + memset(&dyn, 0, sizeof(dyn)); + dyn.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dyn.dynamicStateCount = sizeof(dynEnable) / sizeof(VkDynamicState); + dyn.pDynamicStates = dynEnable; + pipelineInfo.pDynamicState = &dyn; + + pipelineInfo.layout = m_pipelineLayout; + pipelineInfo.renderPass = m_window->defaultRenderPass(); + + err = m_devFuncs->vkCreateGraphicsPipelines(dev, m_pipelineCache, 1, &pipelineInfo, nullptr, &m_pipeline); + if (err != VK_SUCCESS) + qFatal("Failed to create graphics pipeline: %d", err); + + if (vertShaderModule) + m_devFuncs->vkDestroyShaderModule(dev, vertShaderModule, nullptr); + if (fragShaderModule) + m_devFuncs->vkDestroyShaderModule(dev, fragShaderModule, nullptr); +} + +void TriangleRenderer::initSwapChainResources() +{ + qDebug("initSwapChainResources"); + + // Projection matrix + m_proj = *m_window->clipCorrectionMatrix(); // adjust for Vulkan-OpenGL clip space differences + const QSize sz = m_window->swapChainImageSize(); + m_proj.perspective(45.0f, sz.width() / (float) sz.height(), 0.01f, 100.0f); + m_proj.translate(0, 0, -4); +} + +void TriangleRenderer::releaseSwapChainResources() +{ + qDebug("releaseSwapChainResources"); +} + +void TriangleRenderer::releaseResources() +{ + qDebug("releaseResources"); + + VkDevice dev = m_window->device(); + + if (m_pipeline) { + m_devFuncs->vkDestroyPipeline(dev, m_pipeline, nullptr); + m_pipeline = VK_NULL_HANDLE; + } + + if (m_pipelineLayout) { + m_devFuncs->vkDestroyPipelineLayout(dev, m_pipelineLayout, nullptr); + m_pipelineLayout = VK_NULL_HANDLE; + } + + if (m_pipelineCache) { + m_devFuncs->vkDestroyPipelineCache(dev, m_pipelineCache, nullptr); + m_pipelineCache = VK_NULL_HANDLE; + } + + if (m_descSetLayout) { + m_devFuncs->vkDestroyDescriptorSetLayout(dev, m_descSetLayout, nullptr); + m_descSetLayout = VK_NULL_HANDLE; + } + + if (m_descPool) { + m_devFuncs->vkDestroyDescriptorPool(dev, m_descPool, nullptr); + m_descPool = VK_NULL_HANDLE; + } + + if (m_buf) { + m_devFuncs->vkDestroyBuffer(dev, m_buf, nullptr); + m_buf = VK_NULL_HANDLE; + } + + if (m_bufMem) { + m_devFuncs->vkFreeMemory(dev, m_bufMem, nullptr); + m_bufMem = VK_NULL_HANDLE; + } +} + +void TriangleRenderer::startNextFrame() +{ + VkDevice dev = m_window->device(); + VkCommandBuffer cb = m_window->currentCommandBuffer(); + const QSize sz = m_window->swapChainImageSize(); + + VkClearColorValue clearColor = { 0, 0, 0, 1 }; + VkClearDepthStencilValue clearDS = { 1, 0 }; + VkClearValue clearValues[3]; + memset(clearValues, 0, sizeof(clearValues)); + clearValues[0].color = clearValues[2].color = clearColor; + clearValues[1].depthStencil = clearDS; + + VkRenderPassBeginInfo rpBeginInfo; + memset(&rpBeginInfo, 0, sizeof(rpBeginInfo)); + rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBeginInfo.renderPass = m_window->defaultRenderPass(); + rpBeginInfo.framebuffer = m_window->currentFramebuffer(); + rpBeginInfo.renderArea.extent.width = sz.width(); + rpBeginInfo.renderArea.extent.height = sz.height(); + rpBeginInfo.clearValueCount = m_window->sampleCountFlagBits() > VK_SAMPLE_COUNT_1_BIT ? 3 : 2; + rpBeginInfo.pClearValues = clearValues; + VkCommandBuffer cmdBuf = m_window->currentCommandBuffer(); + m_devFuncs->vkCmdBeginRenderPass(cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + + quint8 *p; + VkResult err = m_devFuncs->vkMapMemory(dev, m_bufMem, m_uniformBufInfo[m_window->currentFrame()].offset, + UNIFORM_DATA_SIZE, 0, reinterpret_cast(&p)); + if (err != VK_SUCCESS) + qFatal("Failed to map memory: %d", err); + QMatrix4x4 m = m_proj; + m.rotate(m_rotation, 0, 1, 0); + memcpy(p, m.constData(), 16 * sizeof(float)); + m_devFuncs->vkUnmapMemory(dev, m_bufMem); + + // Not exactly a real animation system, just advance on every frame for now. + m_rotation += 1.0f; + + m_devFuncs->vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline); + m_devFuncs->vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, + &m_descSet[m_window->currentFrame()], 0, nullptr); + VkDeviceSize vbOffset = 0; + m_devFuncs->vkCmdBindVertexBuffers(cb, 0, 1, &m_buf, &vbOffset); + + VkViewport viewport; + viewport.x = viewport.y = 0; + viewport.width = sz.width(); + viewport.height = sz.height(); + viewport.minDepth = 0; + viewport.maxDepth = 1; + m_devFuncs->vkCmdSetViewport(cb, 0, 1, &viewport); + + VkRect2D scissor; + scissor.offset.x = scissor.offset.y = 0; + scissor.extent.width = viewport.width; + scissor.extent.height = viewport.height; + m_devFuncs->vkCmdSetScissor(cb, 0, 1, &scissor); + + m_devFuncs->vkCmdDraw(cb, 3, 1, 0, 0); + + m_devFuncs->vkCmdEndRenderPass(cmdBuf); + + m_window->frameReady(); + m_window->requestUpdate(); // render continuously, throttled by the presentation rate +} diff --git a/examples/vulkan/shared/trianglerenderer.h b/examples/vulkan/shared/trianglerenderer.h new file mode 100644 index 00000000000..9a33291a959 --- /dev/null +++ b/examples/vulkan/shared/trianglerenderer.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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$ +** +****************************************************************************/ + +#include + +class TriangleRenderer : public QVulkanWindowRenderer +{ +public: + TriangleRenderer(QVulkanWindow *w, bool msaa = false); + + void initResources() override; + void initSwapChainResources() override; + void releaseSwapChainResources() override; + void releaseResources() override; + + void startNextFrame() override; + +protected: + VkShaderModule createShader(const QString &name); + + QVulkanWindow *m_window; + QVulkanDeviceFunctions *m_devFuncs; + + VkDeviceMemory m_bufMem = VK_NULL_HANDLE; + VkBuffer m_buf = VK_NULL_HANDLE; + VkDescriptorBufferInfo m_uniformBufInfo[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT]; + + VkDescriptorPool m_descPool = VK_NULL_HANDLE; + VkDescriptorSetLayout m_descSetLayout = VK_NULL_HANDLE; + VkDescriptorSet m_descSet[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT]; + + VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; + VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; + VkPipeline m_pipeline = VK_NULL_HANDLE; + + QMatrix4x4 m_proj; + float m_rotation = 0.0f; +}; diff --git a/examples/vulkan/vulkan.pro b/examples/vulkan/vulkan.pro new file mode 100644 index 00000000000..ef5496bcd4e --- /dev/null +++ b/examples/vulkan/vulkan.pro @@ -0,0 +1,7 @@ +TEMPLATE = subdirs + +SUBDIRS = hellovulkanwindow \ + hellovulkantriangle \ + hellovulkantexture + +qtHaveModule(widgets): SUBDIRS += hellovulkanwidget diff --git a/src/gui/vulkan/qvulkanwindow.cpp b/src/gui/vulkan/qvulkanwindow.cpp new file mode 100644 index 00000000000..2540a694261 --- /dev/null +++ b/src/gui/vulkan/qvulkanwindow.cpp @@ -0,0 +1,2678 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qvulkanwindow_p.h" +#include "qvulkanfunctions.h" +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcVk, "qt.vulkan") + +/*! + \class QVulkanWindow + \inmodule QtGui + \since 5.10 + \brief The QVulkanWindow class is a convenience subclass of QWindow to perform Vulkan rendering. + + QVulkanWindow is a Vulkan-capable QWindow that manages a Vulkan device, a + graphics queue, a command pool and buffer, a depth-stencil image and a + double-buffered FIFO swapchain, while taking care of correct behavior when it + comes to events like resize, special situations like not having a device + queue supporting both graphics and presentation, device lost scenarios, and + additional functionality like reading the rendered content back. Conceptually + it is the counterpart of QOpenGLWindow in the Vulkan world. + + \note QVulkanWindow does not always eliminate the need to implement a fully + custom QWindow subclass as it will not necessarily be sufficient in advanced + use cases. + + QVulkanWindow can be embedded into QWidget-based user interfaces via + QWidget::createWindowContainer(). This approach has a number of limitations, + however. Make sure to study the + \l{QWidget::createWindowContainer()}{documentation} first. + + A typical application using QVulkanWindow may look like the following: + + \code + class VulkanRenderer : public QVulkanWindowRenderer + { + public: + VulkanRenderer(QVulkanWindow *w) : m_window(w) { } + + void initResources() override + { + m_devFuncs = m_window->vulkanInstance()->deviceFunctions(m_window->device()); + ... + } + void initSwapChainResources() override { ... } + void releaseSwapChainResources() override { ... } + void releaseResources() override { ... } + + void startNextFrame() override + { + VkCommandBuffer cmdBuf = m_window->currentCommandBuffer(); + ... + m_devFuncs->vkCmdBeginRenderPass(...); + ... + m_window->frameReady(); + } + + private: + QVulkanWindow *m_window; + QVulkanDeviceFunctions *m_devFuncs; + }; + + class VulkanWindow : public QVulkanWindow + { + public: + QVulkanWindowRenderer *createRenderer() override { + return new VulkanRenderer(this); + } + }; + + int main(int argc, char *argv[]) + { + QGuiApplication app(argc, argv); + + QVulkanInstance inst; + // enable the standard validation layers, when available + inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation"); + if (!inst.create()) + qFatal("Failed to create Vulkan instance: %d", inst.errorCode()); + + VulkanWindow w; + w.setVulkanInstance(&inst); + w.showMaximized(); + + return app.exec(); + } + \endcode + + As it can be seen in the example, the main patterns in QVulkanWindow usage are: + + \list + + \li The QVulkanInstance is associated via QWindow::setVulkanInstance(). It is + then retrievable via QWindow::vulkanInstance() from everywhere, on any + thread. + + \li Similarly to QVulkanInstance, device extensions can be queried via + supportedDeviceExtensions() before the actual initialization. Requesting an + extension to be enabled is done via setDeviceExtensions(). Such calls must be + made before the window becomes visible, that is, before calling show() or + similar functions. Unsupported extension requests are gracefully ignored. + + \li The renderer is implemented in a QVulkanWindowRenderer subclass, an + instance of which is created in the createRenderer() factory function. + + \li The core Vulkan commands are exposed via the QVulkanFunctions object, + retrievable by calling QVulkanInstance::functions(). Device level functions + are available after creating a VkDevice by calling + QVulkanInstance::deviceFunctions(). + + \li The building of the draw calls for the next frame happens in + QVulkanWindowRenderer::startNextFrame(). The implementation is expected to + add commands to the command buffer returned from currentCommandBuffer(). + Returning from the function does not indicate that the commands are ready for + submission. Rather, an explicit call to frameReady() is required. This allows + asynchronous generation of commands, possibly on multiple threads. Simple + implementations will simply call frameReady() at the end of their + QVulkanWindowRenderer::startNextFrame(). + + \li The basic Vulkan resources (physical device, graphics queue, a command + pool, the window's main command buffer, image formats, etc.) are exposed on + the QVulkanWindow via lightweight getter functions. Some of these are for + convenience only, and applications are always free to query, create and + manage additional resources directly via the Vulkan API. + + \li The renderer lives in the gui/main thread, like the window itself. This + thread is then throttled to the presentation rate, similarly to how OpenGl + with a swap interval of 1 would behave. However, the renderer implementation + is free to utilize multiple threads in any way it sees fit. The accessors + like vulkanInstance(), currentCommandBuffer(), etc. can be called from any + thread. The submission of the main command buffer, the queueing of present, + and the building of the next frame do not start until frameReady() is + invoked on the gui/main thread. + + \li When the window is made visible, the content is updated automatically. + Further updates can be requested by calling QWindow::requestUpdate(). To + render continuously, call requestUpdate() after frameReady(). + + \endlist + + For troubleshooting, enable the logging category \c{qt.vulkan}. Critical + errors are printed via qWarning() automatically. + + \section1 Coordinate system differences between OpenGL and Vulkan + + There are two notable differences to be aware of: First, with Vulkan Y points + down the screen in clip space, while OpenGL uses an upwards pointing Y axis. + Second, the standard OpenGL projection matrix assume a near and far plane + values of -1 and 1, while Vulkan prefers 0 and 1. + + In order to help applications migrate from OpenGL-based code without having + to flip Y coordinates in the vertex data, and to allow using QMatrix4x4 + functions like QMatrix4x4::perspective() while keeping the Vulkan viewport's + minDepth and maxDepth set to 0 and 1, QVulkanWindow provides a correction + matrix retrievable by calling clipCorrectionMatrix(). + + \section1 Multisampling + + While disabled by default, multisample antialiasing is fully supported by + QVulkanWindow. Additional color buffers and resolving into the swapchain's + non-multisample buffers are all managed automatically. + + To query the supported sample counts, call supportedSampleCounts(). When the + returned set contains 4, 8, ..., passing one of those values to setSampleCount() + requests multisample rendering. + + \note unlike QSurfaceFormat::setSamples(), the list of supported sample + counts are exposed to the applications in advance and there is no automatic + falling back to lower sample counts in setSampleCount(). If the requested value + is not supported, a warning is shown and a no multisampling will be used. + + \section1 Reading images back + + When supportsGrab() returns true, QVulkanWindow can perform readbacks from + the color buffer into a QImage. grab() is a slow and inefficient operation, + so frequent usage should be avoided. It is nonetheless valuable since it + allows applications to take screenshots, or tools and tests to process and + verify the output of the GPU rendering. + + \section1 sRGB support + + While many applications will be fine with the default behavior of + QVulkanWindow when it comes to swapchain image formats, + setPreferredColorFormats() allows requesting a pre-defined format. This is + useful most notably when working in the sRGB color space. Passing a format + like \c{VK_FORMAT_B8G8R8A8_SRGB} results in choosing an sRGB format, when + available. + + \section1 Validation layers + + During application development it can be extremely valuable to have the + Vulkan validation layers enabled. As shown in the example code above, calling + QVulkanInstance::setLayers() on the QVulkanInstance before + QVulkanInstance::create() enables validation, assuming the Vulkan driver + stack in the system contains the necessary layers. + + \note Be aware of platform-specific differences. On desktop platforms + installing the \l{https://www.lunarg.com/vulkan-sdk/}{Vulkan SDK} is + typically sufficient. However, Android for example requires deploying + additional shared libraries together with the application, and also mandates + a different list of validation layer names. See + \l{https://developer.android.com/ndk/guides/graphics/validation-layer.html}{the + Android Vulkan development pages} for more information. + + \note QVulkanWindow does not expose device layers since this functionality + has been deprecated since version 1.0.13 of the Vulkan API. + + \sa QVulkanInstance, QVulkanFunctions, QWindow + */ + +/*! + Constructs a new QVulkanWindow with the given \a parent. + + The surface type is set to QSurface::VulkanSurface. + */ +QVulkanWindow::QVulkanWindow(QWindow *parent) + : QWindow(*(new QVulkanWindowPrivate), parent) +{ + setSurfaceType(QSurface::VulkanSurface); +} + +/*! + Destructor. +*/ +QVulkanWindow::~QVulkanWindow() +{ +} + +QVulkanWindowPrivate::~QVulkanWindowPrivate() +{ + // graphics resource cleanup is already done at this point due to + // QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed + + delete renderer; +} + +/*! + \enum QVulkanWindow::Flag + + This enum describes the flags that can be passed to setFlags(). + + \value PersistentResources Ensures no graphics resources are released when + the window becomes unexposed. The default behavior is to release + everything, and reinitialize later when becoming visible again. + */ + +/*! + Configures the behavior based on the provided \a flags. + + \note This function must be called before the window is made visible or at + latest in QVulkanWindowRenderer::preInitResources(), and has no effect if + called afterwards. + */ +void QVulkanWindow::setFlags(Flags flags) +{ + Q_D(QVulkanWindow); + if (d->status != QVulkanWindowPrivate::StatusUninitialized) { + qWarning("QVulkanWindow: Attempted to set flags when already initialized"); + return; + } + d->flags = flags; +} + +/*! + \return the requested flags. + */ +QVulkanWindow::Flags QVulkanWindow::flags() const +{ + Q_D(const QVulkanWindow); + return d->flags; +} + +/*! + \return the list of properties for the supported physical devices in the system. + + \note This function can be called before making the window visible. + */ +QVector QVulkanWindow::availablePhysicalDevices() +{ + Q_D(QVulkanWindow); + if (!d->physDevs.isEmpty() && !d->physDevProps.isEmpty()) + return d->physDevProps; + + QVulkanInstance *inst = vulkanInstance(); + if (!inst) { + qWarning("QVulkanWindow: Attempted to call availablePhysicalDevices() without a QVulkanInstance"); + return d->physDevProps; + } + + QVulkanFunctions *f = inst->functions(); + uint32_t count = 1; + VkResult err = f->vkEnumeratePhysicalDevices(inst->vkInstance(), &count, nullptr); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to get physical device count: %d", err); + return d->physDevProps; + } + + qCDebug(lcVk, "%d physical devices", count); + if (!count) + return d->physDevProps; + + QVector devs(count); + err = f->vkEnumeratePhysicalDevices(inst->vkInstance(), &count, devs.data()); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to enumerate physical devices: %d", err); + return d->physDevProps; + } + + d->physDevs = devs; + d->physDevProps.resize(count); + for (uint32_t i = 0; i < count; ++i) { + VkPhysicalDeviceProperties *p = &d->physDevProps[i]; + f->vkGetPhysicalDeviceProperties(d->physDevs.at(i), p); + qCDebug(lcVk, "Physical device [%d]: name '%s' version %d.%d.%d", i, p->deviceName, + VK_VERSION_MAJOR(p->driverVersion), VK_VERSION_MINOR(p->driverVersion), + VK_VERSION_PATCH(p->driverVersion)); + } + + return d->physDevProps; +} + +/*! + Requests the usage of the physical device with index \a idx. The index + corresponds to the list returned from availablePhysicalDevices(). + + By default the first physical device is used. + + \note This function must be called before the window is made visible or at + latest in QVulkanWindowRenderer::preInitResources(), and has no effect if + called afterwards. + */ +void QVulkanWindow::setPhysicalDeviceIndex(int idx) +{ + Q_D(QVulkanWindow); + if (d->status != QVulkanWindowPrivate::StatusUninitialized) { + qWarning("QVulkanWindow: Attempted to set physical device when already initialized"); + return; + } + const int count = availablePhysicalDevices().count(); + if (idx < 0 || idx >= count) { + qWarning("QVulkanWindow: Invalid physical device index %d (total physical devices: %d)", idx, count); + return; + } + d->physDevIndex = idx; +} + +/*! + \return the list of the extensions that are supported by logical devices + created from the physical device selected by setPhysicalDeviceIndex(). + + \note This function can be called before making the window visible. + */ +QVulkanInfoVector QVulkanWindow::supportedDeviceExtensions() +{ + Q_D(QVulkanWindow); + + availablePhysicalDevices(); + + if (d->physDevs.isEmpty()) { + qWarning("QVulkanWindow: No physical devices found"); + return QVulkanInfoVector(); + } + + VkPhysicalDevice physDev = d->physDevs.at(d->physDevIndex); + if (d->supportedDevExtensions.contains(physDev)) + return d->supportedDevExtensions.value(physDev); + + QVulkanFunctions *f = vulkanInstance()->functions(); + uint32_t count = 0; + VkResult err = f->vkEnumerateDeviceExtensionProperties(physDev, nullptr, &count, nullptr); + if (err == VK_SUCCESS) { + QVector extProps(count); + err = f->vkEnumerateDeviceExtensionProperties(physDev, nullptr, &count, extProps.data()); + if (err == VK_SUCCESS) { + QVulkanInfoVector exts; + for (const VkExtensionProperties &prop : extProps) { + QVulkanExtension ext; + ext.name = prop.extensionName; + ext.version = prop.specVersion; + exts.append(ext); + } + d->supportedDevExtensions.insert(physDev, exts); + qDebug(lcVk) << "Supported device extensions:" << exts; + return exts; + } + } + + qWarning("QVulkanWindow: Failed to query device extension count: %d", err); + return QVulkanInfoVector(); +} + +/*! + Sets the list of device \a extensions to be enabled. + + Unsupported extensions are ignored. + + The swapchain extension will always be added automatically, no need to + include it in this list. + + \note This function must be called before the window is made visible or at + latest in QVulkanWindowRenderer::preInitResources(), and has no effect if + called afterwards. + */ +void QVulkanWindow::setDeviceExtensions(const QByteArrayList &extensions) +{ + Q_D(QVulkanWindow); + if (d->status != QVulkanWindowPrivate::StatusUninitialized) { + qWarning("QVulkanWindow: Attempted to set device extensions when already initialized"); + return; + } + d->requestedDevExtensions = extensions; +} + +/*! + Sets the preferred \a formats of the swapchain. + + By default no application-preferred format is set. In this case the + surface's preferred format will be used or, in absence of that, + \c{VK_FORMAT_B8G8R8A8_UNORM}. + + The list in \a formats is ordered. If the first format is not supported, + the second will be considered, and so on. When no formats in the list are + supported, the behavior is the same as in the default case. + + To query the actual format after initialization, call colorFormat(). + + \note This function must be called before the window is made visible or at + latest in QVulkanWindowRenderer::preInitResources(), and has no effect if + called afterwards. + + \note Reimplementing QVulkanWindowRenderer::preInitResources() allows + dynamically examining the list of supported formats, should that be + desired. There the surface is retrievable via + QVulkanInstace::surfaceForWindow(), while this function can still safely be + called to affect the later stages of initialization. + + \sa colorFormat() + */ +void QVulkanWindow::setPreferredColorFormats(const QVector &formats) +{ + Q_D(QVulkanWindow); + if (d->status != QVulkanWindowPrivate::StatusUninitialized) { + qWarning("QVulkanWindow: Attempted to set preferred color format when already initialized"); + return; + } + d->requestedColorFormats = formats; +} + +static struct { + VkSampleCountFlagBits mask; + int count; +} qvk_sampleCounts[] = { + { VK_SAMPLE_COUNT_1_BIT, 1 }, + { VK_SAMPLE_COUNT_2_BIT, 2 }, + { VK_SAMPLE_COUNT_4_BIT, 4 }, + { VK_SAMPLE_COUNT_8_BIT, 8 }, + { VK_SAMPLE_COUNT_16_BIT, 16 }, + { VK_SAMPLE_COUNT_32_BIT, 32 }, + { VK_SAMPLE_COUNT_64_BIT, 64 } +}; + +/* + \return the set of supported sample counts when using the physical device + selected by setPhysicalDeviceIndex(). + + By default QVulkanWindow uses a sample count of 1. By calling setSampleCount() + with a different value (2, 4, 8, ...) from the set returned by this + function, multisample anti-aliasing can be requested. + + \note This function can be called before making the window visible. + + \sa setSampleCount() + */ +QSet QVulkanWindow::supportedSampleCounts() +{ + Q_D(const QVulkanWindow); + QSet result; + + availablePhysicalDevices(); + + if (d->physDevs.isEmpty()) { + qWarning("QVulkanWindow: No physical devices found"); + return result; + } + + const VkPhysicalDeviceLimits *limits = &d->physDevProps[d->physDevIndex].limits; + VkSampleCountFlags color = limits->framebufferColorSampleCounts; + VkSampleCountFlags depth = limits->framebufferDepthSampleCounts; + VkSampleCountFlags stencil = limits->framebufferStencilSampleCounts; + + for (size_t i = 0; i < sizeof(qvk_sampleCounts) / sizeof(qvk_sampleCounts[0]); ++i) { + if ((color & qvk_sampleCounts[i].mask) + && (depth & qvk_sampleCounts[i].mask) + && (stencil & qvk_sampleCounts[i].mask)) + { + result.insert(qvk_sampleCounts[i].count); + } + } + + return result; +} + +/*! + Requests multisample antialiasing with the given \a sampleCount. The valid + values are 1, 2, 4, 8, ... up until the maximum value supported by the + physical device. + + When the sample count is greater than 1, QVulkanWindow will create a + multisample color buffer instead of simply targeting the swapchain's + images. The rendering in the multisample buffer will get resolved into the + non-multisample buffers at the end of each frame. + + To examine the list of supported sample counts, call supportedSampleCounts(). + + When setting up the rendering pipeline, call sampleCountFlagBits() to query the + active sample count as a \c VkSampleCountFlagBits value. + + \note This function must be called before the window is made visible or at + latest in QVulkanWindowRenderer::preInitResources(), and has no effect if + called afterwards. + + \sa supportedSampleCounts(), sampleCountFlagBits() + */ +void QVulkanWindow::setSampleCount(int sampleCount) +{ + Q_D(QVulkanWindow); + if (d->status != QVulkanWindowPrivate::StatusUninitialized) { + qWarning("QVulkanWindow: Attempted to set sample count when already initialized"); + return; + } + + // Stay compatible with QSurfaceFormat and friends where samples == 0 means the same as 1. + sampleCount = qBound(1, sampleCount, 64); + + if (!supportedSampleCounts().contains(sampleCount)) { + qWarning("QVulkanWindow: Attempted to set unsupported sample count %d", sampleCount); + return; + } + + for (size_t i = 0; i < sizeof(qvk_sampleCounts) / sizeof(qvk_sampleCounts[0]); ++i) { + if (qvk_sampleCounts[i].count == sampleCount) { + d->sampleCount = qvk_sampleCounts[i].mask; + return; + } + } + + Q_UNREACHABLE(); +} + +void QVulkanWindowPrivate::init() +{ + Q_Q(QVulkanWindow); + Q_ASSERT(status == StatusUninitialized); + + qCDebug(lcVk, "QVulkanWindow init"); + + inst = q->vulkanInstance(); + if (!inst) { + qWarning("QVulkanWindow: Attempted to initialize without a QVulkanInstance"); + // This is a simple user error, recheck on the next expose instead of + // going into the permanent failure state. + status = StatusFailRetry; + return; + } + + if (!renderer) + renderer = q->createRenderer(); + + surface = QVulkanInstance::surfaceForWindow(q); + if (surface == VK_NULL_HANDLE) { + qWarning("QVulkanWindow: Failed to retrieve Vulkan surface for window"); + status = StatusFailRetry; + return; + } + + q->availablePhysicalDevices(); + + if (physDevs.isEmpty()) { + qWarning("QVulkanWindow: No physical devices found"); + status = StatusFail; + return; + } + + if (physDevIndex < 0 || physDevIndex >= physDevs.count()) { + qWarning("QVulkanWindow: Invalid physical device index; defaulting to 0"); + physDevIndex = 0; + } + qCDebug(lcVk, "Using physical device [%d]", physDevIndex); + + // Give a last chance to do decisions based on the physical device and the surface. + if (renderer) + renderer->preInitResources(); + + VkPhysicalDevice physDev = physDevs.at(physDevIndex); + QVulkanFunctions *f = inst->functions(); + + uint32_t queueCount = 0; + f->vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, nullptr); + QVector queueFamilyProps(queueCount); + f->vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, queueFamilyProps.data()); + gfxQueueFamilyIdx = uint32_t(-1); + presQueueFamilyIdx = uint32_t(-1); + for (int i = 0; i < queueFamilyProps.count(); ++i) { + const bool supportsPresent = inst->supportsPresent(physDev, i, q); + qCDebug(lcVk, "queue family %d: flags=0x%x count=%d supportsPresent=%d", i, + queueFamilyProps[i].queueFlags, queueFamilyProps[i].queueCount, supportsPresent); + if (gfxQueueFamilyIdx == uint32_t(-1) + && (queueFamilyProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) + && supportsPresent) + gfxQueueFamilyIdx = i; + } + if (gfxQueueFamilyIdx != uint32_t(-1)) { + presQueueFamilyIdx = gfxQueueFamilyIdx; + } else { + qCDebug(lcVk, "No queue with graphics+present; trying separate queues"); + for (int i = 0; i < queueFamilyProps.count(); ++i) { + if (gfxQueueFamilyIdx == uint32_t(-1) && (queueFamilyProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) + gfxQueueFamilyIdx = i; + if (presQueueFamilyIdx == uint32_t(-1) && inst->supportsPresent(physDev, i, q)) + presQueueFamilyIdx = i; + } + } + if (gfxQueueFamilyIdx == uint32_t(-1)) { + qWarning("QVulkanWindow: No graphics queue family found"); + status = StatusFail; + return; + } + if (presQueueFamilyIdx == uint32_t(-1)) { + qWarning("QVulkanWindow: No present queue family found"); + status = StatusFail; + return; + } +#ifdef QT_DEBUG + // allow testing the separate present queue case in debug builds on AMD cards + if (qEnvironmentVariableIsSet("QT_VK_PRESENT_QUEUE_INDEX")) + presQueueFamilyIdx = qEnvironmentVariableIntValue("QT_VK_PRESENT_QUEUE_INDEX"); +#endif + qCDebug(lcVk, "Using queue families: graphics = %u present = %u", gfxQueueFamilyIdx, presQueueFamilyIdx); + + VkDeviceQueueCreateInfo queueInfo[2]; + const float prio[] = { 0 }; + memset(queueInfo, 0, sizeof(queueInfo)); + queueInfo[0].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo[0].queueFamilyIndex = gfxQueueFamilyIdx; + queueInfo[0].queueCount = 1; + queueInfo[0].pQueuePriorities = prio; + if (gfxQueueFamilyIdx != presQueueFamilyIdx) { + queueInfo[1].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo[1].queueFamilyIndex = presQueueFamilyIdx; + queueInfo[1].queueCount = 1; + queueInfo[1].pQueuePriorities = prio; + } + + // Filter out unsupported extensions in order to keep symmetry + // with how QVulkanInstance behaves. Add the swapchain extension. + QVector devExts; + QVulkanInfoVector supportedExtensions = q->supportedDeviceExtensions(); + QByteArrayList reqExts = requestedDevExtensions; + reqExts.append("VK_KHR_swapchain"); + for (const QByteArray &ext : reqExts) { + if (supportedExtensions.contains(ext)) + devExts.append(ext.constData()); + } + qCDebug(lcVk) << "Enabling device extensions:" << devExts; + + VkDeviceCreateInfo devInfo; + memset(&devInfo, 0, sizeof(devInfo)); + devInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + devInfo.queueCreateInfoCount = gfxQueueFamilyIdx == presQueueFamilyIdx ? 1 : 2; + devInfo.pQueueCreateInfos = queueInfo; + devInfo.enabledExtensionCount = devExts.count(); + devInfo.ppEnabledExtensionNames = devExts.constData(); + + // Device layers are not supported by QVulkanWindow since that's an already deprecated + // API. However, have a workaround for systems with older API and layers (f.ex. L4T + // 24.2 for the Jetson TX1 provides API 1.0.13 and crashes when the validation layer + // is enabled for the instance but not the device). + uint32_t apiVersion = physDevProps[physDevIndex].apiVersion; + if (VK_VERSION_MAJOR(apiVersion) == 1 + && VK_VERSION_MINOR(apiVersion) == 0 + && VK_VERSION_PATCH(apiVersion) <= 13) + { + // Make standard validation work at least. + const QByteArray stdValName = QByteArrayLiteral("VK_LAYER_LUNARG_standard_validation"); + const char *stdValNamePtr = stdValName.constData(); + if (inst->layers().contains(stdValName)) { + uint32_t count = 0; + VkResult err = f->vkEnumerateDeviceLayerProperties(physDev, &count, nullptr); + if (err == VK_SUCCESS) { + QVector layerProps(count); + err = f->vkEnumerateDeviceLayerProperties(physDev, &count, layerProps.data()); + if (err == VK_SUCCESS) { + for (const VkLayerProperties &prop : layerProps) { + if (!strncmp(prop.layerName, stdValNamePtr, stdValName.count())) { + devInfo.enabledLayerCount = 1; + devInfo.ppEnabledLayerNames = &stdValNamePtr; + break; + } + } + } + } + } + } + + VkResult err = f->vkCreateDevice(physDev, &devInfo, nullptr, &dev); + if (err == VK_ERROR_DEVICE_LOST) { + qWarning("QVulkanWindow: Physical device lost"); + if (renderer) + renderer->physicalDeviceLost(); + // clear the caches so the list of physical devices is re-queried + physDevs.clear(); + physDevProps.clear(); + status = StatusUninitialized; + qCDebug(lcVk, "Attempting to restart in 2 seconds"); + QTimer::singleShot(2000, q, [this]() { ensureStarted(); }); + return; + } + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create device: %d", err); + status = StatusFail; + return; + } + + devFuncs = inst->deviceFunctions(dev); + Q_ASSERT(devFuncs); + + devFuncs->vkGetDeviceQueue(dev, gfxQueueFamilyIdx, 0, &gfxQueue); + if (gfxQueueFamilyIdx == presQueueFamilyIdx) + presQueue = gfxQueue; + else + devFuncs->vkGetDeviceQueue(dev, presQueueFamilyIdx, 0, &presQueue); + + VkCommandPoolCreateInfo poolInfo; + memset(&poolInfo, 0, sizeof(poolInfo)); + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.queueFamilyIndex = gfxQueueFamilyIdx; + err = devFuncs->vkCreateCommandPool(dev, &poolInfo, nullptr, &cmdPool); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create command pool: %d", err); + status = StatusFail; + return; + } + if (gfxQueueFamilyIdx != presQueueFamilyIdx) { + poolInfo.queueFamilyIndex = presQueueFamilyIdx; + err = devFuncs->vkCreateCommandPool(dev, &poolInfo, nullptr, &presCmdPool); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create command pool for present queue: %d", err); + status = StatusFail; + return; + } + } + + hostVisibleMemIndex = 0; + VkPhysicalDeviceMemoryProperties physDevMemProps; + bool hostVisibleMemIndexSet = false; + f->vkGetPhysicalDeviceMemoryProperties(physDev, &physDevMemProps); + for (uint32_t i = 0; i < physDevMemProps.memoryTypeCount; ++i) { + const VkMemoryType *memType = physDevMemProps.memoryTypes; + qCDebug(lcVk, "memtype %d: flags=0x%x", i, memType[i].propertyFlags); + // Find a host visible, host coherent memtype. If there is one that is + // cached as well (in addition to being coherent), prefer that. + const int hostVisibleAndCoherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + if ((memType[i].propertyFlags & hostVisibleAndCoherent) == hostVisibleAndCoherent) { + if (!hostVisibleMemIndexSet + || (memType[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) { + hostVisibleMemIndexSet = true; + hostVisibleMemIndex = i; + } + } + } + qCDebug(lcVk, "Picked memtype %d for host visible memory", hostVisibleMemIndex); + deviceLocalMemIndex = 0; + for (uint32_t i = 0; i < physDevMemProps.memoryTypeCount; ++i) { + const VkMemoryType *memType = physDevMemProps.memoryTypes; + // Just pick the first device local memtype. + if (memType[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { + deviceLocalMemIndex = i; + break; + } + } + qCDebug(lcVk, "Picked memtype %d for device local memory", deviceLocalMemIndex); + + if (!vkGetPhysicalDeviceSurfaceCapabilitiesKHR || !vkGetPhysicalDeviceSurfaceFormatsKHR) { + vkGetPhysicalDeviceSurfaceCapabilitiesKHR = reinterpret_cast( + inst->getInstanceProcAddr("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")); + vkGetPhysicalDeviceSurfaceFormatsKHR = reinterpret_cast( + inst->getInstanceProcAddr("vkGetPhysicalDeviceSurfaceFormatsKHR")); + if (!vkGetPhysicalDeviceSurfaceCapabilitiesKHR || !vkGetPhysicalDeviceSurfaceFormatsKHR) { + qWarning("QVulkanWindow: Physical device surface queries not available"); + status = StatusFail; + return; + } + } + + // Figure out the color format here. Must not wait until recreateSwapChain() + // because the renderpass should be available already from initResources (so + // that apps do not have to defer pipeline creation to + // initSwapChainResources), but the renderpass needs the final color format. + + uint32_t formatCount = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &formatCount, nullptr); + QVector formats(formatCount); + if (formatCount) + vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &formatCount, formats.data()); + + colorFormat = VK_FORMAT_B8G8R8A8_UNORM; // our documented default if all else fails + colorSpace = VkColorSpaceKHR(0); // this is in fact VK_COLOR_SPACE_SRGB_NONLINEAR_KHR + + // Pick the preferred format, if there is one. + if (!formats.isEmpty() && formats[0].format != VK_FORMAT_UNDEFINED) { + colorFormat = formats[0].format; + colorSpace = formats[0].colorSpace; + } + + // Try to honor the user request. + if (!formats.isEmpty() && !requestedColorFormats.isEmpty()) { + for (VkFormat reqFmt : qAsConst(requestedColorFormats)) { + auto r = std::find_if(formats.cbegin(), formats.cend(), + [reqFmt](const VkSurfaceFormatKHR &sfmt) { return sfmt.format == reqFmt; }); + if (r != formats.cend()) { + colorFormat = r->format; + colorSpace = r->colorSpace; + break; + } + } + } + + const VkFormat dsFormatCandidates[] = { + VK_FORMAT_D24_UNORM_S8_UINT, + VK_FORMAT_D32_SFLOAT_S8_UINT, + VK_FORMAT_D16_UNORM_S8_UINT + }; + const int dsFormatCandidateCount = sizeof(dsFormatCandidates) / sizeof(VkFormat); + int dsFormatIdx = 0; + while (dsFormatIdx < dsFormatCandidateCount) { + dsFormat = dsFormatCandidates[dsFormatIdx]; + VkFormatProperties fmtProp; + f->vkGetPhysicalDeviceFormatProperties(physDev, dsFormat, &fmtProp); + if (fmtProp.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) + break; + ++dsFormatIdx; + } + if (dsFormatIdx == dsFormatCandidateCount) + qWarning("QVulkanWindow: Failed to find an optimal depth-stencil format"); + + qCDebug(lcVk, "Color format: %d Depth-stencil format: %d", colorFormat, dsFormat); + + if (!createDefaultRenderPass()) + return; + + if (renderer) + renderer->initResources(); + + status = StatusDeviceReady; +} + +void QVulkanWindowPrivate::reset() +{ + if (!dev) // do not rely on 'status', a half done init must be cleaned properly too + return; + + qCDebug(lcVk, "QVulkanWindow reset"); + + devFuncs->vkDeviceWaitIdle(dev); + + if (renderer) + renderer->releaseResources(); + + if (defaultRenderPass) { + devFuncs->vkDestroyRenderPass(dev, defaultRenderPass, nullptr); + defaultRenderPass = VK_NULL_HANDLE; + } + + if (cmdPool) { + devFuncs->vkDestroyCommandPool(dev, cmdPool, nullptr); + cmdPool = VK_NULL_HANDLE; + } + + if (presCmdPool) { + devFuncs->vkDestroyCommandPool(dev, presCmdPool, nullptr); + presCmdPool = VK_NULL_HANDLE; + } + + if (frameGrabImage) { + devFuncs->vkDestroyImage(dev, frameGrabImage, nullptr); + frameGrabImage = VK_NULL_HANDLE; + } + + if (frameGrabImageMem) { + devFuncs->vkFreeMemory(dev, frameGrabImageMem, nullptr); + frameGrabImageMem = VK_NULL_HANDLE; + } + + if (dev) { + devFuncs->vkDestroyDevice(dev, nullptr); + inst->resetDeviceFunctions(dev); + dev = VK_NULL_HANDLE; + vkCreateSwapchainKHR = nullptr; // re-resolve swapchain funcs later on since some come via the device + } + + surface = VK_NULL_HANDLE; + + status = StatusUninitialized; +} + +bool QVulkanWindowPrivate::createDefaultRenderPass() +{ + VkAttachmentDescription attDesc[3]; + memset(attDesc, 0, sizeof(attDesc)); + + const bool msaa = sampleCount > VK_SAMPLE_COUNT_1_BIT; + + // This is either the non-msaa render target or the resolve target. + attDesc[0].format = colorFormat; + attDesc[0].samples = VK_SAMPLE_COUNT_1_BIT; + attDesc[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // ignored when msaa + attDesc[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attDesc[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attDesc[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attDesc[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attDesc[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + attDesc[1].format = dsFormat; + attDesc[1].samples = sampleCount; + attDesc[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attDesc[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attDesc[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attDesc[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attDesc[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attDesc[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + if (msaa) { + // msaa render target + attDesc[2].format = colorFormat; + attDesc[2].samples = sampleCount; + attDesc[2].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attDesc[2].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attDesc[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attDesc[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attDesc[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attDesc[2].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + + VkAttachmentReference colorRef = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkAttachmentReference resolveRef = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkAttachmentReference dsRef = { 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; + + VkSubpassDescription subPassDesc; + memset(&subPassDesc, 0, sizeof(subPassDesc)); + subPassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subPassDesc.colorAttachmentCount = 1; + subPassDesc.pColorAttachments = &colorRef; + subPassDesc.pDepthStencilAttachment = &dsRef; + + VkRenderPassCreateInfo rpInfo; + memset(&rpInfo, 0, sizeof(rpInfo)); + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 2; + rpInfo.pAttachments = attDesc; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subPassDesc; + + if (msaa) { + colorRef.attachment = 2; + subPassDesc.pResolveAttachments = &resolveRef; + rpInfo.attachmentCount = 3; + } + + VkResult err = devFuncs->vkCreateRenderPass(dev, &rpInfo, nullptr, &defaultRenderPass); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create renderpass: %d", err); + return false; + } + + return true; +} + +void QVulkanWindowPrivate::recreateSwapChain() +{ + Q_Q(QVulkanWindow); + Q_ASSERT(status >= StatusDeviceReady); + + swapChainImageSize = q->size() * q->devicePixelRatio(); // note: may change below due to surfaceCaps + + if (swapChainImageSize.isEmpty()) // handle null window size gracefully + return; + + QVulkanInstance *inst = q->vulkanInstance(); + QVulkanFunctions *f = inst->functions(); + devFuncs->vkDeviceWaitIdle(dev); + + if (!vkCreateSwapchainKHR) { + vkCreateSwapchainKHR = reinterpret_cast(f->vkGetDeviceProcAddr(dev, "vkCreateSwapchainKHR")); + vkDestroySwapchainKHR = reinterpret_cast(f->vkGetDeviceProcAddr(dev, "vkDestroySwapchainKHR")); + vkGetSwapchainImagesKHR = reinterpret_cast(f->vkGetDeviceProcAddr(dev, "vkGetSwapchainImagesKHR")); + vkAcquireNextImageKHR = reinterpret_cast(f->vkGetDeviceProcAddr(dev, "vkAcquireNextImageKHR")); + vkQueuePresentKHR = reinterpret_cast(f->vkGetDeviceProcAddr(dev, "vkQueuePresentKHR")); + } + + VkPhysicalDevice physDev = physDevs.at(physDevIndex); + VkSurfaceCapabilitiesKHR surfaceCaps; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDev, surface, &surfaceCaps); + uint32_t reqBufferCount = swapChainBufferCount; + if (surfaceCaps.maxImageCount) + reqBufferCount = qBound(surfaceCaps.minImageCount, reqBufferCount, surfaceCaps.maxImageCount); + + VkExtent2D bufferSize = surfaceCaps.currentExtent; + if (bufferSize.width == uint32_t(-1)) { + Q_ASSERT(bufferSize.height == uint32_t(-1)); + bufferSize.width = swapChainImageSize.width(); + bufferSize.height = swapChainImageSize.height(); + } else { + swapChainImageSize = QSize(bufferSize.width, bufferSize.height); + } + + VkSurfaceTransformFlagBitsKHR preTransform = + (surfaceCaps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) + ? VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR + : surfaceCaps.currentTransform; + + VkCompositeAlphaFlagBitsKHR compositeAlpha = + (surfaceCaps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) + ? VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR + : VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + + if (q->requestedFormat().hasAlpha()) { + if (surfaceCaps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR) + compositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR; + else if (surfaceCaps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR) + compositeAlpha = VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR; + } + + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + swapChainSupportsReadBack = (surfaceCaps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + if (swapChainSupportsReadBack) + usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + VkSwapchainKHR oldSwapChain = swapChain; + VkSwapchainCreateInfoKHR swapChainInfo; + memset(&swapChainInfo, 0, sizeof(swapChainInfo)); + swapChainInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + swapChainInfo.surface = surface; + swapChainInfo.minImageCount = reqBufferCount; + swapChainInfo.imageFormat = colorFormat; + swapChainInfo.imageColorSpace = colorSpace; + swapChainInfo.imageExtent = bufferSize; + swapChainInfo.imageArrayLayers = 1; + swapChainInfo.imageUsage = usage; + swapChainInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + swapChainInfo.preTransform = preTransform; + swapChainInfo.compositeAlpha = compositeAlpha; + swapChainInfo.presentMode = presentMode; + swapChainInfo.clipped = true; + swapChainInfo.oldSwapchain = oldSwapChain; + + qCDebug(lcVk, "Creating new swap chain of %d buffers, size %dx%d", reqBufferCount, bufferSize.width, bufferSize.height); + + VkSwapchainKHR newSwapChain; + VkResult err = vkCreateSwapchainKHR(dev, &swapChainInfo, nullptr, &newSwapChain); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create swap chain: %d", err); + return; + } + + if (oldSwapChain) + releaseSwapChain(); + + swapChain = newSwapChain; + + uint32_t actualSwapChainBufferCount = 0; + err = vkGetSwapchainImagesKHR(dev, swapChain, &actualSwapChainBufferCount, nullptr); + if (err != VK_SUCCESS || actualSwapChainBufferCount < 2) { + qWarning("QVulkanWindow: Failed to get swapchain images: %d (count=%d)", err, actualSwapChainBufferCount); + return; + } + + qCDebug(lcVk, "Actual swap chain buffer count: %d (supportsReadback=%d)", + actualSwapChainBufferCount, swapChainSupportsReadBack); + if (actualSwapChainBufferCount > MAX_SWAPCHAIN_BUFFER_COUNT) { + qWarning("QVulkanWindow: Too many swapchain buffers (%d)", actualSwapChainBufferCount); + return; + } + swapChainBufferCount = actualSwapChainBufferCount; + + VkImage swapChainImages[MAX_SWAPCHAIN_BUFFER_COUNT]; + err = vkGetSwapchainImagesKHR(dev, swapChain, &actualSwapChainBufferCount, swapChainImages); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to get swapchain images: %d", err); + return; + } + + if (!createTransientImage(dsFormat, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, + &dsImage, + &dsMem, + &dsView, + 1)) + { + return; + } + + const bool msaa = sampleCount > VK_SAMPLE_COUNT_1_BIT; + VkImage msaaImages[MAX_SWAPCHAIN_BUFFER_COUNT]; + VkImageView msaaViews[MAX_SWAPCHAIN_BUFFER_COUNT]; + + if (msaa) { + if (!createTransientImage(colorFormat, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + VK_IMAGE_ASPECT_COLOR_BIT, + msaaImages, + &msaaImageMem, + msaaViews, + swapChainBufferCount)) + { + return; + } + } + + VkFenceCreateInfo fenceInfo = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, nullptr, VK_FENCE_CREATE_SIGNALED_BIT }; + + for (int i = 0; i < swapChainBufferCount; ++i) { + ImageResources &image(imageRes[i]); + image.image = swapChainImages[i]; + + if (msaa) { + image.msaaImage = msaaImages[i]; + image.msaaImageView = msaaViews[i]; + } + + VkImageViewCreateInfo imgViewInfo; + memset(&imgViewInfo, 0, sizeof(imgViewInfo)); + imgViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + imgViewInfo.image = swapChainImages[i]; + imgViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + imgViewInfo.format = colorFormat; + imgViewInfo.components.r = VK_COMPONENT_SWIZZLE_R; + imgViewInfo.components.g = VK_COMPONENT_SWIZZLE_G; + imgViewInfo.components.b = VK_COMPONENT_SWIZZLE_B; + imgViewInfo.components.a = VK_COMPONENT_SWIZZLE_A; + imgViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imgViewInfo.subresourceRange.levelCount = imgViewInfo.subresourceRange.layerCount = 1; + err = devFuncs->vkCreateImageView(dev, &imgViewInfo, nullptr, &image.imageView); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create swapchain image view %d: %d", i, err); + return; + } + + err = devFuncs->vkCreateFence(dev, &fenceInfo, nullptr, &image.cmdFence); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create command buffer fence: %d", err); + return; + } + image.cmdFenceWaitable = true; // fence was created in signaled state + + VkImageView views[3] = { image.imageView, + dsView, + msaa ? image.msaaImageView : VK_NULL_HANDLE }; + VkFramebufferCreateInfo fbInfo; + memset(&fbInfo, 0, sizeof(fbInfo)); + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = defaultRenderPass; + fbInfo.attachmentCount = msaa ? 3 : 2; + fbInfo.pAttachments = views; + fbInfo.width = swapChainImageSize.width(); + fbInfo.height = swapChainImageSize.height(); + fbInfo.layers = 1; + VkResult err = devFuncs->vkCreateFramebuffer(dev, &fbInfo, nullptr, &image.fb); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create framebuffer: %d", err); + return; + } + + if (gfxQueueFamilyIdx != presQueueFamilyIdx) { + // pre-build the static image-acquire-on-present-queue command buffer + VkCommandBufferAllocateInfo cmdBufInfo = { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, nullptr, presCmdPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1 }; + err = devFuncs->vkAllocateCommandBuffers(dev, &cmdBufInfo, &image.presTransCmdBuf); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to allocate acquire-on-present-queue command buffer: %d", err); + return; + } + VkCommandBufferBeginInfo cmdBufBeginInfo = { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, + VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, nullptr }; + err = devFuncs->vkBeginCommandBuffer(image.presTransCmdBuf, &cmdBufBeginInfo); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to begin acquire-on-present-queue command buffer: %d", err); + return; + } + VkImageMemoryBarrier presTrans; + memset(&presTrans, 0, sizeof(presTrans)); + presTrans.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + presTrans.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + presTrans.oldLayout = presTrans.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + presTrans.srcQueueFamilyIndex = gfxQueueFamilyIdx; + presTrans.dstQueueFamilyIndex = presQueueFamilyIdx; + presTrans.image = image.image; + presTrans.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + presTrans.subresourceRange.levelCount = presTrans.subresourceRange.layerCount = 1; + devFuncs->vkCmdPipelineBarrier(image.presTransCmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &presTrans); + err = devFuncs->vkEndCommandBuffer(image.presTransCmdBuf); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to end acquire-on-present-queue command buffer: %d", err); + return; + } + } + } + + currentImage = 0; + + VkSemaphoreCreateInfo semInfo = { VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, nullptr, 0 }; + for (int i = 0; i < frameLag; ++i) { + FrameResources &frame(frameRes[i]); + + frame.imageAcquired = false; + frame.imageSemWaitable = false; + + devFuncs->vkCreateFence(dev, &fenceInfo, nullptr, &frame.fence); + frame.fenceWaitable = true; // fence was created in signaled state + + devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.imageSem); + devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.drawSem); + if (gfxQueueFamilyIdx != presQueueFamilyIdx) + devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.presTransSem); + } + + currentFrame = 0; + + if (renderer) + renderer->initSwapChainResources(); + + status = StatusReady; +} + +uint32_t QVulkanWindowPrivate::chooseTransientImageMemType(VkImage img, uint32_t startIndex) +{ + VkPhysicalDeviceMemoryProperties physDevMemProps; + inst->functions()->vkGetPhysicalDeviceMemoryProperties(physDevs[physDevIndex], &physDevMemProps); + + VkMemoryRequirements memReq; + devFuncs->vkGetImageMemoryRequirements(dev, img, &memReq); + uint32_t memTypeIndex = uint32_t(-1); + + if (memReq.memoryTypeBits) { + // Find a device local + lazily allocated, or at least device local memtype. + const VkMemoryType *memType = physDevMemProps.memoryTypes; + bool foundDevLocal = false; + for (uint32_t i = startIndex; i < physDevMemProps.memoryTypeCount; ++i) { + if (memReq.memoryTypeBits & (1 << i)) { + if (memType[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { + if (!foundDevLocal) { + foundDevLocal = true; + memTypeIndex = i; + } + if (memType[i].propertyFlags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) { + memTypeIndex = i; + break; + } + } + } + } + } + + return memTypeIndex; +} + +static inline VkDeviceSize aligned(VkDeviceSize v, VkDeviceSize byteAlign) +{ + return (v + byteAlign - 1) & ~(byteAlign - 1); +} + +bool QVulkanWindowPrivate::createTransientImage(VkFormat format, + VkImageUsageFlags usage, + VkImageAspectFlags aspectMask, + VkImage *images, + VkDeviceMemory *mem, + VkImageView *views, + int count) +{ + VkMemoryRequirements memReq; + VkResult err; + + for (int i = 0; i < count; ++i) { + VkImageCreateInfo imgInfo; + memset(&imgInfo, 0, sizeof(imgInfo)); + imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgInfo.imageType = VK_IMAGE_TYPE_2D; + imgInfo.format = format; + imgInfo.extent.width = swapChainImageSize.width(); + imgInfo.extent.height = swapChainImageSize.height(); + imgInfo.extent.depth = 1; + imgInfo.mipLevels = imgInfo.arrayLayers = 1; + imgInfo.samples = sampleCount; + imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imgInfo.usage = usage | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + + err = devFuncs->vkCreateImage(dev, &imgInfo, nullptr, images + i); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create image: %d", err); + return false; + } + + // Assume the reqs are the same since the images are same in every way. + // Still, call GetImageMemReq for every image, in order to prevent the + // validation layer from complaining. + devFuncs->vkGetImageMemoryRequirements(dev, images[i], &memReq); + } + + VkMemoryAllocateInfo memInfo; + memset(&memInfo, 0, sizeof(memInfo)); + memInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + memInfo.allocationSize = aligned(memReq.size, memReq.alignment) * count; + + uint32_t startIndex = 0; + do { + memInfo.memoryTypeIndex = chooseTransientImageMemType(images[0], startIndex); + if (memInfo.memoryTypeIndex == uint32_t(-1)) { + qWarning("QVulkanWindow: No suitable memory type found"); + return false; + } + startIndex = memInfo.memoryTypeIndex + 1; + qCDebug(lcVk, "Allocating %u bytes for transient image (memtype %u)", + uint32_t(memInfo.allocationSize), memInfo.memoryTypeIndex); + err = devFuncs->vkAllocateMemory(dev, &memInfo, nullptr, mem); + if (err != VK_SUCCESS && err != VK_ERROR_OUT_OF_DEVICE_MEMORY) { + qWarning("QVulkanWindow: Failed to allocate image memory: %d", err); + return false; + } + } while (err != VK_SUCCESS); + + VkDeviceSize ofs = 0; + for (int i = 0; i < count; ++i) { + err = devFuncs->vkBindImageMemory(dev, images[i], *mem, ofs); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to bind image memory: %d", err); + return false; + } + ofs += aligned(memReq.size, memReq.alignment); + + VkImageViewCreateInfo imgViewInfo; + memset(&imgViewInfo, 0, sizeof(imgViewInfo)); + imgViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + imgViewInfo.image = images[i]; + imgViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + imgViewInfo.format = format; + imgViewInfo.components.r = VK_COMPONENT_SWIZZLE_R; + imgViewInfo.components.g = VK_COMPONENT_SWIZZLE_G; + imgViewInfo.components.b = VK_COMPONENT_SWIZZLE_B; + imgViewInfo.components.a = VK_COMPONENT_SWIZZLE_A; + imgViewInfo.subresourceRange.aspectMask = aspectMask; + imgViewInfo.subresourceRange.levelCount = imgViewInfo.subresourceRange.layerCount = 1; + + err = devFuncs->vkCreateImageView(dev, &imgViewInfo, nullptr, views + i); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create image view: %d", err); + return false; + } + } + + return true; +} + +void QVulkanWindowPrivate::releaseSwapChain() +{ + if (!dev || !swapChain) // do not rely on 'status', a half done init must be cleaned properly too + return; + + qCDebug(lcVk, "Releasing swapchain"); + + devFuncs->vkDeviceWaitIdle(dev); + + if (renderer) + renderer->releaseSwapChainResources(); + + for (int i = 0; i < frameLag; ++i) { + FrameResources &frame(frameRes[i]); + if (frame.fence) { + if (frame.fenceWaitable) + devFuncs->vkWaitForFences(dev, 1, &frame.fence, VK_TRUE, UINT64_MAX); + devFuncs->vkDestroyFence(dev, frame.fence, nullptr); + frame.fence = VK_NULL_HANDLE; + frame.fenceWaitable = false; + } + if (frame.imageSem) { + devFuncs->vkDestroySemaphore(dev, frame.imageSem, nullptr); + frame.imageSem = VK_NULL_HANDLE; + } + if (frame.drawSem) { + devFuncs->vkDestroySemaphore(dev, frame.drawSem, nullptr); + frame.drawSem = VK_NULL_HANDLE; + } + if (frame.presTransSem) { + devFuncs->vkDestroySemaphore(dev, frame.presTransSem, nullptr); + frame.presTransSem = VK_NULL_HANDLE; + } + } + + for (int i = 0; i < swapChainBufferCount; ++i) { + ImageResources &image(imageRes[i]); + if (image.cmdFence) { + if (image.cmdFenceWaitable) + devFuncs->vkWaitForFences(dev, 1, &image.cmdFence, VK_TRUE, UINT64_MAX); + devFuncs->vkDestroyFence(dev, image.cmdFence, nullptr); + image.cmdFence = VK_NULL_HANDLE; + image.cmdFenceWaitable = false; + } + if (image.fb) { + devFuncs->vkDestroyFramebuffer(dev, image.fb, nullptr); + image.fb = VK_NULL_HANDLE; + } + if (image.imageView) { + devFuncs->vkDestroyImageView(dev, image.imageView, nullptr); + image.imageView = VK_NULL_HANDLE; + } + if (image.cmdBuf) { + devFuncs->vkFreeCommandBuffers(dev, cmdPool, 1, &image.cmdBuf); + image.cmdBuf = VK_NULL_HANDLE; + } + if (image.presTransCmdBuf) { + devFuncs->vkFreeCommandBuffers(dev, presCmdPool, 1, &image.presTransCmdBuf); + image.presTransCmdBuf = VK_NULL_HANDLE; + } + if (image.msaaImageView) { + devFuncs->vkDestroyImageView(dev, image.msaaImageView, nullptr); + image.msaaImageView = VK_NULL_HANDLE; + } + if (image.msaaImage) { + devFuncs->vkDestroyImage(dev, image.msaaImage, nullptr); + image.msaaImage = VK_NULL_HANDLE; + } + } + + if (msaaImageMem) { + devFuncs->vkFreeMemory(dev, msaaImageMem, nullptr); + msaaImageMem = VK_NULL_HANDLE; + } + + if (dsView) { + devFuncs->vkDestroyImageView(dev, dsView, nullptr); + dsView = VK_NULL_HANDLE; + } + if (dsImage) { + devFuncs->vkDestroyImage(dev, dsImage, nullptr); + dsImage = VK_NULL_HANDLE; + } + if (dsMem) { + devFuncs->vkFreeMemory(dev, dsMem, nullptr); + dsMem = VK_NULL_HANDLE; + } + + if (swapChain) { + vkDestroySwapchainKHR(dev, swapChain, nullptr); + swapChain = VK_NULL_HANDLE; + } + + if (status == StatusReady) + status = StatusDeviceReady; +} + +/*! + \internal + */ +void QVulkanWindow::exposeEvent(QExposeEvent *) +{ + Q_D(QVulkanWindow); + + if (isExposed()) { + d->ensureStarted(); + } else { + if (!d->flags.testFlag(PersistentResources)) { + d->releaseSwapChain(); + d->reset(); + } + } +} + +void QVulkanWindowPrivate::ensureStarted() +{ + Q_Q(QVulkanWindow); + if (status == QVulkanWindowPrivate::StatusFailRetry) + status = QVulkanWindowPrivate::StatusUninitialized; + if (status == QVulkanWindowPrivate::StatusUninitialized) { + init(); + if (status == QVulkanWindowPrivate::StatusDeviceReady) + recreateSwapChain(); + } + if (status == QVulkanWindowPrivate::StatusReady) + q->requestUpdate(); +} + +/*! + \internal + */ +void QVulkanWindow::resizeEvent(QResizeEvent *) +{ + // Nothing to do here - recreating the swapchain is handled when building the next frame. +} + +/*! + \internal + */ +bool QVulkanWindow::event(QEvent *e) +{ + Q_D(QVulkanWindow); + + switch (e->type()) { + case QEvent::UpdateRequest: + d->beginFrame(); + break; + + // The swapchain must be destroyed before the surface as per spec. This is + // not ideal for us because the surface is managed by the QPlatformWindow + // which may be gone already when the unexpose comes, making the validation + // layer scream. The solution is to listen to the PlatformSurface events. + case QEvent::PlatformSurface: + if (static_cast(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { + d->releaseSwapChain(); + d->reset(); + } + break; + + default: + break; + } + + return QWindow::event(e); +} + +/*! + \return true if this window has successfully initialized all Vulkan + resources, including the swapchain. + + \note Initialization happens on the first expose event after the window is + made visible. + */ +bool QVulkanWindow::isValid() const +{ + Q_D(const QVulkanWindow); + return d->status == QVulkanWindowPrivate::StatusReady; +} + +/*! + \return a new instance of QVulkanWindowRenderer. + + This virtual function is called once during the lifetime of the window, at + some point after making it visible for the first time. + + The default implementation returns null and so no rendering will be + performed apart from clearing the buffers. + + The window takes ownership of the returned renderer object. + */ +QVulkanWindowRenderer *QVulkanWindow::createRenderer() +{ + return nullptr; +} + +/*! + Virtual destructor. + */ +QVulkanWindowRenderer::~QVulkanWindowRenderer() +{ +} + +/*! + This virtual function is called right before graphics initialization, that + ends up in calling initResources(), is about to begin. + + Normally there is no need to reimplement this function. However, there are + cases that involve decisions based on both the physical device and the + surface. These cannot normally be performed before making the QVulkanWindow + visible since the Vulkan surface is not retrievable at that stage. + + Instead, applications can reimplement this function. Here both + QVulkanWindow::physicalDevice() and QVulkanInstance::surfaceForWindow() are + functional, but no further logical device initialization has taken place + yet. + + The default implementation is empty. + */ +void QVulkanWindowRenderer::preInitResources() +{ +} + +/*! + This virtual function is called when it is time to create the renderer's + graphics resources. + + Depending on the QVulkanWindow::PersistentResources flag, device lost + situations, etc. this function may be called more than once during the + lifetime of a QVulkanWindow. However, subsequent invocations are always + preceded by a call to releaseResources(). + + Accessors like device(), graphicsQueue() and graphicsCommandPool() are only + guaranteed to return valid values inside this function and afterwards, up + until releaseResources() is called. + + The default implementation is empty. + */ +void QVulkanWindowRenderer::initResources() +{ +} + +/*! + This virtual function is called when swapchain, framebuffer or renderpass + related initialization can be performed. Swapchain and related resources + are reset and then recreated in response to window resize events, and + therefore a pair of calls to initResources() and releaseResources() can + have multiple calls to initSwapChainResources() and + releaseSwapChainResources() calls in-between. + + Accessors like swapChainImageSize() are only guaranteed to return valid + values inside this function and afterwards, up until + releaseSwapChainResources() is called. + + This is also the place where size-dependent calculations (for example, the + projection matrix) should be made since this function is called effectively + on every resize. + + The default implementation is empty. + */ +void QVulkanWindowRenderer::initSwapChainResources() +{ +} + +/*! + This virtual function is called when swapchain, framebuffer or renderpass + related resources must be released. + + The implementation must be prepared that a call to this function may be + followed by a new call to initSwapChainResources() at a later point. + + QVulkanWindow takes care of waiting for the device to become idle before + invoking this function. + + The default implementation is empty. + */ +void QVulkanWindowRenderer::releaseSwapChainResources() +{ +} + +/*! + This virtual function is called when the renderer's graphics resources must be + released. + + The implementation must be prepared that a call to this function may be + followed by an initResources() at a later point. + + QVulkanWindow takes care of waiting for the device to become idle before + invoking this function. + + The default implementation is empty. + */ +void QVulkanWindowRenderer::releaseResources() +{ +} + +/*! + \fn QVulkanWindowRenderer::startNextFrame() + + This virtual function is called when the draw calls for the next frame are + to be added to the command buffer. + + Each call to this function must be followed by a call to + QVulkanWindow::frameReady(). Failing to do so will stall the rendering + loop. The call can also be made at a later time, after returning from this + function. This means that it is possible to kick off asynchronous work, and + only update the command buffer and notify QVulkanWindow when that work has + finished. + + All Vulkan resources are initialized and ready when this function is + invoked. The current framebuffer and main command buffer can be retrieved + via QVulkanWindow::currentFramebuffer() and + QVulkanWindow::currentCommandBuffer(). The logical device and the active + graphics queue are available via QVulkanWindow::device() and + QVulkanWindow::graphicsQueue(). Implementations can create additional + command buffers from the pool returned by + QVulkanWindow::graphicsCommandPool(). For convenience, the index of the + best performing host visible memory type index is exposed via + QVulkanWindow::hostVisibleMemoryIndex(). All these accessors are safe to + invoke from any thread. + + \sa QVulkanWindow::frameReady(), QVulkanWindow + */ + +/*! + This virtual function is called when the physical device is lost, meaning + the creation of the logical device fails with \c{VK_ERROR_DEVICE_LOST}. + + The default implementation is empty. + + There is typically no need to perform anything special in this function + because QVulkanWindow will automatically retry to initialize itself after a + certain amount of time. + + \sa logicalDeviceLost() + */ +void QVulkanWindowRenderer::physicalDeviceLost() +{ +} + +/*! + This virtual function is called when the logical device (VkDevice) is lost, + meaning some operation failed with \c{VK_ERROR_DEVICE_LOST}. + + The default implementation is empty. + + There is typically no need to perform anything special in this function. + QVulkanWindow will automatically release all resources (invoking + releaseSwapChainResources() and releaseResources() as necessary) and will + attempt to reinitialize, acquiring a new device. When the physical device + was also lost, this reinitialization attempt may then result in + physicalDeviceLost(). + + \sa physicalDeviceLost() + */ +void QVulkanWindowRenderer::logicalDeviceLost() +{ +} + +void QVulkanWindowPrivate::beginFrame() +{ + if (!swapChain || framePending) + return; + + Q_Q(QVulkanWindow); + if (q->size() * q->devicePixelRatio() != swapChainImageSize) { + recreateSwapChain(); + if (!swapChain) + return; + } + + FrameResources &frame(frameRes[currentFrame]); + + if (!frame.imageAcquired) { + // Wait if we are too far ahead, i.e. the thread gets throttled based on the presentation rate + // (note that we are using FIFO mode -> vsync) + if (frame.fenceWaitable) { + devFuncs->vkWaitForFences(dev, 1, &frame.fence, VK_TRUE, UINT64_MAX); + devFuncs->vkResetFences(dev, 1, &frame.fence); + frame.fenceWaitable = false; + } + + // move on to next swapchain image + VkResult err = vkAcquireNextImageKHR(dev, swapChain, UINT64_MAX, + frame.imageSem, frame.fence, ¤tImage); + if (err == VK_SUCCESS || err == VK_SUBOPTIMAL_KHR) { + frame.imageSemWaitable = true; + frame.imageAcquired = true; + frame.fenceWaitable = true; + } else if (err == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + q->requestUpdate(); + return; + } else { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to acquire next swapchain image: %d", err); + q->requestUpdate(); + return; + } + } + + // make sure the previous draw for the same image has finished + ImageResources &image(imageRes[currentImage]); + if (image.cmdFenceWaitable) { + devFuncs->vkWaitForFences(dev, 1, &image.cmdFence, VK_TRUE, UINT64_MAX); + devFuncs->vkResetFences(dev, 1, &image.cmdFence); + image.cmdFenceWaitable = false; + } + + // build new draw command buffer + if (image.cmdBuf) { + devFuncs->vkFreeCommandBuffers(dev, cmdPool, 1, &image.cmdBuf); + image.cmdBuf = 0; + } + + VkCommandBufferAllocateInfo cmdBufInfo = { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, nullptr, cmdPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1 }; + VkResult err = devFuncs->vkAllocateCommandBuffers(dev, &cmdBufInfo, &image.cmdBuf); + if (err != VK_SUCCESS) { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to allocate frame command buffer: %d", err); + return; + } + + VkCommandBufferBeginInfo cmdBufBeginInfo = { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, 0, nullptr }; + err = devFuncs->vkBeginCommandBuffer(image.cmdBuf, &cmdBufBeginInfo); + if (err != VK_SUCCESS) { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to begin frame command buffer: %d", err); + return; + } + + if (frameGrabbing) + frameGrabTargetImage = QImage(swapChainImageSize, QImage::Format_RGBA8888); + + if (renderer) { + framePending = true; + renderer->startNextFrame(); + // done for now - endFrame() will get invoked when frameReady() is called back + } else { + VkClearColorValue clearColor = { 0.0f, 0.0f, 0.0f, 1.0f }; + VkClearDepthStencilValue clearDS = { 1.0f, 0 }; + VkClearValue clearValues[3]; + memset(clearValues, 0, sizeof(clearValues)); + clearValues[0].color = clearValues[2].color = clearColor; + clearValues[1].depthStencil = clearDS; + + VkRenderPassBeginInfo rpBeginInfo; + memset(&rpBeginInfo, 0, sizeof(rpBeginInfo)); + rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBeginInfo.renderPass = defaultRenderPass; + rpBeginInfo.framebuffer = image.fb; + rpBeginInfo.renderArea.extent.width = swapChainImageSize.width(); + rpBeginInfo.renderArea.extent.height = swapChainImageSize.height(); + rpBeginInfo.clearValueCount = sampleCount > VK_SAMPLE_COUNT_1_BIT ? 3 : 2; + rpBeginInfo.pClearValues = clearValues; + devFuncs->vkCmdBeginRenderPass(image.cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + devFuncs->vkCmdEndRenderPass(image.cmdBuf); + + endFrame(); + } +} + +void QVulkanWindowPrivate::endFrame() +{ + Q_Q(QVulkanWindow); + + FrameResources &frame(frameRes[currentFrame]); + ImageResources &image(imageRes[currentImage]); + + if (gfxQueueFamilyIdx != presQueueFamilyIdx && !frameGrabbing) { + // Add the swapchain image release to the command buffer that will be + // submitted to the graphics queue. + VkImageMemoryBarrier presTrans; + memset(&presTrans, 0, sizeof(presTrans)); + presTrans.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + presTrans.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + presTrans.oldLayout = presTrans.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + presTrans.srcQueueFamilyIndex = gfxQueueFamilyIdx; + presTrans.dstQueueFamilyIndex = presQueueFamilyIdx; + presTrans.image = image.image; + presTrans.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + presTrans.subresourceRange.levelCount = presTrans.subresourceRange.layerCount = 1; + devFuncs->vkCmdPipelineBarrier(image.cmdBuf, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &presTrans); + } + + // When grabbing a frame, add a readback at the end and skip presenting. + if (frameGrabbing) + addReadback(); + + VkResult err = devFuncs->vkEndCommandBuffer(image.cmdBuf); + if (err != VK_SUCCESS) { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to end frame command buffer: %d", err); + return; + } + + // submit draw calls + VkSubmitInfo submitInfo; + memset(&submitInfo, 0, sizeof(submitInfo)); + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &image.cmdBuf; + if (frame.imageSemWaitable) { + submitInfo.waitSemaphoreCount = 1; + submitInfo.pWaitSemaphores = &frame.imageSem; + } + if (!frameGrabbing) { + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &frame.drawSem; + } + VkPipelineStageFlags psf = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + submitInfo.pWaitDstStageMask = &psf; + + Q_ASSERT(!image.cmdFenceWaitable); + + err = devFuncs->vkQueueSubmit(gfxQueue, 1, &submitInfo, image.cmdFence); + if (err == VK_SUCCESS) { + frame.imageSemWaitable = false; + image.cmdFenceWaitable = true; + } else { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to submit to graphics queue: %d", err); + return; + } + + // block and then bail out when grabbing + if (frameGrabbing) { + finishBlockingReadback(); + frameGrabbing = false; + // Leave frame.imageAcquired set to true. + // Do not change currentFrame. + emit q->frameGrabbed(frameGrabTargetImage); + return; + } + + if (gfxQueueFamilyIdx != presQueueFamilyIdx) { + // Submit the swapchain image acquire to the present queue. + submitInfo.pWaitSemaphores = &frame.drawSem; + submitInfo.pSignalSemaphores = &frame.presTransSem; + submitInfo.pCommandBuffers = &image.presTransCmdBuf; // must be USAGE_SIMULTANEOUS + err = devFuncs->vkQueueSubmit(presQueue, 1, &submitInfo, VK_NULL_HANDLE); + if (err != VK_SUCCESS) { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to submit to present queue: %d", err); + return; + } + } + + // queue present + VkPresentInfoKHR presInfo; + memset(&presInfo, 0, sizeof(presInfo)); + presInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presInfo.swapchainCount = 1; + presInfo.pSwapchains = &swapChain; + presInfo.pImageIndices = ¤tImage; + presInfo.waitSemaphoreCount = 1; + presInfo.pWaitSemaphores = gfxQueueFamilyIdx == presQueueFamilyIdx ? &frame.drawSem : &frame.presTransSem; + + err = vkQueuePresentKHR(gfxQueue, &presInfo); + if (err != VK_SUCCESS) { + if (err == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + q->requestUpdate(); + return; + } else if (err != VK_SUBOPTIMAL_KHR) { + if (!checkDeviceLost(err)) + qWarning("QVulkanWindow: Failed to present: %d", err); + return; + } + } + + frame.imageAcquired = false; + + inst->presentQueued(q); + + currentFrame = (currentFrame + 1) % frameLag; +} + +/*! + This function must be called exactly once in response to each invocation of + the QVulkanWindowRenderer::startNextFrame() implementation. At the time of + this call, the main command buffer, exposed via currentCommandBuffer(), + must have all necessary rendering commands added to it since this function + will trigger submitting the commands and queuing the present command. + + \note This function must only be called from the gui/main thread, which is + where QVulkanWindowRenderer's functions are invoked and where the + QVulkanWindow instance lives. + + \sa QVulkanWindowRenderer::startNextFrame() + */ +void QVulkanWindow::frameReady() +{ + Q_ASSERT_X(QThread::currentThread() == QCoreApplication::instance()->thread(), + "QVulkanWindow", "frameReady() can only be called from the GUI (main) thread"); + + Q_D(QVulkanWindow); + + if (!d->framePending) { + qWarning("QVulkanWindow: frameReady() called without a corresponding startNextFrame()"); + return; + } + + d->framePending = false; + + d->endFrame(); +} + +bool QVulkanWindowPrivate::checkDeviceLost(VkResult err) +{ + if (err == VK_ERROR_DEVICE_LOST) { + qWarning("QVulkanWindow: Device lost"); + if (renderer) + renderer->logicalDeviceLost(); + qCDebug(lcVk, "Releasing all resources due to device lost"); + releaseSwapChain(); + reset(); + qCDebug(lcVk, "Restarting"); + ensureStarted(); + return true; + } + return false; +} + +void QVulkanWindowPrivate::addReadback() +{ + VkImageCreateInfo imageInfo; + memset(&imageInfo, 0, sizeof(imageInfo)); + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.extent.width = frameGrabTargetImage.width(); + imageInfo.extent.height = frameGrabTargetImage.height(); + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_LINEAR; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; + + VkResult err = devFuncs->vkCreateImage(dev, &imageInfo, nullptr, &frameGrabImage); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to create image for readback: %d", err); + return; + } + + VkMemoryRequirements memReq; + devFuncs->vkGetImageMemoryRequirements(dev, frameGrabImage, &memReq); + + VkMemoryAllocateInfo allocInfo = { + VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + nullptr, + memReq.size, + hostVisibleMemIndex + }; + + err = devFuncs->vkAllocateMemory(dev, &allocInfo, nullptr, &frameGrabImageMem); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to allocate memory for readback image: %d", err); + return; + } + + err = devFuncs->vkBindImageMemory(dev, frameGrabImage, frameGrabImageMem, 0); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to bind readback image memory: %d", err); + return; + } + + ImageResources &image(imageRes[currentImage]); + + VkImageMemoryBarrier barrier; + memset(&barrier, 0, sizeof(barrier)); + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.levelCount = barrier.subresourceRange.layerCount = 1; + + barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.image = image.image; + + devFuncs->vkCmdPipelineBarrier(image.cmdBuf, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); + + barrier.oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.image = frameGrabImage; + + devFuncs->vkCmdPipelineBarrier(image.cmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); + + VkImageCopy copyInfo; + memset(©Info, 0, sizeof(copyInfo)); + copyInfo.srcSubresource.aspectMask = copyInfo.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copyInfo.srcSubresource.layerCount = copyInfo.dstSubresource.layerCount = 1; + copyInfo.extent.width = frameGrabTargetImage.width(); + copyInfo.extent.height = frameGrabTargetImage.height(); + copyInfo.extent.depth = 1; + + devFuncs->vkCmdCopyImage(image.cmdBuf, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + frameGrabImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Info); + + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + barrier.image = frameGrabImage; + + devFuncs->vkCmdPipelineBarrier(image.cmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, + 1, &barrier); +} + +void QVulkanWindowPrivate::finishBlockingReadback() +{ + ImageResources &image(imageRes[currentImage]); + + // Block until the current frame is done. Normally this wait would only be + // done in current + concurrentFrameCount(). + devFuncs->vkWaitForFences(dev, 1, &image.cmdFence, VK_TRUE, UINT64_MAX); + devFuncs->vkResetFences(dev, 1, &image.cmdFence); + // will reuse the same image for the next "real" frame, do not wait then + image.cmdFenceWaitable = false; + + VkImageSubresource subres = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0 }; + VkSubresourceLayout layout; + devFuncs->vkGetImageSubresourceLayout(dev, frameGrabImage, &subres, &layout); + + uchar *p; + VkResult err = devFuncs->vkMapMemory(dev, frameGrabImageMem, layout.offset, layout.size, 0, reinterpret_cast(&p)); + if (err != VK_SUCCESS) { + qWarning("QVulkanWindow: Failed to map readback image memory after transfer: %d", err); + return; + } + + for (int y = 0; y < frameGrabTargetImage.height(); ++y) { + memcpy(frameGrabTargetImage.scanLine(y), p, frameGrabTargetImage.width() * 4); + p += layout.rowPitch; + } + + devFuncs->vkUnmapMemory(dev, frameGrabImageMem); + + devFuncs->vkDestroyImage(dev, frameGrabImage, nullptr); + frameGrabImage = VK_NULL_HANDLE; + devFuncs->vkFreeMemory(dev, frameGrabImageMem, nullptr); + frameGrabImageMem = VK_NULL_HANDLE; +} + +/*! + \return the active physical device. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::preInitResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +VkPhysicalDevice QVulkanWindow::physicalDevice() const +{ + Q_D(const QVulkanWindow); + if (d->physDevIndex < d->physDevs.count()) + return d->physDevs[d->physDevIndex]; + qWarning("QVulkanWindow: Physical device not available"); + return VK_NULL_HANDLE; +} + +/*! + \return a pointer to the properties for the active physical device. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::preInitResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +const VkPhysicalDeviceProperties *QVulkanWindow::physicalDeviceProperties() const +{ + Q_D(const QVulkanWindow); + if (d->physDevIndex < d->physDevProps.count()) + return &d->physDevProps[d->physDevIndex]; + qWarning("QVulkanWindow: Physical device properties not available"); + return nullptr; +} + +/*! + \return the active logical device. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +VkDevice QVulkanWindow::device() const +{ + Q_D(const QVulkanWindow); + return d->dev; +} + +/*! + \return the active graphics queue. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +VkQueue QVulkanWindow::graphicsQueue() const +{ + Q_D(const QVulkanWindow); + return d->gfxQueue; +} + +/*! + \return the active graphics command pool. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +VkCommandPool QVulkanWindow::graphicsCommandPool() const +{ + Q_D(const QVulkanWindow); + return d->cmdPool; +} + +/*! + \return a host visible memory type index suitable for general use. + + The returned memory type will be both host visible and coherent. In + addition, it will also be cached, if possible. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +uint32_t QVulkanWindow::hostVisibleMemoryIndex() const +{ + Q_D(const QVulkanWindow); + return d->hostVisibleMemIndex; +} + +/*! + \return a device local memory type index suitable for general use. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +uint32_t QVulkanWindow::deviceLocalMemoryIndex() const +{ + Q_D(const QVulkanWindow); + return d->deviceLocalMemIndex; +} + +/*! + \return a typical render pass with one sub-pass. + + \note Applications are not required to use this render pass. However, they + are then responsible for ensuring the current swap chain and depth-stencil + images get transitioned from \c{VK_IMAGE_LAYOUT_UNDEFINED} to + \c{VK_IMAGE_LAYOUT_PRESENT_SRC_KHR} and + \c{VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL} either via the + application's custom render pass or by other means. + + \note Stencil read/write is not enabled in this render pass. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + + \sa currentFramebuffer() + */ +VkRenderPass QVulkanWindow::defaultRenderPass() const +{ + Q_D(const QVulkanWindow); + return d->defaultRenderPass; +} + +/*! + \return the color buffer format used by the swapchain. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + + \sa setPreferredColorFormats() + */ +VkFormat QVulkanWindow::colorFormat() const +{ + Q_D(const QVulkanWindow); + return d->colorFormat; +} + +/*! + \return the format used by the depth-stencil buffer(s). + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initResources() up until + QVulkanWindowRenderer::releaseResources(). + */ +VkFormat QVulkanWindow::depthStencilFormat() const +{ + Q_D(const QVulkanWindow); + return d->dsFormat; +} + +/*! + \return the image size of the swapchain. + + This usually matches the size of the window, but may also differ in case + \c vkGetPhysicalDeviceSurfaceCapabilitiesKHR reports a fixed size. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +QSize QVulkanWindow::swapChainImageSize() const +{ + Q_D(const QVulkanWindow); + return d->swapChainImageSize; +} + +/*! + \return The active command buffer for the current swap chain image. + Implementations of QVulkanWindowRenderer::startNextFrame() are expected to + add commands to this command buffer. + + \note This function must only be called from within startNextFrame() and, in + case of asynchronous command generation, up until the call to frameReady(). + */ +VkCommandBuffer QVulkanWindow::currentCommandBuffer() const +{ + Q_D(const QVulkanWindow); + if (!d->framePending) { + qWarning("QVulkanWindow: Attempted to call currentCommandBuffer() without an active frame"); + return VK_NULL_HANDLE; + } + return d->imageRes[d->currentImage].cmdBuf; +} + +/*! + \return a VkFramebuffer for the current swapchain image using the default + render pass. + + The framebuffer has two attachments (color, depth-stencil) when + multisampling is not in use, and three (color resolve, depth-stencil, + multisample color) when sampleCountFlagBits() is greater than + \c{VK_SAMPLE_COUNT_1_BIT}. Renderers must take this into account, for + example when providing clear values. + + \note Applications are not required to use this framebuffer in case they + provide their own render pass instead of using the one returned from + defaultRenderPass(). + + \note This function must only be called from within startNextFrame() and, in + case of asynchronous command generation, up until the call to frameReady(). + + \sa defaultRenderPass() + */ +VkFramebuffer QVulkanWindow::currentFramebuffer() const +{ + Q_D(const QVulkanWindow); + if (!d->framePending) { + qWarning("QVulkanWindow: Attempted to call currentFramebuffer() without an active frame"); + return VK_NULL_HANDLE; + } + return d->imageRes[d->currentImage].fb; +} + +/*! + \return the current frame index in the range [0, concurrentFrameCount() - 1]. + + Renderer implementations will have to ensure that uniform data and other + dynamic resources exist in multiple copies, in order to prevent frame N + altering the data used by the still-active frames N - 1, N - 2, ... N - + concurrentFrameCount() + 1. + + To avoid relying on dynamic array sizes, applications can use + MAX_CONCURRENT_FRAME_COUNT when declaring arrays. This is guaranteed to be + always equal to or greater than the value returned from + concurrentFrameCount(). Such arrays can then be indexed by the value + returned from this function. + + \code + class Renderer { + ... + VkDescriptorBufferInfo m_uniformBufInfo[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT]; + }; + + void Renderer::startNextFrame() + { + VkDescriptorBufferInfo &uniformBufInfo(m_uniformBufInfo[m_window->currentFrame()]); + ... + } + \endcode + + \note This function must only be called from within startNextFrame() and, in + case of asynchronous command generation, up until the call to frameReady(). + + \sa concurrentFrameCount() + */ +int QVulkanWindow::currentFrame() const +{ + Q_D(const QVulkanWindow); + if (!d->framePending) + qWarning("QVulkanWindow: Attempted to call currentFrame() without an active frame"); + return d->currentFrame; +} + +/*! + \variable QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT + + \brief A constant value that is always equal to or greater than the maximum value + of concurrentFrameCount(). + */ + +/*! + \return the number of frames that can be potentially active at the same time. + + \note The value is constant for the entire lifetime of the QVulkanWindow. + + \code + class Renderer { + ... + VkDescriptorBufferInfo m_uniformBufInfo[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT]; + }; + + void Renderer::startNextFrame() + { + const int count = m_window->concurrentFrameCount(); + for (int i = 0; i < count; ++i) + m_uniformBufInfo[i] = ... + ... + } + \endcode + + \sa currentFrame() + */ +int QVulkanWindow::concurrentFrameCount() const +{ + Q_D(const QVulkanWindow); + return d->frameLag; +} + +/*! + \return the number of images in the swap chain. + + \note Accessing this is necessary when providing a custom render pass and + framebuffer. The framebuffer is specific to the current swapchain image and + hence the application must provide multiple framebuffers. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +int QVulkanWindow::swapChainImageCount() const +{ + Q_D(const QVulkanWindow); + return d->swapChainBufferCount; +} + +/*! + \return the current swap chain image index in the range [0, swapChainImageCount() - 1]. + + \note This function must only be called from within startNextFrame() and, in + case of asynchronous command generation, up until the call to frameReady(). + */ +int QVulkanWindow::currentSwapChainImageIndex() const +{ + Q_D(const QVulkanWindow); + if (!d->framePending) + qWarning("QVulkanWindow: Attempted to call currentSwapChainImageIndex() without an active frame"); + return d->currentImage; +} + +/*! + \return the specified swap chain image. + + \a idx must be in the range [0, swapChainImageCount() - 1]. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +VkImage QVulkanWindow::swapChainImage(int idx) const +{ + Q_D(const QVulkanWindow); + return idx >= 0 && idx < d->swapChainBufferCount ? d->imageRes[idx].image : VK_NULL_HANDLE; +} + +/*! + \return the specified swap chain image view. + + \a idx must be in the range [0, swapChainImageCount() - 1]. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +VkImageView QVulkanWindow::swapChainImageView(int idx) const +{ + Q_D(const QVulkanWindow); + return idx >= 0 && idx < d->swapChainBufferCount ? d->imageRes[idx].imageView : VK_NULL_HANDLE; +} + +/*! + \return the depth-stencil image. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +VkImage QVulkanWindow::depthStencilImage() const +{ + Q_D(const QVulkanWindow); + return d->dsImage; +} + +/*! + \return the depth-stencil image view. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +VkImageView QVulkanWindow::depthStencilImageView() const +{ + Q_D(const QVulkanWindow); + return d->dsView; +} + +/*! + \return the current sample count as a \c VkSampleCountFlagBits value. + + When targeting the default render target, the \c rasterizationSamples field + of \c VkPipelineMultisampleStateCreateInfo must be set to this value. + + \sa setSampleCount(), supportedSampleCounts() + */ +VkSampleCountFlagBits QVulkanWindow::sampleCountFlagBits() const +{ + Q_D(const QVulkanWindow); + return d->sampleCount; +} + +/*! + \return the specified multisample color image, or \c{VK_NULL_HANDLE} if + multisampling is not in use. + + \a idx must be in the range [0, swapChainImageCount() - 1]. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +VkImage QVulkanWindow::msaaColorImage(int idx) const +{ + Q_D(const QVulkanWindow); + return idx >= 0 && idx < d->swapChainBufferCount ? d->imageRes[idx].msaaImage : VK_NULL_HANDLE; +} + +/*! + \return the specified multisample color image view, or \c{VK_NULL_HANDLE} if + multisampling is not in use. + + \a idx must be in the range [0, swapChainImageCount() - 1]. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +VkImageView QVulkanWindow::msaaColorImageView(int idx) const +{ + Q_D(const QVulkanWindow); + return idx >= 0 && idx < d->swapChainBufferCount ? d->imageRes[idx].msaaImageView : VK_NULL_HANDLE; +} + +/*! + \return true if the swapchain supports usage as transfer source, meaning + grab() is functional. + + \note Calling this function is only valid from the invocation of + QVulkanWindowRenderer::initSwapChainResources() up until + QVulkanWindowRenderer::releaseSwapChainResources(). + */ +bool QVulkanWindow::supportsGrab() const +{ + Q_D(const QVulkanWindow); + return d->swapChainSupportsReadBack; +} + +/*! + Builds and renders the next frame without presenting it, then performs a + blocking readback of the image content. + + \return the image if the renderer's + \l{QVulkanWindowRenderer::startNextFrame()}{startNextFrame()} + implementation calls back frameReady() directly. Otherwise, returns an + incomplete image, that has the correct size but not the content yet. The + content will be delivered via the frameGrabbed() signal in the latter case. + + \note This function should not be called when a frame is in progress + (that is, frameReady() has not yet been called back by the application). + + \note This function is potentially expensive due to the additional, + blocking readback. + + \note This function currently requires that the swapchain supports usage as + a transfer source (\c{VK_IMAGE_USAGE_TRANSFER_SRC_BIT}), and will fail otherwise. + */ +QImage QVulkanWindow::grab() +{ + Q_D(QVulkanWindow); + if (!d->swapChain) { + qWarning("QVulkanWindow: Attempted to call grab() without a swapchain"); + return QImage(); + } + if (d->framePending) { + qWarning("QVulkanWindow: Attempted to call grab() while a frame is still pending"); + return QImage(); + } + if (!d->swapChainSupportsReadBack) { + qWarning("QVulkanWindow: Attempted to call grab() with a swapchain that does not support usage as transfer source"); + return QImage(); + } + + d->frameGrabbing = true; + d->beginFrame(); + + return d->frameGrabTargetImage; +} + +/*! + \return a pointer to a QMatrix4x4 that can be used to correct for coordinate + system differences between OpenGl and Vulkan. + + By pre-multiplying the projection matrix with this matrix, applications can + continue to assume OpenGL-style Y coordinates in clip space (i.e. Y pointing + upwards), and can set minDepth and maxDepth to 0 and 1, respectively, + without any further corrections to the vertex Z positions, while using the + projection matrices retrieved from the QMatrix4x4 functions, such as + QMatrix4x4::perspective(), as-is. + */ +const QMatrix4x4 *QVulkanWindow::clipCorrectionMatrix() +{ + Q_D(QVulkanWindow); + if (d->m_clipCorrect.isIdentity()) { + // NB the ctor takes row-major + d->m_clipCorrect = QMatrix4x4(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, -1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.5f, + 0.0f, 0.0f, 0.0f, 1.0f); + } + return &d->m_clipCorrect; +} + +QT_END_NAMESPACE diff --git a/src/gui/vulkan/qvulkanwindow.h b/src/gui/vulkan/qvulkanwindow.h new file mode 100644 index 00000000000..854da81b05a --- /dev/null +++ b/src/gui/vulkan/qvulkanwindow.h @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QVULKANWINDOW_H +#define QVULKANWINDOW_H + +#include + +#if QT_CONFIG(vulkan) + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QVulkanWindowPrivate; + +class Q_GUI_EXPORT QVulkanWindowRenderer +{ +public: + virtual ~QVulkanWindowRenderer(); + + virtual void preInitResources(); + virtual void initResources(); + virtual void initSwapChainResources(); + virtual void releaseSwapChainResources(); + virtual void releaseResources(); + + virtual void startNextFrame() = 0; + + virtual void physicalDeviceLost(); + virtual void logicalDeviceLost(); +}; + +class Q_GUI_EXPORT QVulkanWindow : public QWindow +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QVulkanWindow) + +public: + enum Flag { + PersistentResources = 0x01 + }; + Q_DECLARE_FLAGS(Flags, Flag) + + explicit QVulkanWindow(QWindow *parent = nullptr); + ~QVulkanWindow(); + + void setFlags(Flags flags); + Flags flags() const; + + QVector availablePhysicalDevices(); + void setPhysicalDeviceIndex(int idx); + + QVulkanInfoVector supportedDeviceExtensions(); + void setDeviceExtensions(const QByteArrayList &extensions); + + void setPreferredColorFormats(const QVector &formats); + + QSet supportedSampleCounts(); + void setSampleCount(int sampleCount); + + bool isValid() const; + + virtual QVulkanWindowRenderer *createRenderer(); + void frameReady(); + + VkPhysicalDevice physicalDevice() const; + const VkPhysicalDeviceProperties *physicalDeviceProperties() const; + VkDevice device() const; + VkQueue graphicsQueue() const; + VkCommandPool graphicsCommandPool() const; + uint32_t hostVisibleMemoryIndex() const; + uint32_t deviceLocalMemoryIndex() const; + VkRenderPass defaultRenderPass() const; + + VkFormat colorFormat() const; + VkFormat depthStencilFormat() const; + QSize swapChainImageSize() const; + + VkCommandBuffer currentCommandBuffer() const; + VkFramebuffer currentFramebuffer() const; + int currentFrame() const; + + static const int MAX_CONCURRENT_FRAME_COUNT = 3; + int concurrentFrameCount() const; + + int swapChainImageCount() const; + int currentSwapChainImageIndex() const; + VkImage swapChainImage(int idx) const; + VkImageView swapChainImageView(int idx) const; + VkImage depthStencilImage() const; + VkImageView depthStencilImageView() const; + + VkSampleCountFlagBits sampleCountFlagBits() const; + VkImage msaaColorImage(int idx) const; + VkImageView msaaColorImageView(int idx) const; + + bool supportsGrab() const; + QImage grab(); + + const QMatrix4x4 *clipCorrectionMatrix(); + +Q_SIGNALS: + void frameGrabbed(const QImage &image); + +protected: + void exposeEvent(QExposeEvent *) override; + void resizeEvent(QResizeEvent *) override; + bool event(QEvent *) override; + +private: + Q_DISABLE_COPY(QVulkanWindow) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QVulkanWindow::Flags) + +QT_END_NAMESPACE + +#endif // QT_CONFIG(vulkan) + +#endif diff --git a/src/gui/vulkan/qvulkanwindow_p.h b/src/gui/vulkan/qvulkanwindow_p.h new file mode 100644 index 00000000000..9d2f13c87e8 --- /dev/null +++ b/src/gui/vulkan/qvulkanwindow_p.h @@ -0,0 +1,188 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QVULKANWINDOW_P_H +#define QVULKANWINDOW_P_H + +#include + +#if QT_CONFIG(vulkan) + +#include "qvulkanwindow.h" +#include +#include + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class QVulkanWindowPrivate : public QWindowPrivate +{ + Q_DECLARE_PUBLIC(QVulkanWindow) + +public: + ~QVulkanWindowPrivate(); + + void ensureStarted(); + void init(); + void reset(); + bool createDefaultRenderPass(); + void recreateSwapChain(); + uint32_t chooseTransientImageMemType(VkImage img, uint32_t startIndex); + bool createTransientImage(VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspectMask, + VkImage *images, VkDeviceMemory *mem, VkImageView *views, int count); + void releaseSwapChain(); + void beginFrame(); + void endFrame(); + bool checkDeviceLost(VkResult err); + void addReadback(); + void finishBlockingReadback(); + + enum Status { + StatusUninitialized, + StatusFail, + StatusFailRetry, + StatusDeviceReady, + StatusReady + }; + Status status = StatusUninitialized; + QVulkanWindowRenderer *renderer = nullptr; + QVulkanInstance *inst = nullptr; + VkSurfaceKHR surface = VK_NULL_HANDLE; + int physDevIndex = 0; + QVector physDevs; + QVector physDevProps; + QVulkanWindow::Flags flags = 0; + QByteArrayList requestedDevExtensions; + QHash > supportedDevExtensions; + QVector requestedColorFormats; + VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; + + VkDevice dev = VK_NULL_HANDLE; + QVulkanDeviceFunctions *devFuncs; + uint32_t gfxQueueFamilyIdx; + uint32_t presQueueFamilyIdx; + VkQueue gfxQueue; + VkQueue presQueue; + VkCommandPool cmdPool = VK_NULL_HANDLE; + VkCommandPool presCmdPool = VK_NULL_HANDLE; + uint32_t hostVisibleMemIndex; + uint32_t deviceLocalMemIndex; + VkFormat colorFormat; + VkColorSpaceKHR colorSpace; + VkFormat dsFormat = VK_FORMAT_D24_UNORM_S8_UINT; + + PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR = nullptr; + PFN_vkDestroySwapchainKHR vkDestroySwapchainKHR; + PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR; + PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR; + PFN_vkQueuePresentKHR vkQueuePresentKHR; + PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vkGetPhysicalDeviceSurfaceCapabilitiesKHR = nullptr; + PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR; + + static const int MAX_SWAPCHAIN_BUFFER_COUNT = 3; + static const int MAX_FRAME_LAG = QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT; + // QVulkanWindow only supports the always available FIFO mode. The + // rendering thread will get throttled to the presentation rate (vsync). + // This is in effect Example 5 from the VK_KHR_swapchain spec. + VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; + int swapChainBufferCount = 2; + int frameLag = 2; + + QSize swapChainImageSize; + VkSwapchainKHR swapChain = VK_NULL_HANDLE; + bool swapChainSupportsReadBack = false; + + struct ImageResources { + VkImage image = VK_NULL_HANDLE; + VkImageView imageView = VK_NULL_HANDLE; + VkCommandBuffer cmdBuf = VK_NULL_HANDLE; + VkFence cmdFence = VK_NULL_HANDLE; + bool cmdFenceWaitable = false; + VkFramebuffer fb = VK_NULL_HANDLE; + VkCommandBuffer presTransCmdBuf = VK_NULL_HANDLE; + VkImage msaaImage = VK_NULL_HANDLE; + VkImageView msaaImageView = VK_NULL_HANDLE; + } imageRes[MAX_SWAPCHAIN_BUFFER_COUNT]; + + VkDeviceMemory msaaImageMem = VK_NULL_HANDLE; + + uint32_t currentImage; + + struct FrameResources { + VkFence fence = VK_NULL_HANDLE; + bool fenceWaitable = false; + VkSemaphore imageSem = VK_NULL_HANDLE; + VkSemaphore drawSem = VK_NULL_HANDLE; + VkSemaphore presTransSem = VK_NULL_HANDLE; + bool imageAcquired = false; + bool imageSemWaitable = false; + } frameRes[MAX_FRAME_LAG]; + + uint32_t currentFrame; + + VkRenderPass defaultRenderPass = VK_NULL_HANDLE; + + VkDeviceMemory dsMem = VK_NULL_HANDLE; + VkImage dsImage = VK_NULL_HANDLE; + VkImageView dsView = VK_NULL_HANDLE; + + bool framePending = false; + bool frameGrabbing = false; + QImage frameGrabTargetImage; + VkImage frameGrabImage = VK_NULL_HANDLE; + VkDeviceMemory frameGrabImageMem = VK_NULL_HANDLE; + + QMatrix4x4 m_clipCorrect; +}; + +QT_END_NAMESPACE + +#endif // QT_CONFIG(vulkan) + +#endif diff --git a/src/gui/vulkan/vulkan.pri b/src/gui/vulkan/vulkan.pri index 25635a84c48..b9d2a9b4b5c 100644 --- a/src/gui/vulkan/vulkan.pri +++ b/src/gui/vulkan/vulkan.pri @@ -1,12 +1,15 @@ qtConfig(vulkan) { HEADERS += \ vulkan/qvulkaninstance.h \ - vulkan/qplatformvulkaninstance.h + vulkan/qplatformvulkaninstance.h \ + vulkan/qvulkanwindow.h \ + vulkan/qvulkanwindow_p.h SOURCES += \ vulkan/qvulkaninstance.cpp \ vulkan/qplatformvulkaninstance.cpp \ - vulkan/qvulkanfunctions.cpp + vulkan/qvulkanfunctions.cpp \ + vulkan/qvulkanwindow.cpp # Applications must inherit the Vulkan header include path. QMAKE_USE += vulkan/nolink diff --git a/tests/auto/gui/qvulkan/tst_qvulkan.cpp b/tests/auto/gui/qvulkan/tst_qvulkan.cpp index 2803b84e8c5..80279350036 100644 --- a/tests/auto/gui/qvulkan/tst_qvulkan.cpp +++ b/tests/auto/gui/qvulkan/tst_qvulkan.cpp @@ -28,7 +28,7 @@ #include #include -#include +#include #include @@ -41,8 +41,11 @@ class tst_QVulkan : public QObject private slots: void vulkanInstance(); void vulkanCheckSupported(); - void vulkanWindow(); + void vulkanPlainWindow(); void vulkanVersionRequest(); + void vulkanWindow(); + void vulkanWindowRenderer(); + void vulkanWindowGrab(); }; void tst_QVulkan::vulkanInstance() @@ -102,7 +105,7 @@ void tst_QVulkan::vulkanCheckSupported() } } -void tst_QVulkan::vulkanWindow() +void tst_QVulkan::vulkanPlainWindow() { QVulkanInstance inst; if (!inst.create()) @@ -154,6 +157,279 @@ void tst_QVulkan::vulkanVersionRequest() QCOMPARE(inst.errorCode(), VK_ERROR_INCOMPATIBLE_DRIVER); } +static void waitForUnexposed(QWindow *w) +{ + QElapsedTimer timer; + timer.start(); + while (w->isExposed()) { + int remaining = 5000 - int(timer.elapsed()); + if (remaining <= 0) + break; + QCoreApplication::processEvents(QEventLoop::AllEvents, remaining); + QCoreApplication::sendPostedEvents(Q_NULLPTR, QEvent::DeferredDelete); + QTest::qSleep(10); + } +} + +void tst_QVulkan::vulkanWindow() +{ + QVulkanInstance inst; + if (!inst.create()) + QSKIP("Vulkan init failed; skip"); + + // First let's forget to set the instance. + QVulkanWindow w; + QVERIFY(!w.isValid()); + w.resize(1024, 768); + w.show(); + QTest::qWaitForWindowExposed(&w); + QVERIFY(!w.isValid()); + + // Now set it. A simple hide - show should be enough to correct, this, no + // need for a full destroy - create. + w.hide(); + waitForUnexposed(&w); + w.setVulkanInstance(&inst); + QVector pdevs = w.availablePhysicalDevices(); + if (pdevs.isEmpty()) + QSKIP("No Vulkan physical devices; skip"); + w.show(); + QTest::qWaitForWindowExposed(&w); + QVERIFY(w.isValid()); + QCOMPARE(w.vulkanInstance(), &inst); + QVulkanInfoVector exts = w.supportedDeviceExtensions(); + + // Now destroy and recreate. + w.destroy(); + waitForUnexposed(&w); + QVERIFY(!w.isValid()); + // check that flags can be set between a destroy() - show() + w.setFlags(QVulkanWindow::PersistentResources); + // supported lists can be queried before expose too + QVERIFY(w.supportedDeviceExtensions() == exts); + w.show(); + QTest::qWaitForWindowExposed(&w); + QVERIFY(w.isValid()); + QVERIFY(w.flags().testFlag(QVulkanWindow::PersistentResources)); + + QVERIFY(w.physicalDevice() != VK_NULL_HANDLE); + QVERIFY(w.physicalDeviceProperties() != nullptr); + QVERIFY(w.device() != VK_NULL_HANDLE); + QVERIFY(w.graphicsQueue() != VK_NULL_HANDLE); + QVERIFY(w.graphicsCommandPool() != VK_NULL_HANDLE); + QVERIFY(w.defaultRenderPass() != VK_NULL_HANDLE); + + QVERIFY(w.concurrentFrameCount() > 0); + QVERIFY(w.concurrentFrameCount() <= QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT); +} + +class TestVulkanRenderer; + +class TestVulkanWindow : public QVulkanWindow +{ +public: + QVulkanWindowRenderer *createRenderer() override; + +private: + TestVulkanRenderer *m_renderer = nullptr; +}; + +struct TestVulkan { + int preInitResCount = 0; + int initResCount = 0; + int initSwcResCount = 0; + int releaseResCount = 0; + int releaseSwcResCount = 0; + int startNextFrameCount = 0; +} testVulkan; + +class TestVulkanRenderer : public QVulkanWindowRenderer +{ +public: + TestVulkanRenderer(QVulkanWindow *w) : m_window(w) { } + + void preInitResources() override; + void initResources() override; + void initSwapChainResources() override; + void releaseSwapChainResources() override; + void releaseResources() override; + + void startNextFrame() override; + +private: + QVulkanWindow *m_window; + QVulkanDeviceFunctions *m_devFuncs; +}; + +void TestVulkanRenderer::preInitResources() +{ + if (testVulkan.initResCount) { + qWarning("initResources called before preInitResources?!"); + testVulkan.preInitResCount = -1; + return; + } + + // Ensure the physical device and the surface are available at this stage. + VkPhysicalDevice physDev = m_window->physicalDevice(); + if (physDev == VK_NULL_HANDLE) { + qWarning("No physical device in preInitResources"); + testVulkan.preInitResCount = -1; + return; + } + VkSurfaceKHR surface = m_window->vulkanInstance()->surfaceForWindow(m_window); + if (surface == VK_NULL_HANDLE) { + qWarning("No surface in preInitResources"); + testVulkan.preInitResCount = -1; + return; + } + + ++testVulkan.preInitResCount; +} + +void TestVulkanRenderer::initResources() +{ + m_devFuncs = m_window->vulkanInstance()->deviceFunctions(m_window->device()); + ++testVulkan.initResCount; +} + +void TestVulkanRenderer::initSwapChainResources() +{ + ++testVulkan.initSwcResCount; +} + +void TestVulkanRenderer::releaseSwapChainResources() +{ + ++testVulkan.releaseSwcResCount; +} + +void TestVulkanRenderer::releaseResources() +{ + ++testVulkan.releaseResCount; +} + +void TestVulkanRenderer::startNextFrame() +{ + ++testVulkan.startNextFrameCount; + + VkClearColorValue clearColor = { 0, 1, 0, 1 }; + VkClearDepthStencilValue clearDS = { 1, 0 }; + VkClearValue clearValues[2]; + memset(clearValues, 0, sizeof(clearValues)); + clearValues[0].color = clearColor; + clearValues[1].depthStencil = clearDS; + + VkRenderPassBeginInfo rpBeginInfo; + memset(&rpBeginInfo, 0, sizeof(rpBeginInfo)); + rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBeginInfo.renderPass = m_window->defaultRenderPass(); + rpBeginInfo.framebuffer = m_window->currentFramebuffer(); + const QSize sz = m_window->swapChainImageSize(); + rpBeginInfo.renderArea.extent.width = sz.width(); + rpBeginInfo.renderArea.extent.height = sz.height(); + rpBeginInfo.clearValueCount = 2; + rpBeginInfo.pClearValues = clearValues; + VkCommandBuffer cmdBuf = m_window->currentCommandBuffer(); + m_devFuncs->vkCmdBeginRenderPass(cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + + m_devFuncs->vkCmdEndRenderPass(cmdBuf); + + m_window->frameReady(); +} + +QVulkanWindowRenderer *TestVulkanWindow::createRenderer() +{ + Q_ASSERT(!m_renderer); + m_renderer = new TestVulkanRenderer(this); + return m_renderer; +} + +void tst_QVulkan::vulkanWindowRenderer() +{ + QVulkanInstance inst; + if (!inst.create()) + QSKIP("Vulkan init failed; skip"); + + testVulkan = TestVulkan(); + + TestVulkanWindow w; + w.setVulkanInstance(&inst); + w.resize(1024, 768); + w.show(); + QTest::qWaitForWindowExposed(&w); + + if (w.availablePhysicalDevices().isEmpty()) + QSKIP("No Vulkan physical devices; skip"); + + QVERIFY(testVulkan.preInitResCount == 1); + QVERIFY(testVulkan.initResCount == 1); + QVERIFY(testVulkan.initSwcResCount == 1); + // this has to be QTRY due to the async update in QVulkanWindowPrivate::ensureStarted() + QTRY_VERIFY(testVulkan.startNextFrameCount >= 1); + + QVERIFY(!w.swapChainImageSize().isEmpty()); + QVERIFY(w.colorFormat() != VK_FORMAT_UNDEFINED); + QVERIFY(w.depthStencilFormat() != VK_FORMAT_UNDEFINED); + + w.destroy(); + waitForUnexposed(&w); + QVERIFY(testVulkan.releaseSwcResCount == 1); + QVERIFY(testVulkan.releaseResCount == 1); +} + +void tst_QVulkan::vulkanWindowGrab() +{ + QVulkanInstance inst; + inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation"); + if (!inst.create()) + QSKIP("Vulkan init failed; skip"); + + testVulkan = TestVulkan(); + + TestVulkanWindow w; + w.setVulkanInstance(&inst); + w.resize(1024, 768); + w.show(); + QTest::qWaitForWindowExposed(&w); + + if (w.availablePhysicalDevices().isEmpty()) + QSKIP("No Vulkan physical devices; skip"); + + if (!w.supportsGrab()) + QSKIP("No grab support; skip"); + + QVERIFY(!w.swapChainImageSize().isEmpty()); + + QImage img1 = w.grab(); + QImage img2 = w.grab(); + QImage img3 = w.grab(); + + QVERIFY(!img1.isNull()); + QVERIFY(!img2.isNull()); + QVERIFY(!img3.isNull()); + + QCOMPARE(img1.size(), w.swapChainImageSize()); + QCOMPARE(img2.size(), w.swapChainImageSize()); + QCOMPARE(img3.size(), w.swapChainImageSize()); + + QRgb a = img1.pixel(10, 20); + QRgb b = img2.pixel(5, 5); + QRgb c = img3.pixel(50, 30); + + QCOMPARE(a, b); + QCOMPARE(b, c); + QRgb refPixel = qRgb(0, 255, 0); + + int redFuzz = qAbs(qRed(a) - qRed(refPixel)); + int greenFuzz = qAbs(qGreen(a) - qGreen(refPixel)); + int blueFuzz = qAbs(qBlue(a) - qBlue(refPixel)); + + QVERIFY(redFuzz <= 1); + QVERIFY(blueFuzz <= 1); + QVERIFY(greenFuzz <= 1); + + w.destroy(); +} + QTEST_MAIN(tst_QVulkan) #include "tst_qvulkan.moc"