From 8dee03500eb491375804cde8fe3c9d8b96bfd4d7 Mon Sep 17 00:00:00 2001 From: Hatem ElKharashy Date: Thu, 1 Jun 2023 11:34:49 +0300 Subject: [PATCH] Add Manual test for Graphics Frame Capture Simple window with a rotating rectangle that can be captured using QGraphicsFrameCapture. Task-number: QTBUG-116146 Change-Id: Ia3b6928e469d926c53260ee40ed5af97dd280c08 Reviewed-by: Janne Koskinen --- tests/manual/CMakeLists.txt | 3 + .../graphicsframecapture/CMakeLists.txt | 35 +++ .../graphicsframecapture/examplewindow.cpp | 116 ++++++++ .../graphicsframecapture/examplewindow.h | 31 +++ tests/manual/graphicsframecapture/main.cpp | 128 +++++++++ tests/manual/graphicsframecapture/window.cpp | 256 ++++++++++++++++++ tests/manual/graphicsframecapture/window.h | 55 ++++ 7 files changed, 624 insertions(+) create mode 100644 tests/manual/graphicsframecapture/CMakeLists.txt create mode 100644 tests/manual/graphicsframecapture/examplewindow.cpp create mode 100644 tests/manual/graphicsframecapture/examplewindow.h create mode 100644 tests/manual/graphicsframecapture/main.cpp create mode 100644 tests/manual/graphicsframecapture/window.cpp create mode 100644 tests/manual/graphicsframecapture/window.h diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt index 788a0beb71..af13d736b4 100644 --- a/tests/manual/CMakeLists.txt +++ b/tests/manual/CMakeLists.txt @@ -13,6 +13,9 @@ add_subdirectory(filetest) # add_subdirectory(embeddedintoforeignwindow) # add_subdirectory(foreignwindows) add_subdirectory(gestures) +if (QT_FEATURE_graphicsframecapture) + add_subdirectory(graphicsframecapture) +endif() add_subdirectory(highdpi) add_subdirectory(inputmethodhints) add_subdirectory(keypadnavigation) diff --git a/tests/manual/graphicsframecapture/CMakeLists.txt b/tests/manual/graphicsframecapture/CMakeLists.txt new file mode 100644 index 0000000000..8d5fc5952d --- /dev/null +++ b/tests/manual/graphicsframecapture/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## graphicsframecapture Binary: +##################################################################### + +qt_internal_add_manual_test(graphicsframecapture + SOURCES + examplewindow.cpp examplewindow.h + main.cpp + window.cpp window.h + LIBRARIES + Qt::Gui + Qt::GuiPrivate +) + +# Resources: +set_source_files_properties("../rhi/shared/color.frag.qsb" + PROPERTIES QT_RESOURCE_ALIAS "color.frag.qsb" +) +set_source_files_properties("../rhi/shared/color.vert.qsb" + PROPERTIES QT_RESOURCE_ALIAS "color.vert.qsb" +) +set(graphicsframecapture_resource_files + "../rhi/shared/color.frag.qsb" + "../rhi/shared/color.vert.qsb" +) + +qt_internal_add_resource(graphicsframecapture "graphicsframecapture" + PREFIX + "/" + FILES + ${graphicsframecapture_resource_files} +) diff --git a/tests/manual/graphicsframecapture/examplewindow.cpp b/tests/manual/graphicsframecapture/examplewindow.cpp new file mode 100644 index 0000000000..8a8959541e --- /dev/null +++ b/tests/manual/graphicsframecapture/examplewindow.cpp @@ -0,0 +1,116 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "examplewindow.h" +#include +#include + +static float vertexData[] = { + // Y up (note clipSpaceCorrMatrix in m_proj), CCW + -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + + 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, +}; + +ExampleWindow::ExampleWindow(QRhi::Implementation graphicsApi) + : Window(graphicsApi) +{ +} + +QShader ExampleWindow::getShader(const QString &name) +{ + QFile f(name); + if (f.open(QIODevice::ReadOnly)) + return QShader::fromSerialized(f.readAll()); + + return QShader(); +} + +void ExampleWindow::customInit() +{ + m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData))); + m_vbuf->create(); + m_vbufReady = false; + + m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68)); + m_ubuf->create(); + + m_srb.reset(m_rhi->newShaderResourceBindings()); + m_srb->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, + m_ubuf.get()) + }); + m_srb->create(); + + m_ps.reset(m_rhi->newGraphicsPipeline()); + + QRhiGraphicsPipeline::TargetBlend premulAlphaBlend; + premulAlphaBlend.enable = true; + m_ps->setTargetBlends({ premulAlphaBlend }); + + const QShader vs = getShader(QLatin1String(":/color.vert.qsb")); + if (!vs.isValid()) + qFatal("Failed to load shader pack (vertex)"); + const QShader fs = getShader(QLatin1String(":/color.frag.qsb")); + if (!fs.isValid()) + qFatal("Failed to load shader pack (fragment)"); + + m_ps->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 5 * sizeof(float) } + }); + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, + { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) } + }); + + m_ps->setVertexInputLayout(inputLayout); + m_ps->setShaderResourceBindings(m_srb.get()); + m_ps->setRenderPassDescriptor(m_rp.get()); + + m_ps->create(); +} + +// called once per frame +void ExampleWindow::customRender() +{ + QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch(); + if (!m_vbufReady) { + m_vbufReady = true; + u->uploadStaticBuffer(m_vbuf.get(), vertexData); + } + m_rotation += 1.0f; + QMatrix4x4 mvp = m_proj; + mvp.rotate(m_rotation, 0, 0, 1); + u->updateDynamicBuffer(m_ubuf.get(), 0, 64, mvp.constData()); + m_opacity += m_opacityDir * 0.005f; + if (m_opacity < 0.0f || m_opacity > 1.0f) { + m_opacityDir *= -1; + m_opacity = qBound(0.0f, m_opacity, 1.0f); + } + u->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity); + + QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); + const QSize outputSizeInPixels = m_sc->currentPixelSize(); + + cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f), { 1.0f, 0 }, u); + + cb->setGraphicsPipeline(m_ps.get()); + cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) }); + cb->setShaderResources(); + + const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); + cb->setVertexInput(0, 1, &vbufBinding); + cb->draw(6); + + cb->endPass(); +} diff --git a/tests/manual/graphicsframecapture/examplewindow.h b/tests/manual/graphicsframecapture/examplewindow.h new file mode 100644 index 0000000000..ef0379a82e --- /dev/null +++ b/tests/manual/graphicsframecapture/examplewindow.h @@ -0,0 +1,31 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef EXAMPLEWINDOW_H +#define EXAMPLEWINDOW_H + +#include "window.h" + +class ExampleWindow : public Window +{ +public: + ExampleWindow(QRhi::Implementation graphicsApi); + + void customInit() override; + void customRender() override; + +private: + QShader getShader(const QString &name); + + std::unique_ptr m_vbuf; + bool m_vbufReady = false; + std::unique_ptr m_ubuf; + std::unique_ptr m_srb; + std::unique_ptr m_ps; + + float m_rotation = 0; + float m_opacity = 1; + int m_opacityDir = -1; +}; + +#endif diff --git a/tests/manual/graphicsframecapture/main.cpp b/tests/manual/graphicsframecapture/main.cpp new file mode 100644 index 0000000000..389d0bdc1c --- /dev/null +++ b/tests/manual/graphicsframecapture/main.cpp @@ -0,0 +1,128 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +// This is a compact, minimal demo of deciding the backend at runtime while +// using the exact same shaders and rendering code without any branching +// whatsoever once the QWindow is up and the RHI is initialized. + +#include +#include +#include "examplewindow.h" + + +QString graphicsApiName(QRhi::Implementation graphicsApi); +QRhi::Implementation graphicsApiFromCmd(const QGuiApplication &app); + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + QRhi::Implementation graphicsApi = graphicsApiFromCmd(app); + + QSurfaceFormat fmt; + fmt.setDepthBufferSize(24); + fmt.setStencilBufferSize(8); + QSurfaceFormat::setDefaultFormat(fmt); + + ExampleWindow w(graphicsApi); + +#if QT_CONFIG(vulkan) + QVulkanInstance inst; + if (graphicsApi == QRhi::Vulkan) { + inst.setLayers({ "VK_LAYER_KHRONOS_validation" }); + inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions()); + if (!inst.create()) { + qWarning("Failed to create Vulkan instance, switching to OpenGL"); + graphicsApi = QRhi::OpenGLES2; + } + } +#endif + +#if QT_CONFIG(vulkan) + if (graphicsApi == QRhi::Vulkan) + w.setVulkanInstance(&inst); +#endif + w.resize(1280, 720); + w.setTitle(QCoreApplication::applicationName() + QLatin1String(" - ") + graphicsApiName(graphicsApi)); + w.show(); + + int ret = app.exec(); + + // Window::event() will not get invoked when the + // PlatformSurfaceAboutToBeDestroyed event is sent during the QWindow + // destruction. That happens only when exiting via app::quit() instead of + // the more common QWindow::close(). Take care of it: if the QPlatformWindow + // is still around (there was no close() yet), get rid of the swapchain + // while it's not too late. + if (w.handle()) + w.releaseSwapChain(); + + return ret; +} + +QString graphicsApiName(QRhi::Implementation graphicsApi) +{ + switch (graphicsApi) { + case QRhi::Null: + return QLatin1String("Null (no output)"); + case QRhi::OpenGLES2: + return QLatin1String("OpenGL 2.x"); + case QRhi::Vulkan: + return QLatin1String("Vulkan"); + case QRhi::D3D11: + return QLatin1String("Direct3D 11"); + case QRhi::D3D12: + return QLatin1String("Direct3D 12"); + case QRhi::Metal: + return QLatin1String("Metal"); + default: + break; + } + return QString(); +} + +QRhi::Implementation graphicsApiFromCmd(const QGuiApplication &app) { + QRhi::Implementation graphicsApi; +#if defined(Q_OS_WIN) + graphicsApi = QRhi::D3D11; +#elif defined(Q_OS_MACOS) || defined(Q_OS_IOS) + graphicsApi = QRhi::Metal; +#elif QT_CONFIG(vulkan) + graphicsApi = QRhi::Vulkan; +#else + graphicsApi = QRhi::OpenGLES2; +#endif + + QCommandLineParser cmdLineParser; + cmdLineParser.addHelpOption(); + QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null")); + cmdLineParser.addOption(nullOption); + QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)")); + cmdLineParser.addOption(glOption); + QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan")); + cmdLineParser.addOption(vkOption); + QCommandLineOption d3d11Option({ "d", "d3d11" }, QLatin1String("Direct3D 11")); + cmdLineParser.addOption(d3d11Option); + QCommandLineOption d3d12Option({ "D", "d3d12" }, QLatin1String("Direct3D 12")); + cmdLineParser.addOption(d3d12Option); + QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal")); + cmdLineParser.addOption(mtlOption); + + cmdLineParser.process(app); + if (cmdLineParser.isSet(nullOption)) + graphicsApi = QRhi::Null; + if (cmdLineParser.isSet(glOption)) + graphicsApi = QRhi::OpenGLES2; + if (cmdLineParser.isSet(vkOption)) + graphicsApi = QRhi::Vulkan; + if (cmdLineParser.isSet(d3d11Option)) + graphicsApi = QRhi::D3D11; + if (cmdLineParser.isSet(d3d12Option)) + graphicsApi = QRhi::D3D12; + if (cmdLineParser.isSet(mtlOption)) + graphicsApi = QRhi::Metal; + + qDebug("Selected graphics API is %s", qPrintable(graphicsApiName(graphicsApi))); + qDebug("This is a multi-api example, use command line arguments to override:\n%s", qPrintable(cmdLineParser.helpText())); + + return graphicsApi; +} diff --git a/tests/manual/graphicsframecapture/window.cpp b/tests/manual/graphicsframecapture/window.cpp new file mode 100644 index 0000000000..55725958f5 --- /dev/null +++ b/tests/manual/graphicsframecapture/window.cpp @@ -0,0 +1,256 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "window.h" +#include +#include + +Window::Window(QRhi::Implementation graphicsApi) + : m_graphicsApi(graphicsApi) +{ + m_capturer.reset(new QGraphicsFrameCapture); +#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) + qDebug("This example uses Metal Capture Manager In App API to capture frames. Press F9 to capture a frame and F10 to open it for analysis"); +#else + qDebug("This example uses RenderDoc In App API to capture frames. Press F9 to capture a frame and F10 to open it for analysis"); + qDebug("On Linux with OpenGL, make sure that librenderdoc.so is loaded using LD_PRELOAD"); +#endif + qDebug("The Frame Capturer API will save captures in this directory : %s", qPrintable(m_capturer->capturePath())); + + switch (graphicsApi) { + case QRhi::OpenGLES2: + setSurfaceType(OpenGLSurface); + break; + case QRhi::Vulkan: + setSurfaceType(VulkanSurface); + break; + case QRhi::D3D11: + case QRhi::D3D12: + setSurfaceType(Direct3DSurface); + break; + case QRhi::Metal: + setSurfaceType(MetalSurface); + break; + default: + break; + } +} + +void Window::exposeEvent(QExposeEvent *) +{ + // initialize and start rendering when the window becomes usable for graphics purposes + if (isExposed() && !m_running) { + qDebug("init"); + m_running = true; + init(); + resizeSwapChain(); + } + + const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize(); + + // stop pushing frames when not exposed (or size is 0) + if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_running && !m_notExposed) { + qDebug("not exposed"); + m_notExposed = true; + } + + // Continue when exposed again and the surface has a valid size. Note that + // surfaceSize can be (0, 0) even though size() reports a valid one, hence + // trusting surfacePixelSize() and not QWindow. + if (isExposed() && m_running && m_notExposed && !surfaceSize.isEmpty()) { + qDebug("exposed again"); + m_notExposed = false; + m_newlyExposed = true; + } + + // always render a frame on exposeEvent() (when exposed) in order to update + // immediately on window resize. + if (isExposed() && !surfaceSize.isEmpty()) + render(); +} + +bool Window::event(QEvent *e) +{ + + switch (e->type()) { + case QEvent::UpdateRequest: + render(); + break; + + case QEvent::PlatformSurface: + // this is the proper time to tear down the swapchain (while the native window and surface are still around) + if (static_cast(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) + releaseSwapChain(); + break; + + case QEvent::KeyRelease: + if (static_cast(e)->key() == Qt::Key::Key_F9 && !static_cast(e)->isAutoRepeat()) { + m_shouldCapture = true; + return true; + } + else if (static_cast(e)->key() == Qt::Key::Key_F10 && !static_cast(e)->isAutoRepeat()) { + if (!m_capturer.isNull()) + m_capturer->openCapture(); + } + break; + + default: + break; + } + + return QWindow::event(e); + +} + +void Window::init() +{ + QRhi::Flags rhiFlags = QRhi::EnableDebugMarkers; + + if (m_graphicsApi == QRhi::Null) { + QRhiNullInitParams params; + m_rhi.reset(QRhi::create(QRhi::Null, ¶ms, rhiFlags)); + } + +#if QT_CONFIG(opengl) + if (m_graphicsApi == QRhi::OpenGLES2) { + m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface()); + QRhiGles2InitParams params; + params.fallbackSurface = m_fallbackSurface.get(); + params.window = this; + m_rhi.reset(QRhi::create(QRhi::OpenGLES2, ¶ms, rhiFlags)); + } +#endif + +#if QT_CONFIG(vulkan) + if (m_graphicsApi == QRhi::Vulkan) { + QRhiVulkanInitParams params; + params.inst = vulkanInstance(); + params.window = this; + m_rhi.reset(QRhi::create(QRhi::Vulkan, ¶ms, rhiFlags)); + } +#endif + +#ifdef Q_OS_WIN + if (m_graphicsApi == QRhi::D3D11) { + QRhiD3D11InitParams params; + params.enableDebugLayer = true; + m_rhi.reset(QRhi::create(QRhi::D3D11, ¶ms, rhiFlags)); + } else if (m_graphicsApi == QRhi::D3D12) { + QRhiD3D12InitParams params; + params.enableDebugLayer = true; + m_rhi.reset(QRhi::create(QRhi::D3D12, ¶ms, rhiFlags)); + } +#endif + +#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) + if (m_graphicsApi == QRhi::Metal) { + QRhiMetalInitParams params; + m_rhi.reset(QRhi::create(QRhi::Metal, ¶ms, rhiFlags)); + } +#endif + + if (!m_rhi) + qFatal("Failed to create RHI backend"); + + m_sc.reset(m_rhi->newSwapChain()); + m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, + QSize(), // no need to set the size here, due to UsedWithSwapChainOnly + 1, + QRhiRenderBuffer::UsedWithSwapChainOnly)); + m_sc->setWindow(this); + m_sc->setDepthStencil(m_ds.get()); + m_rp.reset(m_sc->newCompatibleRenderPassDescriptor()); + m_sc->setRenderPassDescriptor(m_rp.get()); + + m_capturer->setRhi(m_rhi.get()); + + customInit(); +} + +void Window::resizeSwapChain() +{ + m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds + + const QSize outputSize = m_sc->currentPixelSize(); + m_proj = m_rhi->clipSpaceCorrMatrix(); + m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f); + m_proj.translate(0, 0, -4); +} + +void Window::releaseSwapChain() +{ + if (m_hasSwapChain) { + m_hasSwapChain = false; + m_sc->destroy(); + } +} + +void Window::render() +{ + if (!m_hasSwapChain || m_notExposed) + return; + + // If the window got resized or newly exposed, resize the swapchain. (the + // newly-exposed case is not actually required by some platforms, but + // f.ex. Vulkan on Windows seems to need it) + // + // This (exposeEvent + the logic here) is the only safe way to perform + // resize handling. Note the usage of the RHI's surfacePixelSize(), and + // never QWindow::size(). (the two may or may not be the same under the hood, + // depending on the backend and platform) + // + if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) { + resizeSwapChain(); + if (!m_hasSwapChain) + return; + m_newlyExposed = false; + } + + if (m_shouldCapture) + m_capturer->startCaptureFrame(); + + QRhi::FrameOpResult r = m_rhi->beginFrame(m_sc.get()); + if (r == QRhi::FrameOpSwapChainOutOfDate) { + resizeSwapChain(); + if (!m_hasSwapChain) + return; + r = m_rhi->beginFrame(m_sc.get()); + } + if (r != QRhi::FrameOpSuccess) { + qDebug("beginFrame failed with %d, retry", r); + requestUpdate(); + return; + } + + customRender(); + + m_rhi->endFrame(m_sc.get()); + + if (m_shouldCapture) { + m_capturer->endCaptureFrame(); + m_shouldCapture = false; + } + +// Always request the next frame via requestUpdate(). On some platforms this is backed +// by a platform-specific solution, e.g. CVDisplayLink on macOS, which is potentially +// more efficient than a timer, queued metacalls, etc. +// +// However, the rendering behavior is identical no matter how the next round of +// rendering is triggered: the rendering thread is throttled to the presentation rate +// (either in beginFrame() or endFrame()) so the triangle should rotate at the exact +// same speed no matter which approach is taken here. + +#if 1 + requestUpdate(); +#else + QTimer::singleShot(0, this, [this] { render(); }); +#endif +} + +void Window::customInit() +{ +} + +void Window::customRender() +{ +} diff --git a/tests/manual/graphicsframecapture/window.h b/tests/manual/graphicsframecapture/window.h new file mode 100644 index 0000000000..faacdabfc1 --- /dev/null +++ b/tests/manual/graphicsframecapture/window.h @@ -0,0 +1,55 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef WINDOW_H +#define WINDOW_H + +#include +#include +#include +#include + +class Window : public QWindow +{ +public: + Window(QRhi::Implementation graphicsApi); + + void releaseSwapChain(); + +protected: + virtual void customInit(); + virtual void customRender(); + + // destruction order matters to a certain degree: the fallbackSurface must + // outlive the rhi, the rhi must outlive all other resources. The resources + // need no special order when destroying. +#if QT_CONFIG(opengl) + std::unique_ptr m_fallbackSurface; +#endif + std::unique_ptr m_rhi; + std::unique_ptr m_sc; + std::unique_ptr m_ds; + std::unique_ptr m_rp; + + bool m_hasSwapChain = false; + QMatrix4x4 m_proj; + +private: + void init(); + void resizeSwapChain(); + void render(); + + void exposeEvent(QExposeEvent *) override; + bool event(QEvent *) override; + + QRhi::Implementation m_graphicsApi; + + bool m_running = false; + bool m_notExposed = false; + bool m_newlyExposed = false; + + QScopedPointer m_capturer; + bool m_shouldCapture = false; +}; + +#endif