diff options
21 files changed, 886 insertions, 106 deletions
diff --git a/examples/opengl/CMakeLists.txt b/examples/opengl/CMakeLists.txt index 9c6768f3248..d2c7ece2fd6 100644 --- a/examples/opengl/CMakeLists.txt +++ b/examples/opengl/CMakeLists.txt @@ -15,4 +15,5 @@ if(TARGET Qt6::Widgets) qt_internal_add_example(textures) qt_internal_add_example(hellogles3) qt_internal_add_example(computegles31) + qt_internal_add_example(stereoqopenglwidget) endif() diff --git a/examples/opengl/doc/images/stereoexample-leftbuffer.png b/examples/opengl/doc/images/stereoexample-leftbuffer.png Binary files differnew file mode 100644 index 00000000000..6ae7d5118bb --- /dev/null +++ b/examples/opengl/doc/images/stereoexample-leftbuffer.png diff --git a/examples/opengl/doc/images/stereoexample-rightbuffer.png b/examples/opengl/doc/images/stereoexample-rightbuffer.png Binary files differnew file mode 100644 index 00000000000..3e2dc3c6714 --- /dev/null +++ b/examples/opengl/doc/images/stereoexample-rightbuffer.png diff --git a/examples/opengl/doc/src/stereoqopenglwidget.qdoc b/examples/opengl/doc/src/stereoqopenglwidget.qdoc new file mode 100644 index 00000000000..c4e51f9cee5 --- /dev/null +++ b/examples/opengl/doc/src/stereoqopenglwidget.qdoc @@ -0,0 +1,41 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example stereoqopenglwidget + \title QOpenGLWidget Stereoscopic Rendering Example + + \brief This example shows how to create a minimal QOpenGLWidget based application + with stereoscopic rendering support. + + \image stereoexample-leftbuffer.png + + The above image is what will be rendered to the left buffer. + + \image stereoexample-rightbuffer.png + + The above image is what will be rendered to the right buffer. + + \note Support for stereoscopic rendering has certain hardware requirements, like + your graphics card needs stereo support. + + \section1 Setting the correct surface flag + To enable stereoscopic rendering you need to set the flag + QSurfaceFormat::StereoBuffers globally. Just doing it on the widget is not enough + because of how the flag is handled internally. The safest is to do it on + QSurfaceFormat::SetDefaultFormat prior to starting the application. + + \snippet stereoqopenglwidget/main.cpp 1 + + \section1 Rendering twice + After QSurfaceFormat::StereoBuffers is set, then paintGL() will be called twice, + once for each buffer. In paintGL() you can call currentTargetBuffer() to query + which TargetBuffer is currently active. + + In the following snippet we slightly translate the matrix to not render the + vertices on top of each other. This is a simple example just too see that if the + necessary support is there, at runtime you should see two objects, one on the left + and one on the right. + + \snippet stereoqopenglwidget/glwidget.cpp 1 +*/ diff --git a/examples/opengl/opengl.pro b/examples/opengl/opengl.pro index 907930d7ac2..24cda20f63f 100644 --- a/examples/opengl/opengl.pro +++ b/examples/opengl/opengl.pro @@ -14,5 +14,6 @@ qtHaveModule(widgets) { cube \ textures \ hellogles3 \ - computegles31 + computegles31 \ + stereoqopenglwidget } diff --git a/examples/opengl/stereoqopenglwidget/CMakeLists.txt b/examples/opengl/stereoqopenglwidget/CMakeLists.txt new file mode 100644 index 00000000000..69fd304bb22 --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(stereoqopenglwidget LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/opengl/stereoqopenglwidget") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui OpenGL OpenGLWidgets Widgets) + +qt_add_executable(stereoqopenglwidget + glwidget.cpp glwidget.h + main.cpp + mainwindow.cpp mainwindow.h +) + +set_target_properties(stereoqopenglwidget PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(stereoqopenglwidget PUBLIC + Qt::Core + Qt::Gui + Qt::OpenGL + Qt::OpenGLWidgets + Qt::Widgets +) + + +install(TARGETS stereoqopenglwidget + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/opengl/stereoqopenglwidget/glwidget.cpp b/examples/opengl/stereoqopenglwidget/glwidget.cpp new file mode 100644 index 00000000000..36a6300348e --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/glwidget.cpp @@ -0,0 +1,277 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "glwidget.h" +#include <QPainter> +#include <QPaintEngine> +#include <QOpenGLShaderProgram> +#include <QOpenGLTexture> +#include <QRandomGenerator> +#include <QCoreApplication> +#include <QFileDialog> +#include <qmath.h> + +GLWidget::GLWidget(const QColor &background) + : m_background(background) +{ + setMinimumSize(300, 250); +} + +GLWidget::~GLWidget() +{ + reset(); +} + +void GLWidget::saveImage(TargetBuffer targetBuffer) +{ + QImage img = grabFramebuffer(targetBuffer); + if (img.isNull()) { + qFatal("Failed to grab framebuffer"); + } + + const char *fn = + targetBuffer == TargetBuffer::LeftBuffer + ? "leftBuffer.png" : "rightBuffer.png"; + + QFileDialog fd(this); + fd.setAcceptMode(QFileDialog::AcceptSave); + fd.setDefaultSuffix("png"); + fd.selectFile(fn); + if (fd.exec() == QDialog::Accepted) + img.save(fd.selectedFiles().first()); +} + +void GLWidget::reset() +{ + // And now release all OpenGL resources. + makeCurrent(); + delete m_program; + m_program = nullptr; + delete m_vshader; + m_vshader = nullptr; + delete m_fshader; + m_fshader = nullptr; + m_vbo.destroy(); + doneCurrent(); + + // We are done with the current QOpenGLContext, forget it. If there is a + // subsequent initialize(), that will then connect to the new context. + QObject::disconnect(m_contextWatchConnection); +} +void GLWidget::initializeGL() +{ + initializeOpenGLFunctions(); + + m_vshader = new QOpenGLShader(QOpenGLShader::Vertex); + const char *vsrc1 = + "attribute highp vec4 vertex;\n" + "attribute mediump vec3 normal;\n" + "uniform mediump mat4 matrix;\n" + "varying mediump vec4 color;\n" + "void main(void)\n" + "{\n" + " vec3 toLight = normalize(vec3(0.0, 0.3, 1.0));\n" + " float angle = max(dot(normal, toLight), 0.0);\n" + " vec3 col = vec3(0.40, 1.0, 0.0);\n" + " color = vec4(col * 0.2 + col * 0.8 * angle, 1.0);\n" + " color = clamp(color, 0.0, 1.0);\n" + " gl_Position = matrix * vertex;\n" + "}\n"; + m_vshader->compileSourceCode(vsrc1); + + m_fshader = new QOpenGLShader(QOpenGLShader::Fragment); + const char *fsrc1 = + "varying mediump vec4 color;\n" + "void main(void)\n" + "{\n" + " gl_FragColor = color;\n" + "}\n"; + m_fshader->compileSourceCode(fsrc1); + + m_program = new QOpenGLShaderProgram; + m_program->addShader(m_vshader); + m_program->addShader(m_fshader); + m_program->link(); + + + m_vertexAttr = m_program->attributeLocation("vertex"); + m_normalAttr = m_program->attributeLocation("normal"); + m_matrixUniform = m_program->uniformLocation("matrix"); + + createGeometry(); + + m_vbo.create(); + m_vbo.bind(); + const int vertexCount = m_vertices.count(); + QList<GLfloat> buf; + buf.resize(vertexCount * 3 * 2); + GLfloat *p = buf.data(); + for (int i = 0; i < vertexCount; ++i) { + *p++ = m_vertices[i].x(); + *p++ = m_vertices[i].y(); + *p++ = m_vertices[i].z(); + *p++ = m_normals[i].x(); + *p++ = m_normals[i].y(); + *p++ = m_normals[i].z(); + } + m_vbo.allocate(buf.constData(), (int)buf.count() * sizeof(GLfloat)); + m_vbo.release(); + + m_contextWatchConnection = QObject::connect(context(), &QOpenGLContext::aboutToBeDestroyed, context(), [this] { reset(); }); + + glFrontFace(GL_CW); + glCullFace(GL_FRONT); + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); +} + +void GLWidget::paintGL() +{ + // When QSurfaceFormat::StereoBuffers is enabled, this function is called twice. + // Once where currentTargetBuffer() == QOpenGLWidget::LeftBuffer, + // and once where currentTargetBuffer() == QOpenGLWidget::RightBuffer. + + glClearColor(m_background.redF(), m_background.greenF(), m_background.blueF(), 1.0f); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + //! [1] + // Slightly translate the model, so that there's a visible difference in each buffer. + QMatrix4x4 modelview; + if (currentTargetBuffer() == QOpenGLWidget::LeftBuffer) + modelview.translate(-0.4f, 0.0f, 0.0f); + else if (currentTargetBuffer() == QOpenGLWidget::RightBuffer) + modelview.translate(0.4f, 0.0f, 0.0f); + //! [1] + + m_program->bind(); + m_program->setUniformValue(m_matrixUniform, modelview); + m_program->enableAttributeArray(m_vertexAttr); + m_program->enableAttributeArray(m_normalAttr); + + m_vbo.bind(); + m_program->setAttributeBuffer(m_vertexAttr, GL_FLOAT, 0, 3, 6 * sizeof(GLfloat)); + m_program->setAttributeBuffer(m_normalAttr, GL_FLOAT, 3 * sizeof(GLfloat), 3, 6 * sizeof(GLfloat)); + m_vbo.release(); + + glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); + + m_program->disableAttributeArray(m_normalAttr); + m_program->disableAttributeArray(m_vertexAttr); + m_program->release(); + update(); +} + +void GLWidget::createGeometry() +{ + m_vertices.clear(); + m_normals.clear(); + + qreal x1 = +0.06f; + qreal y1 = -0.14f; + qreal x2 = +0.14f; + qreal y2 = -0.06f; + qreal x3 = +0.08f; + qreal y3 = +0.00f; + qreal x4 = +0.30f; + qreal y4 = +0.22f; + + quad(x1, y1, x2, y2, y2, x2, y1, x1); + quad(x3, y3, x4, y4, y4, x4, y3, x3); + + extrude(x1, y1, x2, y2); + extrude(x2, y2, y2, x2); + extrude(y2, x2, y1, x1); + extrude(y1, x1, x1, y1); + extrude(x3, y3, x4, y4); + extrude(x4, y4, y4, x4); + extrude(y4, x4, y3, x3); + + const int NumSectors = 100; + const qreal sectorAngle = 2 * qreal(M_PI) / NumSectors; + + for (int i = 0; i < NumSectors; ++i) { + qreal angle = i * sectorAngle; + qreal x5 = 0.30 * sin(angle); + qreal y5 = 0.30 * cos(angle); + qreal x6 = 0.20 * sin(angle); + qreal y6 = 0.20 * cos(angle); + + angle += sectorAngle; + qreal x7 = 0.20 * sin(angle); + qreal y7 = 0.20 * cos(angle); + qreal x8 = 0.30 * sin(angle); + qreal y8 = 0.30 * cos(angle); + + quad(x5, y5, x6, y6, x7, y7, x8, y8); + + extrude(x6, y6, x7, y7); + extrude(x8, y8, x5, y5); + } + + for (int i = 0;i < m_vertices.size();i++) + m_vertices[i] *= 2.0f; +} + +void GLWidget::quad(qreal x1, qreal y1, qreal x2, qreal y2, qreal x3, qreal y3, qreal x4, qreal y4) +{ + m_vertices << QVector3D(x1, y1, -0.05f); + m_vertices << QVector3D(x2, y2, -0.05f); + m_vertices << QVector3D(x4, y4, -0.05f); + + m_vertices << QVector3D(x3, y3, -0.05f); + m_vertices << QVector3D(x4, y4, -0.05f); + m_vertices << QVector3D(x2, y2, -0.05f); + + QVector3D n = QVector3D::normal + (QVector3D(x2 - x1, y2 - y1, 0.0f), QVector3D(x4 - x1, y4 - y1, 0.0f)); + + m_normals << n; + m_normals << n; + m_normals << n; + + m_normals << n; + m_normals << n; + m_normals << n; + + m_vertices << QVector3D(x4, y4, 0.05f); + m_vertices << QVector3D(x2, y2, 0.05f); + m_vertices << QVector3D(x1, y1, 0.05f); + + m_vertices << QVector3D(x2, y2, 0.05f); + m_vertices << QVector3D(x4, y4, 0.05f); + m_vertices << QVector3D(x3, y3, 0.05f); + + n = QVector3D::normal + (QVector3D(x2 - x4, y2 - y4, 0.0f), QVector3D(x1 - x4, y1 - y4, 0.0f)); + + m_normals << n; + m_normals << n; + m_normals << n; + + m_normals << n; + m_normals << n; + m_normals << n; +} + +void GLWidget::extrude(qreal x1, qreal y1, qreal x2, qreal y2) +{ + m_vertices << QVector3D(x1, y1, +0.05f); + m_vertices << QVector3D(x2, y2, +0.05f); + m_vertices << QVector3D(x1, y1, -0.05f); + + m_vertices << QVector3D(x2, y2, -0.05f); + m_vertices << QVector3D(x1, y1, -0.05f); + m_vertices << QVector3D(x2, y2, +0.05f); + + QVector3D n = QVector3D::normal + (QVector3D(x2 - x1, y2 - y1, 0.0f), QVector3D(0.0f, 0.0f, -0.1f)); + + m_normals << n; + m_normals << n; + m_normals << n; + + m_normals << n; + m_normals << n; + m_normals << n; +} diff --git a/examples/opengl/stereoqopenglwidget/glwidget.h b/examples/opengl/stereoqopenglwidget/glwidget.h new file mode 100644 index 00000000000..0014ee37c1b --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/glwidget.h @@ -0,0 +1,53 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef GLWIDGET_H +#define GLWIDGET_H + +#include <QOpenGLWidget> +#include <QOpenGLFunctions> +#include <QOpenGLBuffer> +#include <QVector3D> +#include <QMatrix4x4> +#include <QElapsedTimer> +#include <QList> +#include <QPushButton> + + +QT_FORWARD_DECLARE_CLASS(QOpenGLTexture) +QT_FORWARD_DECLARE_CLASS(QOpenGLShader) +QT_FORWARD_DECLARE_CLASS(QOpenGLShaderProgram) + +class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions +{ + Q_OBJECT +public: + GLWidget(const QColor &background); + ~GLWidget(); + + void saveImage(QOpenGLWidget::TargetBuffer targetBuffer); + +protected: + void paintGL() override; + void initializeGL() override; + +private: + void createGeometry(); + void quad(qreal x1, qreal y1, qreal x2, qreal y2, qreal x3, qreal y3, qreal x4, qreal y4); + void extrude(qreal x1, qreal y1, qreal x2, qreal y2); + void reset(); + + QList<QVector3D> m_vertices; + QList<QVector3D> m_normals; + QOpenGLShader *m_vshader = nullptr; + QOpenGLShader *m_fshader = nullptr; + QOpenGLShaderProgram *m_program = nullptr; + QOpenGLBuffer m_vbo; + int m_vertexAttr; + int m_normalAttr; + int m_matrixUniform; + QColor m_background; + QMetaObject::Connection m_contextWatchConnection; +}; + +#endif diff --git a/examples/opengl/stereoqopenglwidget/main.cpp b/examples/opengl/stereoqopenglwidget/main.cpp new file mode 100644 index 00000000000..8aad756ecab --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/main.cpp @@ -0,0 +1,31 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QApplication> +#include <QSurfaceFormat> +#include "mainwindow.h" + +int main( int argc, char ** argv ) +{ + QApplication a( argc, argv ); + + QCoreApplication::setApplicationName("Qt QOpenGLWidget Stereoscopic Rendering Example"); + QCoreApplication::setOrganizationName("QtProject"); + QCoreApplication::setApplicationVersion(QT_VERSION_STR); + + //! [1] + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + + // Enable stereoscopic rendering support + format.setStereo(true); + + QSurfaceFormat::setDefaultFormat(format); + //! [1] + + MainWindow mw; + mw.resize(1280, 720); + mw.show(); + return a.exec(); +} diff --git a/examples/opengl/stereoqopenglwidget/mainwindow.cpp b/examples/opengl/stereoqopenglwidget/mainwindow.cpp new file mode 100644 index 00000000000..33f93ba7de8 --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/mainwindow.cpp @@ -0,0 +1,25 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "mainwindow.h" +#include <QApplication> +#include <QMenuBar> +#include "glwidget.h" + +MainWindow::MainWindow() +{ + GLWidget *glwidget = new GLWidget(qRgb(20, 20, 50)); + setCentralWidget(glwidget); + + QMenu *screenShotMenu = menuBar()->addMenu("&Screenshot"); + screenShotMenu->addAction("Left buffer", this, [glwidget](){ + glwidget->saveImage(QOpenGLWidget::LeftBuffer); + }); + + screenShotMenu->addAction("Right buffer", this, [glwidget](){ + glwidget->saveImage(QOpenGLWidget::RightBuffer); + }); + + QMenu *helpMenu = menuBar()->addMenu("&Help"); + helpMenu->addAction("About Qt", qApp, &QApplication::aboutQt); +} diff --git a/examples/opengl/stereoqopenglwidget/mainwindow.h b/examples/opengl/stereoqopenglwidget/mainwindow.h new file mode 100644 index 00000000000..aa6f722e590 --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/mainwindow.h @@ -0,0 +1,21 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QMainWindow> +#include <QTimer> +#include <QGridLayout> + +QT_FORWARD_DECLARE_CLASS(QOpenGLWidget) + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(); +}; + +#endif diff --git a/examples/opengl/stereoqopenglwidget/stereoqopenglwidget.pro b/examples/opengl/stereoqopenglwidget/stereoqopenglwidget.pro new file mode 100644 index 00000000000..197afff571e --- /dev/null +++ b/examples/opengl/stereoqopenglwidget/stereoqopenglwidget.pro @@ -0,0 +1,11 @@ +QT += widgets opengl openglwidgets + +SOURCES += main.cpp \ + glwidget.cpp \ + mainwindow.cpp + +HEADERS += glwidget.h \ + mainwindow.h + +target.path = $$[QT_INSTALL_EXAMPLES]/opengl/stereoqopenglwidget +INSTALLS += target diff --git a/src/gui/painting/qbackingstoredefaultcompositor.cpp b/src/gui/painting/qbackingstoredefaultcompositor.cpp index 076ae3e9848..1dd116ac814 100644 --- a/src/gui/painting/qbackingstoredefaultcompositor.cpp +++ b/src/gui/painting/qbackingstoredefaultcompositor.cpp @@ -335,7 +335,7 @@ static QRhiGraphicsPipeline *createGraphicsPipeline(QRhi *rhi, static const int UBUF_SIZE = 120; -QBackingStoreDefaultCompositor::PerQuadData QBackingStoreDefaultCompositor::createPerQuadData(QRhiTexture *texture) +QBackingStoreDefaultCompositor::PerQuadData QBackingStoreDefaultCompositor::createPerQuadData(QRhiTexture *texture, QRhiTexture *textureExtra) { PerQuadData d; @@ -350,13 +350,24 @@ QBackingStoreDefaultCompositor::PerQuadData QBackingStoreDefaultCompositor::crea }); if (!d.srb->create()) qWarning("QBackingStoreDefaultCompositor: Failed to create srb"); - d.lastUsedTexture = texture; + if (textureExtra) { + d.srbExtra = m_rhi->newShaderResourceBindings(); + d.srbExtra->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf, 0, UBUF_SIZE), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, textureExtra, m_sampler) + }); + if (!d.srbExtra->create()) + qWarning("QBackingStoreDefaultCompositor: Failed to create srb"); + } + + d.lastUsedTextureExtra = textureExtra; + return d; } -void QBackingStoreDefaultCompositor::updatePerQuadData(PerQuadData *d, QRhiTexture *texture) +void QBackingStoreDefaultCompositor::updatePerQuadData(PerQuadData *d, QRhiTexture *texture, QRhiTexture *textureExtra) { // This whole check-if-texture-ptr-is-different is needed because there is // nothing saying a QPlatformTextureList cannot return a different @@ -371,8 +382,17 @@ void QBackingStoreDefaultCompositor::updatePerQuadData(PerQuadData *d, QRhiTextu }); d->srb->updateResources(QRhiShaderResourceBindings::BindingsAreSorted); - d->lastUsedTexture = texture; + + if (textureExtra) { + d->srbExtra->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d->ubuf, 0, UBUF_SIZE), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, textureExtra, m_sampler) + }); + + d->srbExtra->updateResources(QRhiShaderResourceBindings::BindingsAreSorted); + d->lastUsedTextureExtra = textureExtra; + } } void QBackingStoreDefaultCompositor::updateUniforms(PerQuadData *d, QRhiResourceUpdateBatch *resourceUpdates, @@ -534,11 +554,14 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo continue; } QRhiTexture *t = textures->texture(i); + QRhiTexture *tExtra = textures->textureExtra(i); if (t) { - if (!m_textureQuadData[i].isValid()) - m_textureQuadData[i] = createPerQuadData(t); - else - updatePerQuadData(&m_textureQuadData[i], t); + if (!m_textureQuadData[i].isValid()) { + m_textureQuadData[i] = createPerQuadData(t, tExtra); + } + else { + updatePerQuadData(&m_textureQuadData[i], t, tExtra); + } updateUniforms(&m_textureQuadData[i], resourceUpdates, target, source, NoOption); } else { m_textureQuadData[i].reset(); @@ -549,47 +572,74 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo QRhiCommandBuffer *cb = swapchain->currentFrameCommandBuffer(); const QSize outputSizeInPixels = swapchain->currentPixelSize(); QColor clearColor = translucentBackground ? Qt::transparent : Qt::black; - cb->beginPass(swapchain->currentFrameRenderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates); - cb->setGraphicsPipeline(m_psNoBlend); - cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) }); - QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf, 0); - cb->setVertexInput(0, 1, &vbufBinding); + cb->resourceUpdate(resourceUpdates); - // Textures for renderToTexture widgets. - for (int i = 0; i < textureWidgetCount; ++i) { - if (!textures->flags(i).testFlag(QPlatformTextureList::StacksOnTop)) { - if (m_textureQuadData[i].isValid()) { - cb->setShaderResources(m_textureQuadData[i].srb); - cb->draw(6); + auto render = [&](std::optional<QRhiSwapChain::StereoTargetBuffer> buffer = std::nullopt) { + QRhiRenderTarget* target = nullptr; + if (buffer.has_value()) + target = swapchain->currentFrameRenderTarget(buffer.value()); + else + target = swapchain->currentFrameRenderTarget(); + + cb->beginPass(target, clearColor, { 1.0f, 0 }); + + cb->setGraphicsPipeline(m_psNoBlend); + cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) }); + QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf, 0); + cb->setVertexInput(0, 1, &vbufBinding); + + // Textures for renderToTexture widgets. + for (int i = 0; i < textureWidgetCount; ++i) { + if (!textures->flags(i).testFlag(QPlatformTextureList::StacksOnTop)) { + if (m_textureQuadData[i].isValid()) { + + QRhiShaderResourceBindings* srb = m_textureQuadData[i].srb; + if (buffer == QRhiSwapChain::RightBuffer && m_textureQuadData[i].srbExtra) + srb = m_textureQuadData[i].srbExtra; + + cb->setShaderResources(srb); + cb->draw(6); + } } } - } - cb->setGraphicsPipeline(premultiplied ? m_psPremulBlend : m_psBlend); + cb->setGraphicsPipeline(premultiplied ? m_psPremulBlend : m_psBlend); - // Backingstore texture with the normal widgets. - if (m_texture) { - cb->setShaderResources(m_widgetQuadData.srb); - cb->draw(6); - } + // Backingstore texture with the normal widgets. + if (m_texture) { + cb->setShaderResources(m_widgetQuadData.srb); + cb->draw(6); + } - // Textures for renderToTexture widgets that have WA_AlwaysStackOnTop set. - for (int i = 0; i < textureWidgetCount; ++i) { - const QPlatformTextureList::Flags flags = textures->flags(i); - if (flags.testFlag(QPlatformTextureList::StacksOnTop)) { - if (m_textureQuadData[i].isValid()) { - if (flags.testFlag(QPlatformTextureList::NeedsPremultipliedAlphaBlending)) - cb->setGraphicsPipeline(m_psPremulBlend); - else - cb->setGraphicsPipeline(m_psBlend); - cb->setShaderResources(m_textureQuadData[i].srb); - cb->draw(6); + // Textures for renderToTexture widgets that have WA_AlwaysStackOnTop set. + for (int i = 0; i < textureWidgetCount; ++i) { + const QPlatformTextureList::Flags flags = textures->flags(i); + if (flags.testFlag(QPlatformTextureList::StacksOnTop)) { + if (m_textureQuadData[i].isValid()) { + if (flags.testFlag(QPlatformTextureList::NeedsPremultipliedAlphaBlending)) + cb->setGraphicsPipeline(m_psPremulBlend); + else + cb->setGraphicsPipeline(m_psBlend); + + QRhiShaderResourceBindings* srb = m_textureQuadData[i].srb; + if (buffer == QRhiSwapChain::RightBuffer && m_textureQuadData[i].srbExtra) + srb = m_textureQuadData[i].srbExtra; + + cb->setShaderResources(srb); + cb->draw(6); + } } } - } - cb->endPass(); + cb->endPass(); + }; + + if (swapchain->window()->format().stereo()) { + render(QRhiSwapChain::LeftBuffer); + render(QRhiSwapChain::RightBuffer); + } else + render(); rhi->endFrame(swapchain); diff --git a/src/gui/painting/qbackingstoredefaultcompositor_p.h b/src/gui/painting/qbackingstoredefaultcompositor_p.h index 75080f69946..d69c17f98f3 100644 --- a/src/gui/painting/qbackingstoredefaultcompositor_p.h +++ b/src/gui/painting/qbackingstoredefaultcompositor_p.h @@ -70,21 +70,28 @@ private: QRhiBuffer *ubuf = nullptr; // All srbs are layout-compatible. QRhiShaderResourceBindings *srb = nullptr; + QRhiShaderResourceBindings *srbExtra = nullptr; // may be null (used for stereo) QRhiTexture *lastUsedTexture = nullptr; + QRhiTexture *lastUsedTextureExtra = nullptr; // may be null (used for stereo) bool isValid() const { return ubuf && srb; } void reset() { delete ubuf; ubuf = nullptr; delete srb; srb = nullptr; + if (srbExtra) { + delete srbExtra; + srbExtra = nullptr; + } lastUsedTexture = nullptr; + lastUsedTextureExtra = nullptr; } }; PerQuadData m_widgetQuadData; QVarLengthArray<PerQuadData, 8> m_textureQuadData; - PerQuadData createPerQuadData(QRhiTexture *texture); - void updatePerQuadData(PerQuadData *d, QRhiTexture *texture); + PerQuadData createPerQuadData(QRhiTexture *texture, QRhiTexture *textureExtra = nullptr); + void updatePerQuadData(PerQuadData *d, QRhiTexture *texture, QRhiTexture *textureExtra = nullptr); void updateUniforms(PerQuadData *d, QRhiResourceUpdateBatch *resourceUpdates, const QMatrix4x4 &target, const QMatrix3x3 &source, UpdateUniformOption option); }; diff --git a/src/gui/painting/qplatformbackingstore.cpp b/src/gui/painting/qplatformbackingstore.cpp index 8a230030656..82e7778b86a 100644 --- a/src/gui/painting/qplatformbackingstore.cpp +++ b/src/gui/painting/qplatformbackingstore.cpp @@ -37,6 +37,7 @@ struct QBackingstoreTextureInfo { void *source; // may be null QRhiTexture *texture; + QRhiTexture *textureExtra; QRect rect; QRect clipRect; QPlatformTextureList::Flags flags; @@ -77,6 +78,12 @@ QRhiTexture *QPlatformTextureList::texture(int index) const return d->textures.at(index).texture; } +QRhiTexture *QPlatformTextureList::textureExtra(int index) const +{ + Q_D(const QPlatformTextureList); + return d->textures.at(index).textureExtra; +} + void *QPlatformTextureList::source(int index) { Q_D(const QPlatformTextureList); @@ -123,6 +130,22 @@ void QPlatformTextureList::appendTexture(void *source, QRhiTexture *texture, con QBackingstoreTextureInfo bi; bi.source = source; bi.texture = texture; + bi.textureExtra = nullptr; + bi.rect = geometry; + bi.clipRect = clipRect; + bi.flags = flags; + d->textures.append(bi); +} + +void QPlatformTextureList::appendTexture(void *source, QRhiTexture *textureLeft, QRhiTexture *textureRight, const QRect &geometry, + const QRect &clipRect, Flags flags) +{ + Q_D(QPlatformTextureList); + + QBackingstoreTextureInfo bi; + bi.source = source; + bi.texture = textureLeft; + bi.textureExtra = textureRight; bi.rect = geometry; bi.clipRect = clipRect; bi.flags = flags; diff --git a/src/gui/painting/qplatformbackingstore.h b/src/gui/painting/qplatformbackingstore.h index c6b66e57efd..40453574aaf 100644 --- a/src/gui/painting/qplatformbackingstore.h +++ b/src/gui/painting/qplatformbackingstore.h @@ -103,6 +103,7 @@ public: int count() const; bool isEmpty() const { return count() == 0; } QRhiTexture *texture(int index) const; + QRhiTexture *textureExtra(int index) const; QRect geometry(int index) const; QRect clipRect(int index) const; void *source(int index); @@ -112,6 +113,9 @@ public: void appendTexture(void *source, QRhiTexture *texture, const QRect &geometry, const QRect &clipRect = QRect(), Flags flags = { }); + + void appendTexture(void *source, QRhiTexture *textureLeft, QRhiTexture *textureRight, const QRect &geometry, + const QRect &clipRect = QRect(), Flags flags = { }); void clear(); Q_SIGNALS: diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index 79c7c96158a..ccb6db445be 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -4883,18 +4883,18 @@ QRhiResource::Type QRhiSwapChain::resourceType() const is backed by two color buffers, one for each eye, instead of just one. When stereoscopic rendering is not supported, the return value will be - null. For the time being the only backend and 3D API where traditional + the default target. For the time being the only backend and 3D API where traditional stereoscopic rendering is supported is OpenGL (excluding OpenGL ES), in combination with \l QSurfaceFormat::StereoBuffers, assuming it is supported by the graphics and display driver stack at run time. All other backends - are going to return null from this overload. + are going to return the default render target from this overload. \note the value must not be cached and reused between frames */ QRhiRenderTarget *QRhiSwapChain::currentFrameRenderTarget(StereoTargetBuffer targetBuffer) { Q_UNUSED(targetBuffer); - return nullptr; + return currentFrameRenderTarget(); } /*! diff --git a/src/openglwidgets/qopenglwidget.cpp b/src/openglwidgets/qopenglwidget.cpp index 1e3f32e3eb6..7a0ac05cee2 100644 --- a/src/openglwidgets/qopenglwidget.cpp +++ b/src/openglwidgets/qopenglwidget.cpp @@ -415,6 +415,26 @@ QT_BEGIN_NAMESPACE certain desktop platforms (e.g. \macos) too. The stable, cross-platform solution is always QOpenGLWidget. + + \section1 Stereoscopic rendering + + Starting from 6.5 QOpenGLWidget has support for stereoscopic rendering. + To enable it, set the QSurfaceFormat::StereoBuffers flag + globally before the window is created, using QSurfaceFormat::SetDefaultFormat(). + + \note Using setFormat() will not necessarily work because of how the flag is + handled internally. + + This will trigger paintGL() to be called twice each frame, + once for each QOpenGLWidget::TargetBuffer. In paintGL(), call + currentTargetBuffer() to query which one is currently being drawn to. + + \note For more control over the left and right color buffers, consider using + QOpenGLWindow + QWidget::createWindowContainer() instead. + + \note This type of 3D rendering has certain hardware requirements, + like the graphics card needs to be setup with stereo support. + \e{OpenGL is a trademark of Silicon Graphics, Inc. in the United States and other countries.} @@ -451,6 +471,20 @@ QT_BEGIN_NAMESPACE */ /*! + \enum QOpenGLWidget::TargetBuffer + \since 6.5 + + Specifies the buffer to use when stereoscopic rendering is enabled, which is + toggled by setting \l QSurfaceFormat::StereoBuffers. + + \note LeftBuffer is always the default and used as fallback value when + stereoscopic rendering is disabled or not supported by the graphics driver. + + \value LeftBuffer + \value RightBuffer + */ + +/*! \enum QOpenGLWidget::UpdateBehavior \since 5.5 @@ -503,10 +537,10 @@ public: void reset(); void resetRhiDependentResources(); - void recreateFbo(); + void recreateFbos(); void ensureRhiDependentResources(); - QRhiTexture *texture() const override; + QWidgetPrivate::TextureData texture() const override; QPlatformTextureList::Flags textureListFlags() override; QPlatformBackingStoreRhiConfig rhiConfig() const override { return { QPlatformBackingStoreRhiConfig::OpenGL }; } @@ -516,6 +550,10 @@ public: void invalidateFbo(); + void destroyFbos(); + + void setCurrentTargetBuffer(QOpenGLWidget::TargetBuffer targetBuffer); + QImage grabFramebuffer(QOpenGLWidget::TargetBuffer targetBuffer); QImage grabFramebuffer() override; void beginBackingStorePainting() override { inBackingStorePaint = true; } void endBackingStorePainting() override { inBackingStorePaint = false; } @@ -526,9 +564,9 @@ public: void resolveSamples() override; QOpenGLContext *context = nullptr; - QRhiTexture *wrapperTexture = nullptr; - QOpenGLFramebufferObject *fbo = nullptr; - QOpenGLFramebufferObject *resolvedFbo = nullptr; + QRhiTexture *wrapperTextures[2] = {}; + QOpenGLFramebufferObject *fbos[2] = {}; + QOpenGLFramebufferObject *resolvedFbos[2] = {}; QOffscreenSurface *surface = nullptr; QOpenGLPaintDevice *paintDevice = nullptr; int requestedSamples = 0; @@ -541,6 +579,7 @@ public: bool hasBeenComposed = false; bool flushPending = false; bool inPaintGL = false; + QOpenGLWidget::TargetBuffer currentTargetBuffer = QOpenGLWidget::LeftBuffer; }; void QOpenGLWidgetPaintDevicePrivate::beginPaint() @@ -582,10 +621,11 @@ void QOpenGLWidgetPaintDevice::ensureActiveTarget() if (QOpenGLContext::currentContext() != wd->context) d->w->makeCurrent(); else - wd->fbo->bind(); + wd->fbos[wd->currentTargetBuffer]->bind(); + if (!wd->inPaintGL) - QOpenGLContextPrivate::get(wd->context)->defaultFboRedirect = wd->fbo->handle(); + QOpenGLContextPrivate::get(wd->context)->defaultFboRedirect = wd->fbos[wd->currentTargetBuffer]->handle(); // When used as a viewport, drawing is done via opening a QPainter on the widget // without going through paintEvent(). We will have to make sure a glFlush() is done @@ -593,9 +633,9 @@ void QOpenGLWidgetPaintDevice::ensureActiveTarget() wd->flushPending = true; } -QRhiTexture *QOpenGLWidgetPrivate::texture() const +QWidgetPrivate::TextureData QOpenGLWidgetPrivate::texture() const { - return wrapperTexture; + return { wrapperTextures[QOpenGLWidget::LeftBuffer], wrapperTextures[QOpenGLWidget::RightBuffer] }; } #ifndef GL_SRGB @@ -637,10 +677,8 @@ void QOpenGLWidgetPrivate::reset() delete paintDevice; paintDevice = nullptr; - delete fbo; - fbo = nullptr; - delete resolvedFbo; - resolvedFbo = nullptr; + + destroyFbos(); resetRhiDependentResources(); @@ -659,15 +697,22 @@ void QOpenGLWidgetPrivate::reset() void QOpenGLWidgetPrivate::resetRhiDependentResources() { + Q_Q(QOpenGLWidget); + // QRhi resource created from the QRhi. These must be released whenever the // widget gets associated with a different QRhi, even when all OpenGL // contexts share resources. - delete wrapperTexture; - wrapperTexture = nullptr; + delete wrapperTextures[0]; + wrapperTextures[0] = nullptr; + + if (q->format().stereo()) { + delete wrapperTextures[1]; + wrapperTextures[1] = nullptr; + } } -void QOpenGLWidgetPrivate::recreateFbo() +void QOpenGLWidgetPrivate::recreateFbos() { Q_Q(QOpenGLWidget); @@ -675,10 +720,7 @@ void QOpenGLWidgetPrivate::recreateFbo() context->makeCurrent(surface); - delete fbo; - fbo = nullptr; - delete resolvedFbo; - resolvedFbo = nullptr; + destroyFbos(); int samples = requestedSamples; QOpenGLExtensions *extfuncs = static_cast<QOpenGLExtensions *>(context->functions()); @@ -692,21 +734,37 @@ void QOpenGLWidgetPrivate::recreateFbo() format.setInternalTextureFormat(textureFormat); const QSize deviceSize = q->size() * q->devicePixelRatio(); - fbo = new QOpenGLFramebufferObject(deviceSize, format); + fbos[QOpenGLWidget::LeftBuffer] = new QOpenGLFramebufferObject(deviceSize, format); if (samples > 0) - resolvedFbo = new QOpenGLFramebufferObject(deviceSize); + resolvedFbos[QOpenGLWidget::LeftBuffer] = new QOpenGLFramebufferObject(deviceSize); + + const bool stereoEnabled = q->format().stereo(); + + if (stereoEnabled) { + fbos[QOpenGLWidget::RightBuffer] = new QOpenGLFramebufferObject(deviceSize, format); + if (samples > 0) + resolvedFbos[QOpenGLWidget::RightBuffer] = new QOpenGLFramebufferObject(deviceSize); + } - textureFormat = fbo->format().internalTextureFormat(); + textureFormat = fbos[QOpenGLWidget::LeftBuffer]->format().internalTextureFormat(); - fbo->bind(); + currentTargetBuffer = QOpenGLWidget::LeftBuffer; + fbos[currentTargetBuffer]->bind(); context->functions()->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + ensureRhiDependentResources(); + + if (stereoEnabled) { + currentTargetBuffer = QOpenGLWidget::RightBuffer; + fbos[currentTargetBuffer]->bind(); + context->functions()->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + ensureRhiDependentResources(); + } + flushPending = true; // Make sure the FBO is initialized before use paintDevice->setSize(deviceSize); paintDevice->setDevicePixelRatio(q->devicePixelRatio()); - ensureRhiDependentResources(); - emit q->resized(); } @@ -721,13 +779,15 @@ void QOpenGLWidgetPrivate::ensureRhiDependentResources() // If there is no rhi, because we are completely offscreen, then there's no wrapperTexture either if (rhi && rhi->backend() == QRhi::OpenGLES2) { const QSize deviceSize = q->size() * q->devicePixelRatio(); - if (!wrapperTexture || wrapperTexture->pixelSize() != deviceSize) { - const uint textureId = resolvedFbo ? resolvedFbo->texture() : (fbo ? fbo->texture() : 0); - if (!wrapperTexture) - wrapperTexture = rhi->newTexture(QRhiTexture::RGBA8, deviceSize, 1, QRhiTexture::RenderTarget); + if (!wrapperTextures[currentTargetBuffer] || wrapperTextures[currentTargetBuffer]->pixelSize() != deviceSize) { + const uint textureId = resolvedFbos[currentTargetBuffer] ? + resolvedFbos[currentTargetBuffer]->texture() + : (fbos[currentTargetBuffer] ? fbos[currentTargetBuffer]->texture() : 0); + if (!wrapperTextures[currentTargetBuffer]) + wrapperTextures[currentTargetBuffer] = rhi->newTexture(QRhiTexture::RGBA8, deviceSize, 1, QRhiTexture::RenderTarget); else - wrapperTexture->setPixelSize(deviceSize); - if (!wrapperTexture->createFrom({textureId, 0 })) + wrapperTextures[currentTargetBuffer]->setPixelSize(deviceSize); + if (!wrapperTextures[currentTargetBuffer]->createFrom({textureId, 0 })) qWarning("QOpenGLWidget: Failed to create wrapper texture"); } } @@ -836,10 +896,10 @@ void QOpenGLWidgetPrivate::initialize() void QOpenGLWidgetPrivate::resolveSamples() { Q_Q(QOpenGLWidget); - if (resolvedFbo) { + if (resolvedFbos[currentTargetBuffer]) { q->makeCurrent(); - QRect rect(QPoint(0, 0), fbo->size()); - QOpenGLFramebufferObject::blitFramebuffer(resolvedFbo, rect, fbo, rect); + QRect rect(QPoint(0, 0), fbos[currentTargetBuffer]->size()); + QOpenGLFramebufferObject::blitFramebuffer(resolvedFbos[currentTargetBuffer], rect, fbos[currentTargetBuffer], rect); flushPending = true; } } @@ -851,33 +911,56 @@ void QOpenGLWidgetPrivate::render() if (fakeHidden || !initialized) return; - q->makeCurrent(); + setCurrentTargetBuffer(QOpenGLWidget::LeftBuffer); QOpenGLContext *ctx = QOpenGLContext::currentContext(); if (!ctx) { qWarning("QOpenGLWidget: No current context, cannot render"); return; } - if (!fbo) { + + if (!fbos[QOpenGLWidget::LeftBuffer]) { qWarning("QOpenGLWidget: No fbo, cannot render"); return; } + const bool stereoEnabled = q->format().stereo(); + if (stereoEnabled) { + static bool warningGiven = false; + if (!fbos[QOpenGLWidget::RightBuffer] && !warningGiven) { + qWarning("QOpenGLWidget: Stereo is enabled, but no right buffer. Using only left buffer"); + warningGiven = true; + } + } + if (updateBehavior == QOpenGLWidget::NoPartialUpdate && hasBeenComposed) { invalidateFbo(); + + if (stereoEnabled && fbos[QOpenGLWidget::RightBuffer]) { + setCurrentTargetBuffer(QOpenGLWidget::RightBuffer); + invalidateFbo(); + setCurrentTargetBuffer(QOpenGLWidget::LeftBuffer); + } + hasBeenComposed = false; } QOpenGLFunctions *f = ctx->functions(); - QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = fbo->handle(); - f->glViewport(0, 0, q->width() * q->devicePixelRatio(), q->height() * q->devicePixelRatio()); inPaintGL = true; + + QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = fbos[currentTargetBuffer]->handle(); q->paintGL(); - inPaintGL = false; - flushPending = true; + if (stereoEnabled && fbos[QOpenGLWidget::RightBuffer]) { + setCurrentTargetBuffer(QOpenGLWidget::RightBuffer); + QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = fbos[currentTargetBuffer]->handle(); + q->paintGL(); + } QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = 0; + + inPaintGL = false; + flushPending = true; } void QOpenGLWidgetPrivate::invalidateFbo() @@ -906,25 +989,47 @@ void QOpenGLWidgetPrivate::invalidateFbo() } } +void QOpenGLWidgetPrivate::destroyFbos() +{ + delete fbos[QOpenGLWidget::LeftBuffer]; + fbos[QOpenGLWidget::LeftBuffer] = nullptr; + delete resolvedFbos[QOpenGLWidget::LeftBuffer]; + resolvedFbos[QOpenGLWidget::LeftBuffer] = nullptr; + + delete fbos[QOpenGLWidget::RightBuffer]; + fbos[QOpenGLWidget::RightBuffer] = nullptr; + delete resolvedFbos[QOpenGLWidget::RightBuffer]; + resolvedFbos[QOpenGLWidget::RightBuffer] = nullptr; +} + QImage QOpenGLWidgetPrivate::grabFramebuffer() { + return grabFramebuffer(QOpenGLWidget::LeftBuffer); +} + +QImage QOpenGLWidgetPrivate::grabFramebuffer(QOpenGLWidget::TargetBuffer targetBuffer) +{ Q_Q(QOpenGLWidget); initialize(); if (!initialized) return QImage(); - if (!fbo) // could be completely offscreen, without ever getting a resize event - recreateFbo(); + // The second fbo is only created when stereoscopic rendering is enabled + // Just use the default one if not. + if (targetBuffer == QOpenGLWidget::RightBuffer && !q->format().stereo()) + targetBuffer = QOpenGLWidget::LeftBuffer; + + if (!fbos[targetBuffer]) // could be completely offscreen, without ever getting a resize event + recreateFbos(); if (!inPaintGL) render(); - if (resolvedFbo) { + setCurrentTargetBuffer(targetBuffer); + if (resolvedFbos[targetBuffer]) { resolveSamples(); - resolvedFbo->bind(); - } else { - q->makeCurrent(); + resolvedFbos[targetBuffer]->bind(); } const bool hasAlpha = q->format().hasAlpha(); @@ -934,8 +1039,9 @@ QImage QOpenGLWidgetPrivate::grabFramebuffer() // While we give no guarantees of what is going to be left bound, prefer the // multisample fbo instead of the resolved one. Clients may continue to // render straight after calling this function. - if (resolvedFbo) - q->makeCurrent(); + if (resolvedFbos[targetBuffer]) { + setCurrentTargetBuffer(targetBuffer); + } return res; } @@ -954,8 +1060,8 @@ void QOpenGLWidgetPrivate::resizeViewportFramebuffer() if (!initialized) return; - if (!fbo || q->size() * q->devicePixelRatio() != fbo->size()) { - recreateFbo(); + if (!fbos[currentTargetBuffer] || q->size() * q->devicePixelRatio() != fbos[currentTargetBuffer]->size()) { + recreateFbos(); q->update(); } } @@ -1143,8 +1249,8 @@ void QOpenGLWidget::makeCurrent() d->context->makeCurrent(d->surface); - if (d->fbo) // there may not be one if we are in reset() - d->fbo->bind(); + if (d->fbos[d->currentTargetBuffer]) // there may not be one if we are in reset() + d->fbos[d->currentTargetBuffer]->bind(); } /*! @@ -1192,7 +1298,31 @@ QOpenGLContext *QOpenGLWidget::context() const GLuint QOpenGLWidget::defaultFramebufferObject() const { Q_D(const QOpenGLWidget); - return d->fbo ? d->fbo->handle() : 0; + return d->fbos[TargetBuffer::LeftBuffer] ? d->fbos[TargetBuffer::LeftBuffer]->handle() : 0; +} + +/*! + \return The framebuffer object handle of the specified target buffer or + \c 0 if not yet initialized. + + \note Calling this overload only makes sense if \l QSurfaceFormat::StereoBuffer is enabled + and supported by the hardware. Will return the default buffer if it's not. + + \note The framebuffer object belongs to the context returned by context() + and may not be accessible from other contexts. + + \note The context and the framebuffer object used by the widget changes when + reparenting the widget via setParent(). In addition, the framebuffer object + changes on each resize. + + \since 6.5 + + \sa context() + */ +GLuint QOpenGLWidget::defaultFramebufferObject(TargetBuffer targetBuffer) const +{ + Q_D(const QOpenGLWidget); + return d->fbos[targetBuffer] ? d->fbos[targetBuffer]->handle() : 0; } /*! @@ -1241,7 +1371,15 @@ void QOpenGLWidget::resizeGL(int w, int h) other state is set and no clearing or drawing is performed by the framework. - \sa initializeGL(), resizeGL() + When \l QSurfaceFormat::StereoBuffers is enabled, this function + will be called twice - once for each buffer. Query what buffer is + currently bound by calling currentTargetBuffer(). + + \note The framebuffer of each target will be drawn to even when + stereoscopic rendering is not supported by the hardware. + Only the left buffer will actually be visible in the window. + + \sa initializeGL(), resizeGL(), currentTargetBuffer() */ void QOpenGLWidget::paintGL() { @@ -1270,7 +1408,7 @@ void QOpenGLWidget::resizeEvent(QResizeEvent *e) if (!d->initialized) return; - d->recreateFbo(); + d->recreateFbos(); // Make sure our own context is current before invoking user overrides. If // the fbo was recreated then there's a chance something else is current now. makeCurrent(); @@ -1315,6 +1453,39 @@ QImage QOpenGLWidget::grabFramebuffer() } /*! + Renders and returns a 32-bit RGB image of the framebuffer of the specified target buffer. + This overload only makes sense to call when \l QSurfaceFormat::StereoBuffers is enabled. + Grabbing the framebuffer of the right target buffer will return the default image + if stereoscopic rendering is disabled or if not supported by the hardware. + + \note This is a potentially expensive operation because it relies on glReadPixels() + to read back the pixels. This may be slow and can stall the GPU pipeline. + + \since 6.5 +*/ +QImage QOpenGLWidget::grabFramebuffer(TargetBuffer targetBuffer) +{ + Q_D(QOpenGLWidget); + return d->grabFramebuffer(targetBuffer); +} + +/*! + Returns the currently active target buffer. This will be the left buffer by default, + the right buffer is only used when \l QSurfaceFormat::StereoBuffers is enabled. + When stereoscopic rendering is enabled, this can be queried in paintGL() to know + what buffer is currently in use. paintGL() will be called twice, once for each target. + + \since 6.5 + + \sa paintGL() +*/ +QOpenGLWidget::TargetBuffer QOpenGLWidget::currentTargetBuffer() const +{ + Q_D(const QOpenGLWidget); + return d->currentTargetBuffer; +} + +/*! \reimp */ int QOpenGLWidget::metric(QPaintDevice::PaintDeviceMetric metric) const @@ -1414,6 +1585,13 @@ QPaintEngine *QOpenGLWidget::paintEngine() const return d->paintDevice->paintEngine(); } +void QOpenGLWidgetPrivate::setCurrentTargetBuffer(QOpenGLWidget::TargetBuffer targetBuffer) +{ + Q_Q(QOpenGLWidget); + currentTargetBuffer = targetBuffer; + q->makeCurrent(); +} + /*! \reimp */ @@ -1433,7 +1611,7 @@ bool QOpenGLWidget::event(QEvent *e) break; Q_FALLTHROUGH(); case QEvent::Show: // reparenting may not lead to a resize so reinitialize on Show too - if (d->initialized && !d->wrapperTexture && window()->windowHandle()) { + if (d->initialized && !d->wrapperTextures[d->currentTargetBuffer] && window()->windowHandle()) { // Special case: did grabFramebuffer() for a hidden widget that then became visible. // Recreate all resources since the context now needs to share with the TLW's. if (!QCoreApplication::testAttribute(Qt::AA_ShareOpenGLContexts)) @@ -1443,7 +1621,7 @@ bool QOpenGLWidget::event(QEvent *e) if (!d->initialized && !size().isEmpty() && repaintManager->rhi()) { d->initialize(); if (d->initialized) { - d->recreateFbo(); + d->recreateFbos(); // QTBUG-89812: generate a paint event, like resize would do, // otherwise a QOpenGLWidget in a QDockWidget may not show the // content upon (un)docking. @@ -1454,7 +1632,7 @@ bool QOpenGLWidget::event(QEvent *e) break; case QEvent::ScreenChangeInternal: if (d->initialized && d->paintDevice->devicePixelRatio() != devicePixelRatio()) - d->recreateFbo(); + d->recreateFbos(); break; default: break; diff --git a/src/openglwidgets/qopenglwidget.h b/src/openglwidgets/qopenglwidget.h index edd731ae8ec..84097854e59 100644 --- a/src/openglwidgets/qopenglwidget.h +++ b/src/openglwidgets/qopenglwidget.h @@ -25,6 +25,11 @@ public: PartialUpdate }; + enum TargetBuffer { + LeftBuffer = 0, // Default + RightBuffer // Only used when QSurfaceFormat::StereoBuffers is enabled + }; + explicit QOpenGLWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); ~QOpenGLWidget(); @@ -44,8 +49,12 @@ public: QOpenGLContext *context() const; GLuint defaultFramebufferObject() const; + GLuint defaultFramebufferObject(TargetBuffer targetBuffer) const; QImage grabFramebuffer(); + QImage grabFramebuffer(TargetBuffer targetBuffer); + + TargetBuffer currentTargetBuffer() const; Q_SIGNALS: void aboutToCompose(); diff --git a/src/widgets/kernel/qwidget_p.h b/src/widgets/kernel/qwidget_p.h index 65e368d9458..ea03e9c7084 100644 --- a/src/widgets/kernel/qwidget_p.h +++ b/src/widgets/kernel/qwidget_p.h @@ -579,7 +579,13 @@ public: virtual QPlatformBackingStoreRhiConfig rhiConfig() const { return {}; } - virtual QRhiTexture *texture() const { return nullptr; } + // Note that textureRight may be null, as it's only used in stereoscopic rendering + struct TextureData { + QRhiTexture *textureLeft = nullptr; + QRhiTexture *textureRight = nullptr; + }; + + virtual TextureData texture() const { return {}; } virtual QPlatformTextureList::Flags textureListFlags() { Q_Q(QWidget); return q->testAttribute(Qt::WA_AlwaysStackOnTop) diff --git a/src/widgets/kernel/qwidgetrepaintmanager.cpp b/src/widgets/kernel/qwidgetrepaintmanager.cpp index cdd8cee0168..16db49f95e3 100644 --- a/src/widgets/kernel/qwidgetrepaintmanager.cpp +++ b/src/widgets/kernel/qwidgetrepaintmanager.cpp @@ -539,7 +539,8 @@ static void findTextureWidgetsRecursively(QWidget *tlw, QWidget *widget, if (wd->renderToTexture) { QPlatformTextureList::Flags flags = wd->textureListFlags(); const QRect rect(widget->mapTo(tlw, QPoint()), widget->size()); - widgetTextures->appendTexture(widget, wd->texture(), rect, wd->clipRect(), flags); + QWidgetPrivate::TextureData data = wd->texture(); + widgetTextures->appendTexture(widget, data.textureLeft, data.textureRight, rect, wd->clipRect(), flags); } for (int i = 0; i < wd->children.size(); ++i) { |