// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qvulkanwindow_p.h"
#include "qvulkanfunctions.h"
#include <QLoggingCategory>
#include <QTimer>
#include <QThread>
#include <QCoreApplication>
#include <qevent.h>

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(lcGuiVk)

/*!
  \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:

  \snippet code/src_gui_vulkan_qvulkanwindow.cpp 0

  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.

  \section1 Layers, device features, and extensions

  To enable instance layers, call QVulkanInstance::setLayers() before creating
  the QVulkanInstance. To query what instance layer are available, call
  QVulkanInstance::supportedLayers().

  To enable device extensions, call setDeviceExtensions() early on when setting
  up the QVulkanWindow. To query what device extensions are available, call
  supportedDeviceExtensions().

  Specifying an unsupported layer or extension is handled gracefully: this will
  not fail instance or device creation, but the layer or extension request is
  rather ignored.

  When it comes to device features, QVulkanWindow enables all Vulkan 1.0
  features that are reported as supported from vkGetPhysicalDeviceFeatures().
  As an exception to this rule, \c robustBufferAccess is never enabled. Use the
  callback mechanism described below, if enabling that feature is desired.

  This is not always desirable, and may be insufficient with Vulkan 1.1 and
  higher. Therefore, full control over the VkPhysicalDeviceFeatures used for
  device creation is possible too by registering a callback function with
  setEnabledFeaturesModifier(). When set, the callback function is invoked,
  letting it alter the VkPhysicalDeviceFeatures or VkPhysicalDeviceFeatures2.

  \sa QVulkanInstance, QWindow
 */

/*!
  \class QVulkanWindowRenderer
  \inmodule QtGui
  \since 5.10

  \brief The QVulkanWindowRenderer class is used to implement the
  application-specific rendering logic for a QVulkanWindow.

  Applications typically subclass both QVulkanWindow and QVulkanWindowRenderer.
  The former allows handling events, for example, input, while the latter allows
  implementing the Vulkan resource management and command buffer building that
  make up the application's rendering.

  In addition to event handling, the QVulkanWindow subclass is responsible for
  providing an implementation for QVulkanWindow::createRenderer() as well. This
  is where the window and renderer get connected. A typical implementation will
  simply create a new instance of a subclass of QVulkanWindowRenderer.
 */

/*!
    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;
}

/*!
   Returns the list of properties for the supported physical devices in the system.

   \note This function can be called before making the window visible.
 */
QList<VkPhysicalDeviceProperties> 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(lcGuiVk, "%d physical devices", count);
    if (!count)
        return d->physDevProps;

    QList<VkPhysicalDevice> 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(lcGuiVk, "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().size();
    if (idx < 0 || idx >= count) {
        qWarning("QVulkanWindow: Invalid physical device index %d (total physical devices: %d)", idx, count);
        return;
    }
    d->physDevIndex = idx;
}

/*!
    Returns 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<QVulkanExtension> QVulkanWindow::supportedDeviceExtensions()
{
    Q_D(QVulkanWindow);

    availablePhysicalDevices();

    if (d->physDevs.isEmpty()) {
        qWarning("QVulkanWindow: No physical devices found");
        return QVulkanInfoVector<QVulkanExtension>();
    }

    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) {
        QList<VkExtensionProperties> extProps(count);
        err = f->vkEnumerateDeviceExtensionProperties(physDev, nullptr, &count, extProps.data());
        if (err == VK_SUCCESS) {
            QVulkanInfoVector<QVulkanExtension> exts;
            for (const VkExtensionProperties &prop : extProps) {
                QVulkanExtension ext;
                ext.name = prop.extensionName;
                ext.version = prop.specVersion;
                exts.append(ext);
            }
            d->supportedDevExtensions.insert(physDev, exts);
            qCDebug(lcGuiVk) << "Supported device extensions:" << exts;
            return exts;
        }
    }

    qWarning("QVulkanWindow: Failed to query device extension count: %d", err);
    return QVulkanInfoVector<QVulkanExtension>();
}

/*!
    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 QList<VkFormat> &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;
} q_vk_sampleCounts[] = {
    // keep this sorted by 'count'
    { 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 }
};

/*!
    Returns the set of supported sample counts when using the physical device
    selected by setPhysicalDeviceIndex(), as a sorted list.

    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()
 */
QList<int> QVulkanWindow::supportedSampleCounts()
{
    Q_D(const QVulkanWindow);
    QList<int> 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 (const auto &qvk_sampleCount : q_vk_sampleCounts) {
        if ((color & qvk_sampleCount.mask)
                && (depth & qvk_sampleCount.mask)
                && (stencil & qvk_sampleCount.mask))
        {
            result.append(qvk_sampleCount.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 (const auto &qvk_sampleCount : q_vk_sampleCounts) {
        if (qvk_sampleCount.count == sampleCount) {
            d->sampleCount = qvk_sampleCount.mask;
            return;
        }
    }

    Q_UNREACHABLE();
}

void QVulkanWindowPrivate::init()
{
    Q_Q(QVulkanWindow);
    Q_ASSERT(status == StatusUninitialized);

    qCDebug(lcGuiVk, "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.size()) {
        qWarning("QVulkanWindow: Invalid physical device index; defaulting to 0");
        physDevIndex = 0;
    }
    qCDebug(lcGuiVk, "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);
    QList<VkQueueFamilyProperties> queueFamilyProps(queueCount);
    f->vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, queueFamilyProps.data());
    gfxQueueFamilyIdx = uint32_t(-1);
    presQueueFamilyIdx = uint32_t(-1);
    for (int i = 0; i < queueFamilyProps.size(); ++i) {
        const bool supportsPresent = inst->supportsPresent(physDev, i, q);
        qCDebug(lcGuiVk, "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(lcGuiVk, "No queue with graphics+present; trying separate queues");
        for (int i = 0; i < queueFamilyProps.size(); ++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(lcGuiVk, "Using queue families: graphics = %u present = %u", gfxQueueFamilyIdx, presQueueFamilyIdx);

    QList<VkDeviceQueueCreateInfo> queueInfo;
    queueInfo.reserve(2);
    const float prio[] = { 0 };
    VkDeviceQueueCreateInfo addQueueInfo;
    memset(&addQueueInfo, 0, sizeof(addQueueInfo));
    addQueueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    addQueueInfo.queueFamilyIndex = gfxQueueFamilyIdx;
    addQueueInfo.queueCount = 1;
    addQueueInfo.pQueuePriorities = prio;
    queueInfo.append(addQueueInfo);
    if (gfxQueueFamilyIdx != presQueueFamilyIdx) {
        addQueueInfo.queueFamilyIndex = presQueueFamilyIdx;
        addQueueInfo.queueCount = 1;
        addQueueInfo.pQueuePriorities = prio;
        queueInfo.append(addQueueInfo);
    }
    if (queueCreateInfoModifier) {
        queueCreateInfoModifier(queueFamilyProps.constData(), queueCount, queueInfo);
        bool foundGfxQueue = false;
        bool foundPresQueue = false;
        for (const VkDeviceQueueCreateInfo& createInfo : std::as_const(queueInfo)) {
            foundGfxQueue |= createInfo.queueFamilyIndex == gfxQueueFamilyIdx;
            foundPresQueue |= createInfo.queueFamilyIndex == presQueueFamilyIdx;
        }
        if (!foundGfxQueue) {
            qWarning("QVulkanWindow: Graphics queue missing after call to queueCreateInfoModifier");
            status = StatusFail;
            return;
        }
        if (!foundPresQueue) {
            qWarning("QVulkanWindow: Present queue missing after call to queueCreateInfoModifier");
            status = StatusFail;
            return;
        }
    }

    // Filter out unsupported extensions in order to keep symmetry
    // with how QVulkanInstance behaves. Add the swapchain extension.
    QList<const char *> devExts;
    QVulkanInfoVector<QVulkanExtension> supportedExtensions = q->supportedDeviceExtensions();
    QByteArrayList reqExts = requestedDevExtensions;
    reqExts.append("VK_KHR_swapchain");

    QByteArray envExts = qgetenv("QT_VULKAN_DEVICE_EXTENSIONS");
    if (!envExts.isEmpty()) {
        QByteArrayList envExtList =  envExts.split(';');
        for (auto ext : reqExts)
            envExtList.removeAll(ext);
        reqExts.append(envExtList);
    }

    for (const QByteArray &ext : reqExts) {
        if (supportedExtensions.contains(ext))
            devExts.append(ext.constData());
    }
    qCDebug(lcGuiVk) << "Enabling device extensions:" << devExts;

    VkDeviceCreateInfo devInfo;
    memset(&devInfo, 0, sizeof(devInfo));
    devInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    devInfo.queueCreateInfoCount = queueInfo.size();
    devInfo.pQueueCreateInfos = queueInfo.constData();
    devInfo.enabledExtensionCount = devExts.size();
    devInfo.ppEnabledExtensionNames = devExts.constData();

    VkPhysicalDeviceFeatures features = {};
    VkPhysicalDeviceFeatures2 features2 = {};
    if (enabledFeatures2Modifier) {
        features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
        enabledFeatures2Modifier(features2);
        devInfo.pNext = &features2;
    } else if (enabledFeaturesModifier) {
        enabledFeaturesModifier(features);
        devInfo.pEnabledFeatures = &features;
    } else {
        // Enable all supported 1.0 core features, except ones that likely
        // involve a performance penalty.
        f->vkGetPhysicalDeviceFeatures(physDev, &features);
        features.robustBufferAccess = VK_FALSE;
        devInfo.pEnabledFeatures = &features;
    }

    // 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_KHRONOS_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) {
                QList<VkLayerProperties> layerProps(count);
                err = f->vkEnumerateDeviceLayerProperties(physDev, &count, layerProps.data());
                if (err == VK_SUCCESS) {
                    for (const VkLayerProperties &prop : layerProps) {
                        if (!strncmp(prop.layerName, stdValNamePtr, stdValName.size())) {
                            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(lcGuiVk, "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(lcGuiVk, "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(lcGuiVk, "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(lcGuiVk, "Picked memtype %d for device local memory", deviceLocalMemIndex);

    if (!vkGetPhysicalDeviceSurfaceCapabilitiesKHR || !vkGetPhysicalDeviceSurfaceFormatsKHR) {
        vkGetPhysicalDeviceSurfaceCapabilitiesKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR>(
            inst->getInstanceProcAddr("vkGetPhysicalDeviceSurfaceCapabilitiesKHR"));
        vkGetPhysicalDeviceSurfaceFormatsKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceFormatsKHR>(
            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);
    QList<VkSurfaceFormatKHR> 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 : std::as_const(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(lcGuiVk, "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(lcGuiVk, "QVulkanWindow reset");

    devFuncs->vkDeviceWaitIdle(dev);

    if (renderer) {
        renderer->releaseResources();
        devFuncs->vkDeviceWaitIdle(dev);
    }

    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_CLEAR;
    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_STORE;
        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<PFN_vkCreateSwapchainKHR>(f->vkGetDeviceProcAddr(dev, "vkCreateSwapchainKHR"));
        vkDestroySwapchainKHR = reinterpret_cast<PFN_vkDestroySwapchainKHR>(f->vkGetDeviceProcAddr(dev, "vkDestroySwapchainKHR"));
        vkGetSwapchainImagesKHR = reinterpret_cast<PFN_vkGetSwapchainImagesKHR>(f->vkGetDeviceProcAddr(dev, "vkGetSwapchainImagesKHR"));
        vkAcquireNextImageKHR = reinterpret_cast<PFN_vkAcquireNextImageKHR>(f->vkGetDeviceProcAddr(dev, "vkAcquireNextImageKHR"));
        vkQueuePresentKHR = reinterpret_cast<PFN_vkQueuePresentKHR>(f->vkGetDeviceProcAddr(dev, "vkQueuePresentKHR"));
    }

    VkPhysicalDevice physDev = physDevs.at(physDevIndex);
    VkSurfaceCapabilitiesKHR surfaceCaps;
    vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDev, surface, &surfaceCaps);
    uint32_t reqBufferCount;
    if (surfaceCaps.maxImageCount == 0)
        reqBufferCount = qMax<uint32_t>(2, surfaceCaps.minImageCount);
    else
        reqBufferCount = qMax(qMin<uint32_t>(surfaceCaps.maxImageCount, 3), surfaceCaps.minImageCount);

    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(lcGuiVk, "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(lcGuiVk, "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;

    Q_ASSERT(count > 0);
    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(lcGuiVk, "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(lcGuiVk, "Releasing swapchain");

    devFuncs->vkDeviceWaitIdle(dev);

    if (renderer) {
        renderer->releaseSwapChainResources();
        devFuncs->vkDeviceWaitIdle(dev);
    }

    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<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) {
            d->releaseSwapChain();
            d->reset();
        }
        break;

    default:
        break;
    }

    return QWindow::event(e);
}

/*!
    \typedef QVulkanWindow::QueueCreateInfoModifier

    A function that is called during graphics initialization to add
    additional queues that should be created.

    Set if the renderer needs additional queues besides the default graphics
    queue (e.g. a transfer queue).
    The provided queue family properties can be used to select the indices for
    the additional queues.
    The renderer can subsequently request the actual queue in initResources().

    \note When requesting additional graphics queues, Qt itself always requests
    a graphics queue. You'll need to search queueCreateInfo for the appropriate
    entry and manipulate it to obtain the additional queue.

    \sa setQueueCreateInfoModifier()
 */

/*!
    Sets the queue create info modification function \a modifier.

    \sa QueueCreateInfoModifier

    \since 5.15
 */
void QVulkanWindow::setQueueCreateInfoModifier(const QueueCreateInfoModifier &modifier)
{
    Q_D(QVulkanWindow);
    d->queueCreateInfoModifier = modifier;
}

/*!
    \typedef QVulkanWindow::EnabledFeaturesModifier

    A function that is called during graphics initialization to alter the
    VkPhysicalDeviceFeatures that is passed in when creating a Vulkan device
    object.

    By default QVulkanWindow enables all Vulkan 1.0 core features that the
    physical device reports as supported, with certain exceptions. In
    praticular, \c robustBufferAccess is always disabled in order to avoid
    unexpected performance hits.

    The VkPhysicalDeviceFeatures reference passed in is all zeroed out at the
    point when the function is invoked. It is up to the function to change
    members as it sees fit.

    \note To control Vulkan 1.1, 1.2, or 1.3 features, use
    EnabledFeatures2Modifier instead.

    \sa setEnabledFeaturesModifier()
 */

/*!
    Sets the enabled device features modification function \a modifier.

    \note To control Vulkan 1.1, 1.2, or 1.3 features, use
    the overload taking a EnabledFeatures2Modifier instead.

    \note \a modifier is passed to the callback function with all members set
    to false. It is up to the function to change members as it sees fit.

    \since 6.7
    \sa EnabledFeaturesModifier
 */
void QVulkanWindow::setEnabledFeaturesModifier(const EnabledFeaturesModifier &modifier)
{
    Q_D(QVulkanWindow);
    d->enabledFeaturesModifier = modifier;
}

/*!
    \typedef QVulkanWindow::EnabledFeatures2Modifier

    A function that is called during graphics initialization to alter the
    VkPhysicalDeviceFeatures2 that is changed to the VkDeviceCreateInfo.

    By default QVulkanWindow enables all Vulkan 1.0 core features that the
    physical device reports as supported, with certain exceptions. In
    praticular, \c robustBufferAccess is always disabled in order to avoid
    unexpected performance hits.

    This however is not always sufficient when working with Vulkan 1.1, 1.2, or
    1.3 features and extensions. Hence this callback mechanism. If only Vulkan
    1.0 is relevant at run time, use setEnabledFeaturesModifier() instead.

    The VkPhysicalDeviceFeatures2 reference passed to the callback function
    with \c sType set, but the rest zeroed out. It is up to the function to
    change members to true, or set up \c pNext chains as it sees fit.

    \note When setting up \c pNext chains, make sure the referenced objects
    have a long enough lifetime, for example by storing them as member
    variables in the QVulkanWindow subclass.

    \since 6.7
    \sa setEnabledFeaturesModifier()
 */

/*!
    Sets the enabled device features modification function \a modifier.
    \overload
    \since 6.7
    \sa EnabledFeatures2Modifier
*/
void QVulkanWindow::setEnabledFeaturesModifier(EnabledFeatures2Modifier modifier)
{
    Q_D(QVulkanWindow);
    d->enabledFeatures2Modifier = std::move(modifier);
}

/*!
    Returns 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;
}

/*!
    Returns 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 QVulkanWindow::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
    and after invoking this function.

    The default implementation is empty.

    \note This is the last place to act with all graphics resources intact
    before QVulkanWindow starts releasing them. It is therefore essential that
    implementations with an asynchronous, potentially multi-threaded
    startNextFrame() perform a blocking wait and call
    QVulkanWindow::frameReady() before returning from this function in case
    there is a pending frame submission.
 */
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
    and after invoking this function.

    The default implementation is empty.
 */
void QVulkanWindowRenderer::releaseResources()
{
}

/*!
    \fn void 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 a host
    visible and device local memory type index are exposed via
    QVulkanWindow::hostVisibleMemoryIndex() and
    QVulkanWindow::deviceLocalMemoryIndex(). All these accessors are safe to be
    called 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()
{
}

QSize QVulkanWindowPrivate::surfacePixelSize() const
{
    Q_Q(const QVulkanWindow);
    VkSurfaceCapabilitiesKHR surfaceCaps = {};
    vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDevs.at(physDevIndex), surface, &surfaceCaps);
    VkExtent2D bufferSize = surfaceCaps.currentExtent;
    if (bufferSize.width == uint32_t(-1)) {
        Q_ASSERT(bufferSize.height == uint32_t(-1));
        return q->size() * q->devicePixelRatio();
    }
    return QSize(int(bufferSize.width), int(bufferSize.height));
}

void QVulkanWindowPrivate::beginFrame()
{
    if (!swapChain || framePending)
        return;

    Q_Q(QVulkanWindow);
    if (swapChainImageSize != surfacePixelSize()) {
        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, &currentImage);
        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 = nullptr;
    }

    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); // the format is as documented

    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 = &currentImage;
    presInfo.waitSemaphoreCount = 1;
    presInfo.pWaitSemaphores = gfxQueueFamilyIdx == presQueueFamilyIdx ? &frame.drawSem : &frame.presTransSem;

    // Do platform-specific WM notification. F.ex. essential on Wayland in
    // order to circumvent driver frame callbacks
    inst->presentAboutToBeQueued(q);

    err = vkQueuePresentKHR(presQueue, &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::isMainThread(),
        "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(lcGuiVk, "Releasing all resources due to device lost");
        releaseSwapChain();
        reset();
        qCDebug(lcGuiVk, "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(&copyInfo, 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, &copyInfo);

    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_TRANSFER_BIT,
                                   VK_PIPELINE_STAGE_HOST_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<void **>(&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;
}

/*!
    Returns 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.size())
        return d->physDevs[d->physDevIndex];
    qWarning("QVulkanWindow: Physical device not available");
    return VK_NULL_HANDLE;
}

/*!
    Returns 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.size())
        return &d->physDevProps[d->physDevIndex];
    qWarning("QVulkanWindow: Physical device properties not available");
    return nullptr;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns the family index of the active graphics queue.

    \note Calling this function is only valid from the invocation of
    QVulkanWindowRenderer::initResources() up until
    QVulkanWindowRenderer::releaseResources(). Implementations of
    QVulkanWindowRenderer::updateQueueCreateInfo() can also call this
    function.

    \since 5.15
 */
uint32_t QVulkanWindow::graphicsQueueFamilyIndex() const
{
    Q_D(const QVulkanWindow);
    return d->gfxQueueFamilyIdx;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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().

    \note It is not guaranteed that this memory type is always suitable. The
    correct, cross-implementation solution - especially for device local images
    - is to manually pick a memory type after checking the mask returned from
    \c{vkGetImageMemoryRequirements}.
 */
uint32_t QVulkanWindow::deviceLocalMemoryIndex() const
{
    Q_D(const QVulkanWindow);
    return d->deviceLocalMemIndex;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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.

    In addition, it has been observed on some platforms that the
    Vulkan-reported surface size is different with high DPI scaling active,
    meaning the QWindow-reported
    \l{QWindow::}{size()} multiplied with the \l{QWindow::}{devicePixelRatio()}
    was 1 pixel less or more when compared to the value returned from here,
    presumably due to differences in rounding. Rendering code should be aware
    of this, and any related rendering logic must be based in the value returned
    from here, never on the QWindow-reported size. Regardless of which pixel size
    is correct in theory, Vulkan rendering must only ever rely on the Vulkan
    API-reported surface size. Otherwise validation errors may occur, e.g. when
    setting the viewport, because the application-provided values may become
    out-of-bounds from Vulkan's perspective.

    \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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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.

    \snippet code/src_gui_vulkan_qvulkanwindow.cpp 1

    \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().
 */

/*!
    Returns 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.

    \snippet code/src_gui_vulkan_qvulkanwindow.cpp 2

    \sa currentFrame()
 */
int QVulkanWindow::concurrentFrameCount() const
{
    Q_D(const QVulkanWindow);
    return d->frameLag;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
    Returns 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;
}

/*!
  \fn void QVulkanWindow::frameGrabbed(const QImage &image)

  This signal is emitted when the \a image is ready.
*/

/*!
    Builds and renders the next frame without presenting it, then performs a
    blocking readback of the image content.

    Returns 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.

    The returned QImage always has a format of QImage::Format_RGBA8888. If the
    colorFormat() is \c VK_FORMAT_B8G8R8A8_UNORM, the red and blue channels are
    swapped automatically since this format is commonly used as the default
    choice for swapchain color buffers. With any other color buffer format,
    there is no conversion performed by this function.

    \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();

    if (d->colorFormat == VK_FORMAT_B8G8R8A8_UNORM)
        d->frameGrabTargetImage = std::move(d->frameGrabTargetImage).rgbSwapped();

    return d->frameGrabTargetImage;
}

/*!
   Returns 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 that Y is pointing upwards, and can set minDepth and
   maxDepth in the viewport to 0 and 1, respectively, without having to do any
   further corrections to the vertex Z positions. Geometry from OpenGL
   applications can then be used as-is, assuming a rasterization state matching
   the OpenGL culling and front face settings.
 */
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

#include "moc_qvulkanwindow.cpp"
