diff options
Diffstat (limited to 'tests/manual/graphicsframecapture/window.cpp')
-rw-r--r-- | tests/manual/graphicsframecapture/window.cpp | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/tests/manual/graphicsframecapture/window.cpp b/tests/manual/graphicsframecapture/window.cpp new file mode 100644 index 0000000000..92a5483634 --- /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 GPL-3.0-only + +#include "window.h" +#include <QPlatformSurfaceEvent> +#include <QTimer> + +Window::Window(QRhi::Implementation graphicsApi) + : m_graphicsApi(graphicsApi) +{ + m_capturer.reset(new QGraphicsFrameCapture); +#if QT_CONFIG(metal) + 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<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) + releaseSwapChain(); + break; + + case QEvent::KeyRelease: + if (static_cast<QKeyEvent *>(e)->key() == Qt::Key::Key_F9 && !static_cast<QKeyEvent *>(e)->isAutoRepeat()) { + m_shouldCapture = true; + return true; + } + else if (static_cast<QKeyEvent *>(e)->key() == Qt::Key::Key_F10 && !static_cast<QKeyEvent *>(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 QT_CONFIG(metal) + 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() +{ +} |