qt5base-lts/tests/manual/graphicsframecapture/window.cpp
Hatem ElKharashy 8dee03500e 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 <janne.p.koskinen@qt.io>
2023-09-20 10:51:55 +03:00

257 lines
7.8 KiB
C++

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "window.h"
#include <QPlatformSurfaceEvent>
#include <QTimer>
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<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, &params, 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, &params, 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, &params, rhiFlags));
}
#endif
#ifdef Q_OS_WIN
if (m_graphicsApi == QRhi::D3D11) {
QRhiD3D11InitParams params;
params.enableDebugLayer = true;
m_rhi.reset(QRhi::create(QRhi::D3D11, &params, rhiFlags));
} else if (m_graphicsApi == QRhi::D3D12) {
QRhiD3D12InitParams params;
params.enableDebugLayer = true;
m_rhi.reset(QRhi::create(QRhi::D3D12, &params, 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, &params, 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()
{
}