// Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "window.h" #include #include Window::Window(QRhi::Implementation graphicsApi) : m_graphicsApi(graphicsApi) { 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; 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()); 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; } 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()); // 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() { }