aboutsummaryrefslogtreecommitdiffstats
path: root/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py
diff options
context:
space:
mode:
Diffstat (limited to 'examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py')
-rw-r--r--examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py272
1 files changed, 272 insertions, 0 deletions
diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py b/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py
new file mode 100644
index 000000000..6f1e61f94
--- /dev/null
+++ b/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py
@@ -0,0 +1,272 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import numpy
+from pathlib import Path
+import weakref
+from OpenGL.GL import (GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER,
+ GL_NEAREST, GL_RGBA, GL_TEXTURE_2D, GL_UNSIGNED_BYTE)
+
+from PySide6.QtGui import (QMouseEvent, QOffscreenSurface,
+ QOpenGLContext, QSurface,
+ QSurfaceFormat, QWindow)
+from PySide6.QtOpenGL import QOpenGLFramebufferObject
+from PySide6.QtQml import QQmlComponent, QQmlEngine
+from PySide6.QtQuick import (QQuickGraphicsDevice,
+ QQuickRenderControl,
+ QQuickRenderTarget, QQuickWindow)
+from PySide6.QtCore import QCoreApplication, QTimer, QUrl, Slot
+from shiboken6 import VoidPtr
+
+from cuberenderer import CubeRenderer
+
+
+class RenderControl(QQuickRenderControl):
+ def __init__(self, window=None):
+ super().__init__()
+ self._window = window
+
+ def renderWindow(self, offset):
+ return self._window() # Dereference the weak reference
+
+
+class WindowSingleThreaded(QWindow):
+
+ def __init__(self):
+ super().__init__()
+ self.m_rootItem = None
+ self.m_device = None
+ self.m_texture_ids = numpy.array([0], dtype=numpy.uint32)
+
+ self.m_quickInitialized = False
+ self.m_quickReady = False
+ self.m_dpr = 0
+ self.m_status_conn_id = None
+ self.setSurfaceType(QSurface.OpenGLSurface)
+
+ format = QSurfaceFormat()
+ # Qt Quick may need a depth and stencil buffer. Always make sure these
+ # are available.
+ format.setDepthBufferSize(16)
+ format.setStencilBufferSize(8)
+ self.setFormat(format)
+
+ self.m_context = QOpenGLContext()
+ self.m_context.setFormat(format)
+ self.m_context.create()
+
+ self.m_offscreenSurface = QOffscreenSurface()
+ # Pass m_context.format(), not format. Format does not specify and
+ # color buffer sizes, while the context, that has just been created,
+ # reports a format that has these values filled in. Pass self to the
+ # offscreen surface to make sure it will be compatible with the
+ # context's configuration.
+ self.m_offscreenSurface.setFormat(self.m_context.format())
+ self.m_offscreenSurface.create()
+
+ self.m_cubeRenderer = CubeRenderer(self.m_offscreenSurface)
+
+ self.m_renderControl = RenderControl(weakref.ref(self))
+
+ # Create a QQuickWindow that is associated with out render control.
+ # Note that this window never gets created or shown, meaning that
+ # will never get an underlying native (platform) window.
+ self.m_quickWindow = QQuickWindow(self.m_renderControl)
+
+ # Create a QML engine.
+ self.m_qmlEngine = QQmlEngine()
+ if not self.m_qmlEngine.incubationController():
+ c = self.m_quickWindow.incubationController()
+ self.m_qmlEngine.setIncubationController(c)
+
+ # When Quick says there is a need to render, we will not render
+ # immediately. Instead, a timer with a small interval is used
+ # to get better performance.
+ self.m_updateTimer = QTimer()
+ self.m_updateTimer.setSingleShot(True)
+ self.m_updateTimer.setInterval(5)
+ self.m_updateTimer.timeout.connect(self.render)
+
+ # Now hook up the signals. For simplicy we don't differentiate between
+ # renderRequested (only render is needed, no sync) and sceneChanged
+ # (polish and sync is needed too).
+ self.m_quickWindow.sceneGraphInitialized.connect(self.createTexture)
+ self.m_quickWindow.sceneGraphInvalidated.connect(self.destroyTexture)
+ self.m_renderControl.renderRequested.connect(self.requestUpdate)
+ self.m_renderControl.sceneChanged.connect(self.requestUpdate)
+
+ # Just recreating the texture on resize is not sufficient, when moving
+ # between screens with different devicePixelRatio the QWindow size may
+ # remain the same but the texture dimension is to change regardless.
+ self.screenChanged.connect(self.handleScreenChange)
+
+ def __del__(self):
+ # Make sure the context is current while doing cleanup. Note that
+ # we use the offscreen surface here because passing 'self' at self
+ # point is not safe: the underlying platform window may already be
+ # destroyed. To avoid all the trouble, use another surface that is
+ # valid for sure.
+ self.m_context.makeCurrent(self.m_offscreenSurface)
+
+ del self.m_qmlComponent
+ del self.m_qmlEngine
+ del self.m_quickWindow
+ del self.m_renderControl
+
+ if self.texture_id():
+ self.m_context.functions().glDeleteTextures(1, self.m_texture_ids)
+
+ self.m_context.doneCurrent()
+
+ def texture_id(self):
+ return self.m_texture_ids[0]
+
+ def set_texture_id(self, texture_id):
+ self.m_texture_ids[0] = texture_id
+
+ @Slot()
+ def createTexture(self):
+ # The scene graph has been initialized. It is now time to create a
+ # texture and associate it with the QQuickWindow.
+ self.m_dpr = self.devicePixelRatio()
+ self.m_textureSize = self.size() * self.m_dpr
+ f = self.m_context.functions()
+ f.glGenTextures(1, self.m_texture_ids)
+ f.glBindTexture(GL_TEXTURE_2D, self.texture_id())
+
+ f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
+ f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
+ null = VoidPtr(0)
+ f.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.m_textureSize.width(),
+ self.m_textureSize.height(), 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, null)
+ target = QQuickRenderTarget.fromOpenGLTexture(self.texture_id(),
+ self.m_textureSize)
+ self.m_quickWindow.setRenderTarget(target)
+
+ @Slot()
+ def destroyTexture(self):
+ self.m_context.functions().glDeleteTextures(1, self.m_texture_ids)
+ self.set_texture_id(0)
+
+ @Slot()
+ def render(self):
+ if not self.m_context.makeCurrent(self.m_offscreenSurface):
+ return
+
+ # Polish, synchronize and render the next frame (into our texture).
+ # In this example everything happens on the same thread and therefore
+ # all three steps are performed in succession from here. In a threaded
+ # setup the render() call would happen on a separate thread.
+ self.m_renderControl.beginFrame()
+ self.m_renderControl.polishItems()
+ self.m_renderControl.sync()
+ self.m_renderControl.render()
+ self.m_renderControl.endFrame()
+
+ QOpenGLFramebufferObject.bindDefault()
+ self.m_context.functions().glFlush()
+
+ self.m_quickReady = True
+
+ # Get something onto the screen.
+ texture_id = self.texture_id() if self.m_quickReady else 0
+ self.m_cubeRenderer.render(self, self.m_context, texture_id)
+
+ def requestUpdate(self):
+ if not self.m_updateTimer.isActive():
+ self.m_updateTimer.start()
+
+ def run(self):
+ if self.m_status_conn_id:
+ self.m_qmlComponent.statusChanged.disconnect(self.m_status_conn_id)
+ self.m_status_conn_id = None
+
+ if self.m_qmlComponent.isError():
+ for error in self.m_qmlComponent.errors():
+ print(error.url().toString(), error.line(), error.toString())
+ return
+
+ self.m_rootItem = self.m_qmlComponent.create()
+ if self.m_qmlComponent.isError():
+ for error in self.m_qmlComponent.errors():
+ print(error.url().toString(), error.line(), error.toString())
+ return
+
+ if not self.m_rootItem:
+ print("run: Not a QQuickItem")
+ del self.m_rootItem
+
+ # The root item is ready. Associate it with the window.
+ self.m_rootItem.setParentItem(self.m_quickWindow.contentItem())
+
+ # Update item and rendering related geometries.
+ self.updateSizes()
+
+ # Initialize the render control and our OpenGL resources.
+ self.m_context.makeCurrent(self.m_offscreenSurface)
+ self.m_device = QQuickGraphicsDevice.fromOpenGLContext(self.m_context)
+ self.m_quickWindow.setGraphicsDevice(self.m_device)
+ self.m_renderControl.initialize()
+ self.m_quickInitialized = True
+
+ def updateSizes(self):
+ # Behave like SizeRootObjectToView.
+ w = self.width()
+ h = self.height()
+ self.m_rootItem.setWidth(w)
+ self.m_rootItem.setHeight(h)
+ self.m_quickWindow.setGeometry(0, 0, w, h)
+ self.m_cubeRenderer.resize(w, h)
+
+ def startQuick(self, filename):
+ url = QUrl.fromLocalFile(filename)
+ self.m_qmlComponent = QQmlComponent(self.m_qmlEngine, url)
+ if self.m_qmlComponent.isLoading():
+ self.m_status_conn_id = self.m_qmlComponent.statusChanged.connect(self.run)
+ else:
+ self.run()
+
+ def exposeEvent(self, event):
+ if self.isExposed() and not self.m_quickInitialized:
+ texture_id = self.texture_id() if self.m_quickReady else 0
+ self.m_cubeRenderer.render(self, self.m_context, texture_id)
+ qml_file = Path(__file__).parent / "demo.qml"
+ self.startQuick(qml_file)
+
+ def resizeTexture(self):
+ if self.m_rootItem and self.m_context.makeCurrent(self.m_offscreenSurface):
+ self.m_context.functions().glDeleteTextures(1, self.m_texture_ids)
+ self.set_texture_id(0)
+ self.createTexture()
+ self.m_context.doneCurrent()
+ self.updateSizes()
+ self.render()
+
+ def resizeEvent(self, event):
+ # If self is a resize after the scene is up and running, recreate the
+ # texture and the Quick item and scene.
+ if (self.texture_id()
+ and self.m_textureSize != self.size() * self.devicePixelRatio()):
+ self.resizeTexture()
+
+ @Slot()
+ def handleScreenChange(self):
+ if self.m_dpr != self.devicePixelRatio():
+ self.resizeTexture()
+
+ def mousePressEvent(self, e):
+ # Use the constructor taking position and globalPosition. That puts
+ # position into the event's position and scenePosition, and
+ # globalPosition into the event's globalPosition. This way the
+ # scenePosition in `e` is ignored and is replaced by position.
+ # This is necessary because QQuickWindow thinks of itself as
+ # a top-level window always.
+ mappedEvent = QMouseEvent(e.type(), e.position(), e.globalPosition(),
+ e.button(), e.buttons(), e.modifiers())
+ QCoreApplication.sendEvent(self.m_quickWindow, mappedEvent)
+
+ def mouseReleaseEvent(self, e):
+ mappedEvent = QMouseEvent(e.type(), e.position(), e.globalPosition(),
+ e.button(), e.buttons(), e.modifiers())
+ QCoreApplication.sendEvent(self.m_quickWindow, mappedEvent)