Simple RHI Widget Example

Shows how to render a triangle using QRhi, Qt’s 3D API and shading language abstraction layer.

This example is, in many ways, the counterpart of the RHI Window Example in the QWidget world. The QRhiWidget subclass in this applications renders a single triangle, using a simple graphics pipeline with basic vertex and fragment shaders. Unlike the plain QWindow-based application, this example does not need to worry about lower level details, such as setting up the window and the QRhi, or dealing with swapchain and window events, as that is taken care of by the QWidget framework here. The instance of the QRhiWidget subclass is added to a QVBoxLayout. To keep the example minimal and compact, there are no further widgets or 3D content introduced.

Once an instance of ExampleRhiWidget, a QRhiWidget subclass, is added to a top-level widget’s child hierarchy, the corresponding window automatically becomes a Direct 3D, Vulkan, Metal, or OpenGL-rendered window. The QPainter-rendered widget content, i.e. everything that is not a QRhiWidget, QOpenGLWidget, or QQuickWidget, is then uploaded to a texture, whereas the mentioned special widgets each render to a texture. The resulting set textures is composited together by the top-level widget’s backingstore.

As opposed to the C++ example, the cleanup is done by reimplementing QRhiWidget.releaseResources(), which is called from QWidget.closeEvent() of the top level widget to ensure a deterministic cleanup sequence.

Screenshot of the Simple RHI Widget example

Download this example

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 port of the Qt Simple RHI Widget Example example from Qt v6.x"""

import sys

from PySide6.QtWidgets import QApplication, QVBoxLayout, QWidget

from examplewidget import ExampleRhiWidget
import rc_simplerhiwidget  # noqa F:401


class Widget(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QVBoxLayout(self)
        self._rhi_widget = ExampleRhiWidget(self)
        layout.addWidget(self._rhi_widget)

    def closeEvent(self, e):
        self._rhi_widget.releaseResources()
        e.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)

    w = Widget()
    w.resize(1280, 720)
    w.show()
    exit_code = app.exec()
    del w
    sys.exit(exit_code)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import numpy

from PySide6.QtCore import (QFile, QIODevice)
from PySide6.QtGui import (QColor, QMatrix4x4)
from PySide6.QtGui import (QRhiBuffer,
                           QRhiDepthStencilClearValue,
                           QRhiShaderResourceBinding,
                           QRhiShaderStage,
                           QRhiVertexInputAttribute, QRhiVertexInputBinding,
                           QRhiVertexInputLayout, QRhiViewport,
                           QShader)
from PySide6.QtWidgets import QRhiWidget
from PySide6.support import VoidPtr

VERTEX_DATA = numpy.array([ 0.0,  0.5, 1.0, 0.0, 0.0,  # noqa E:201
                           -0.5, -0.5, 0.0, 1.0, 0.0,  # noqa E:241
                            0.5, -0.5, 0.0, 0.0, 1.0],
                          dtype=numpy.float32)


def getShader(name):
    f = QFile(name)
    if f.open(QIODevice.ReadOnly):
        return QShader.fromSerialized(f.readAll())
    return QShader()


class ExampleRhiWidget(QRhiWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_rhi = None
        self.m_vbuf = None
        self.m_ubuf = None
        self.m_srb = None
        self.m_pipeline = None
        self.m_viewProjection = QMatrix4x4()
        self.m_rotation = 0.0

    def releaseResources(self):
        self.m_pipeline.destroy()
        del self.m_pipeline
        self.m_pipeline = None
        self.m_srb.destroy()
        del self.m_srb
        self.m_srb = None
        self.m_ubuf.destroy()
        del self.m_ubuf
        self.m_ubuf = None
        self.m_vbuf.destroy()
        del self.m_vbuf
        self.m_buf = None

    def initialize(self, cb):
        if self.m_rhi != self.rhi():
            self.m_pipeline = None
            self.m_rhi = self.rhi()

        if not self.m_pipeline:
            vertex_size = 4 * VERTEX_DATA.size
            self.m_vbuf = self.m_rhi.newBuffer(QRhiBuffer.Immutable,
                                               QRhiBuffer.VertexBuffer, vertex_size)
            self.m_vbuf.create()

            self.m_ubuf = self.m_rhi.newBuffer(QRhiBuffer.Dynamic,
                                               QRhiBuffer.UniformBuffer, 64)
            self.m_ubuf.create()

            self.m_srb = self.m_rhi.newShaderResourceBindings()
            bindings = [
                QRhiShaderResourceBinding.uniformBuffer(0, QRhiShaderResourceBinding.VertexStage,
                                                        self.m_ubuf)
            ]
            self.m_srb.setBindings(bindings)
            self.m_srb.create()

            self.m_pipeline = self.m_rhi.newGraphicsPipeline()
            stages = [
                QRhiShaderStage(QRhiShaderStage.Vertex,
                                getShader(":/shader_assets/color.vert.qsb")),
                QRhiShaderStage(QRhiShaderStage.Fragment,
                                getShader(":/shader_assets/color.frag.qsb"))
            ]
            self.m_pipeline.setShaderStages(stages)
            inputLayout = QRhiVertexInputLayout()
            input_bindings = [QRhiVertexInputBinding(5 * 4)]  # sizeof(float)
            inputLayout.setBindings(input_bindings)
            attributes = [  # 4: sizeof(float)
                QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute.Float2, 0),
                QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute.Float3, 2 * 4)
            ]
            inputLayout.setAttributes(attributes)
            self.m_pipeline.setVertexInputLayout(inputLayout)
            self.m_pipeline.setShaderResourceBindings(self.m_srb)
            self.m_pipeline.setRenderPassDescriptor(self.renderTarget().renderPassDescriptor())
            self.m_pipeline.create()

            resourceUpdates = self.m_rhi.nextResourceUpdateBatch()
            resourceUpdates.uploadStaticBuffer(self.m_vbuf, VoidPtr(VERTEX_DATA.tobytes(),
                                                                    vertex_size))
            cb.resourceUpdate(resourceUpdates)

        outputSize = self.renderTarget().pixelSize()
        self.m_viewProjection = self.m_rhi.clipSpaceCorrMatrix()
        r = float(outputSize.width()) / float(outputSize.height())
        self.m_viewProjection.perspective(45.0, r, 0.01, 1000.0)
        self.m_viewProjection.translate(0, 0, -4)

    def render(self, cb):
        resourceUpdates = self.m_rhi.nextResourceUpdateBatch()
        self.m_rotation += 1.0
        modelViewProjection = self.m_viewProjection
        modelViewProjection.rotate(self.m_rotation, 0, 1, 0)
        projection = numpy.array(modelViewProjection.data(),
                                 dtype=numpy.float32)
        resourceUpdates.updateDynamicBuffer(self.m_ubuf, 0, 64,
                                            projection.tobytes())
        clearColor = QColor.fromRgbF(0.4, 0.7, 0.0, 1.0)
        cv = QRhiDepthStencilClearValue(1.0, 0)
        cb.beginPass(self.renderTarget(), clearColor, cv, resourceUpdates)

        cb.setGraphicsPipeline(self.m_pipeline)
        outputSize = self.renderTarget().pixelSize()
        cb.setViewport(QRhiViewport(0, 0, outputSize.width(),
                                    outputSize.height()))
        cb.setShaderResources()
        vbufBinding = (self.m_vbuf, 0)
        cb.setVertexInput(0, [vbufBinding])
        cb.draw(3)
        cb.endPass()

        self.update()
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
   <file>shader_assets/color.vert.qsb</file>
   <file>shader_assets/color.frag.qsb</file>
</qresource>
</RCC>
#version 440

layout(location = 0) in vec3 v_color;

layout(location = 0) out vec4 fragColor;

void main()
{
    fragColor = vec4(v_color, 1.0);
}
#version 440

layout(location = 0) in vec4 position;
layout(location = 1) in vec3 color;

layout(location = 0) out vec3 v_color;

layout(std140, binding = 0) uniform buf {
    mat4 mvp;
};

void main()
{
    v_color = color;
    gl_Position = mvp * position;
}