本页内容

RHI 窗户示例

本示例演示了如何使用QRhi 创建一个基于QWindow 的简易应用程序。

示例截图

从 Qt 6.6 开始,其加速 3D API 和着色器抽象层也向应用程序开放。现在,应用程序可以使用 Qt 自身用于实现Qt Quick 场景图或Qt Quick 3D 引擎的相同 3D 图形类。在早期的 Qt 版本中,QRhi 及其相关类均属于私有 API。 从 6.6 版本开始,这些类与 QPA 类家族属于同一类别:既非完全公开,也非私有,而是介于两者之间,其兼容性承诺相比公开 API 更为有限。另一方面,QRhi 及其相关类现在也像公开 API 一样提供了完整的文档。

使用QRhi 有多种方式,此处的示例展示了最底层的方法:以QWindow 为目标,同时不以任何形式使用Qt Quick 、Qt Quick 3D 或 Widgets,并在应用程序中自行搭建所有渲染和窗口管理基础设施。

相比之下,当使用Qt Quick 或Qt Quick 3D 编写 QML 应用程序,并希望为其添加基于QRhi 的渲染功能时,此类应用程序将依赖于Qt Quick 已初始化的窗口和渲染基础设施,并且很可能会从QQuickWindow 查询现有的QRhi 实例。 在处理QRhi::create()、平台/API 特定功能(如Vulkan instances )或正确处理窗口的expose 和 resize 事件时,这些操作均由Qt Quick 负责管理。而在本例中,所有这些工作均由应用程序自身管理并处理。

注意:特别是对于基于 QWidget的 应用程序,需要注意的是,QWidget::createWindowContainer()允许将QWindow (由原生窗口支持)嵌入到基于小部件的用户界面中。因此,本示例中的HelloWindow 类可在基于QWidget 的应用程序中重复使用,前提是已通过main() 完成了必要的初始化。

3D API 支持

该应用程序支持所有当前的QRhi backends 。如果未指定任何命令行参数,则使用平台特定的默认值:Windows 上为 Direct 3D 11,Linux 上为 OpenGL,macOS/iOS 上为 Metal。

运行时输入 `--help ` 将显示可用的命令行选项:

  • -d 或 –d3d11 用于 Direct 3D 11
  • -D 或 –d3d12 用于 Direct 3D 12
  • -m 或 –metal 用于 Metal
  • -v 或 –vulkan 用于 Vulkan
  • -g 或 –opengl 用于 OpenGL 或 OpenGL ES
  • -n 或 –null 用于Null backend

构建系统说明

本应用程序完全依赖于Qt GUI 模块。它不使用Qt Widgets 或Qt Quick 。

为了访问 RHI API(该 API 虽对所有 Qt 应用程序开放,但兼容性承诺有限),target_link_libraries CMake 命令会列出Qt6::GuiPrivate 。正是这一操作使得#include <rhi/qrhi.h> 的包含语句能够成功编译。

功能

该应用程序具有以下功能:

  • 一个可调整大小的QWindow ,
  • 一个能正确跟随窗口大小的交换链和深度-模板缓冲区,
  • 基于QExposeEvent 和QPlatformSurfaceEvent 等事件,在适当时间进行初始化、渲染和释放的逻辑,
  • 渲染一个带纹理的全屏四边形,该纹理的内容通过`QPainter `在`QImage `中生成(使用光栅绘制引擎,即图像像素数据的生成完全由CPU处理,随后将数据上传至GPU纹理),
  • 渲染一个启用了混合和深度测试的三角形,采用透视投影,同时应用一个每帧都会变化的模型变换,
  • 使用requestUpdate() 实现高效的跨平台渲染循环。

着色器

该应用程序使用了两组顶点着色器和片段着色器对:

  • 一套用于全屏矩形,该组不使用顶点输入,且片段着色器会采样纹理(quad.vert ,quad.frag ),
  • 另一组用于三角形,其中顶点位置和颜色通过顶点缓冲区提供,模型-视图-投影矩阵则通过统一缓冲区提供(color.vert ,color.frag )。

着色器采用与 Vulkan 兼容的 GLSL 源代码编写。

由于这是一个Qt GUI 模块的示例,因此该示例不能依赖于QtShader Tools模块。这意味着无法使用qt_add_shaders()等 CMake 辅助函数。 因此,该示例将预处理后的.qsb 文件包含在shaders/prebuilt 文件夹中,并通过qt_add_resources 直接将其包含在可执行文件中。这种方法通常不建议用于应用程序,建议改用qt_add_shaders(),这样可以避免手动生成和管理.qsb 文件的需要。

为了生成本示例的.qsb 文件,使用了qsb --qt6 color.vert -o prebuilt/color.vert.qsb 等命令。这将导致先编译为SPIR-V,然后转译为GLSL(100 es 和120 )、HLSL(5.0)和MSL(1.2)。随后,所有着色器版本会被打包到QShader 中,并序列化到磁盘上。

API 特定的初始化

对于某些 3D API,main() 函数必须执行相应的 API 特定初始化操作,例如在使用 Vulkan 时创建一个QVulkanInstance 。 对于 OpenGL,我们必须确保深度缓冲区可用,这通过 `QSurfaceFormat` 实现。这些步骤不在QRhi 的范围内,因为 OpenGL 或 Vulkan 的QRhi 后端是基于现有的 Qt 设施(如 `QOpenGLContext ` 或 `QVulkanInstance`)构建的。

   // 对于 OpenGL,需确保窗口具有深度/模板缓冲区。
    // 对于其他 API,这由应用程序控制(如 QRhiRenderBuffer 等) ,
    // 因此无需进行特殊设置。
    QSurfaceFormat fmt;
    fmt.setDepthBufferSize(24);
    fmt.setStencilBufferSize(8);
   // macOS 的特例,用于允许在该系统上使用 OpenGL。
    // (不过,推荐使用默认的 Metal 方案)
    // gl_VertexID 是 GLSL 130 版本的特性,因此  macOS 上 默认 提供的  OpenGL 2.1 渲染上下文 
    // 无法满足此需求。
#ifdef Q_OS_MACOS
    fmt.setVersion(4, 1);
    fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
    QSurfaceFormat::setDefaultFormat(fmt);

    // 针对 Vulkan。
#if QT_CONFIG(vulkan)
    QVulkanInstance inst;
    if(graphicsApi==QRhi::Vulkan) {
        // 若可用,请求验证。此操作完全可选 
        // 且会影响性能,生产环境中应避免使用。
        inst.setLayers({"VK_LAYER_KHRONOS_validation"});
        // 与 QRhi 良好兼容。
        inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
        if(!inst.create()) {
            qWarning("Failed to create Vulkan instance, switching to OpenGL");
            graphicsApi=QRhi::OpenGLES2;
        }
    }
#endif

注意:对于 Vulkan,请注意如何通过QRhiVulkanInitParams::preferredInstanceExtensions() 来确保启用相应的扩展。

HelloWindow 是RhiWindow 的子类,而 又是QWindow 的子类。RhiWindow 包含管理带交换链(以及深度-模板缓冲区)的可调整大小窗口所需的一切内容,并且在其他应用程序中也可能被重复使用。HelloWindow 包含此特定示例应用程序专有的渲染逻辑。

在QWindow 子类的构造函数中,会根据所选的3D API来设置表面类型。

RhiWindow::RhiWindow(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;
    case QRhi::Null:
        break; // RasterSurface
    }
}

QRhi 对象的创建和初始化在 RhiWindow::init() 中实现。请注意,该函数仅在窗口处于renderable 状态时被调用,这由expose event 指示。

根据所使用的3D API不同,需要将相应的InitParams结构体传递给QRhi::create()。以OpenGL为例,应用程序必须创建一个QOffscreenSurface (或其他QSurface ),并将其提供给QRhi 使用。对于Vulkan,则需要一个已成功初始化的QVulkanInstance 。 而其他技术,如 Direct 3D 或 Metal,则无需额外信息即可完成初始化。

voidRhiWindow::init()
{
    if(m_graphicsApi==QRhi::Null) {
        QRhiNullInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Null, &params));
    }

#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));
    }
#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));
    }
#endif

#ifdef Q_OS_WIN
    if(m_graphicsApi==QRhi::D3D11) {
        QRhiD3D11InitParams params;
        // 若可用,启用调试层。此操作为可选
        // 且应避免在生产构建中使用。
        params.enableDebugLayer= true;
        m_rhi.reset(QRhi::create(QRhi::D3D11, &params));
    }else if(m_graphicsApi==QRhi::D3D12) {
        QRhiD3D12InitParams params;
        // 若可用,则启用调试层。此操作为可选
        // 且应避免在正式发布版本中使用。
        params.enableDebugLayer= true;
        m_rhi.reset(QRhi::create(QRhi::D3D12, &params));
    }
#endif

#if QT_CONFIG(metal)
    if(m_graphicsApi==QRhi::Metal) {
        QRhiMetalInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Metal, &params));
    }
#endif

    if(!m_rhi)
        qFatal("Failed to create RHI backend");

除此之外,其余部分——即所有渲染代码——均完全支持跨平台,且不包含针对任何3D API的特定分支或条件。

事件暴露

renderable 的具体含义因平台而异。例如,在 macOS 上,完全被遮挡的窗口(完全位于其他窗口后面)无法进行渲染,而在 Windows 上,遮挡则没有实际意义。幸运的是,应用程序无需了解这些细节:Qt 的平台插件会通过 expose 事件抽象掉这些差异。 不过,exposeEvent() 的重写版本还需注意,空的输出尺寸(例如宽度和高度均为 0)也应被视为不可渲染的情况。例如在 Windows 上,最小化窗口时就会发生这种情况。因此需要通过QRhiSwapChain::surfacePixelSize() 进行检查。

此处对 expose 事件处理的实现力求稳健、安全且可移植。Qt Quick 本身在其渲染循环中也实现了非常相似的逻辑。

void RhiWindow::exposeEvent(QExposeEvent *)
{
    // initialize and start rendering when the window becomes usable for graphics purposes
    if (isExposed() && !m_initialized) {
        init();
        resizeSwapChain();
        m_initialized = true;
    }

    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_initialized && !m_notExposed)
        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_initialized && m_notExposed && !surfaceSize.isEmpty()) {
        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();
}

在 RhiWindow::render() 中(该函数由requestUpdate() 生成的UpdateRequest 事件调用),已设置以下检查机制,以防止在交换链初始化失败或窗口进入不可渲染状态时尝试渲染。

void RhiWindow::render()
{
    if (!m_hasSwapChain || m_notExposed)
        return;

交换链、深度-模板缓冲区和窗口调整大小

要渲染到QWindow 上,需要一个QRhiSwapChain 。此外,还会创建一个用作深度-模板缓冲区的QRhiRenderBuffer ,因为该应用程序演示了如何在图形管道中启用深度测试。在某些传统3D API中(如EGL、WGL、 GLX 等,这意味着深度/模板缓冲区会与window surface 一起被隐式管理),而在现代API中,管理基于窗口的渲染目标的深度-模板缓冲区与管理离屏渲染目标并无二致。QRhi 对此进行了抽象处理,但为了获得最佳性能,仍需明确指定QRhiRenderBuffer 为used with together with a QRhiSwapChain 。

QRhiSwapChain 与QWindow 以及深度/模板缓冲区相关联。

    std::unique_ptr<QRhiSwapChain> m_sc;
    std::unique_ptr<QRhiRenderBuffer> m_ds;
    std::unique_ptr<QRhiRenderPassDescriptor> m_rp;

    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());

当窗口大小发生变化时,交换链也需要相应调整大小。这在 resizeSwapChain() 中实现了。

void RhiWindow::resizeSwapChain()
{
    m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds

    const QSize outputSize = m_sc->currentPixelSize();
    m_viewProjection = m_rhi->clipSpaceCorrMatrix();
    m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
    m_viewProjection.translate(0, 0, -4);
}

与其他QRhiResource 子类不同,QRhiSwapChain 在create-function方面的语义略有不同。正如其名createOrResize()所示,每当得知输出窗口大小可能与swapchain上次初始化时的不一致时,就需要调用该函数。 与深度-模板相关的QRhiRenderBuffer 会自动设置其size ,并且其create()方法会由swapchain的createOrResize()方法隐式调用。

这也是(重新)计算投影和视图矩阵的合适时机,因为我们设置的透视投影取决于输出宽高比。

注意:为 消除坐标系差异,会从QRhi 查询a backend/API-specific "correction" matrix ,并将其烘焙到投影矩阵中。这使得应用程序能够处理OpenGL风格的顶点数据,前提是坐标系原点位于左下角。

当发现当前报告的大小与交换链上次初始化时的大小不再一致时,RhiWindow::render() 会调用 resizeSwapChain() 函数。

更多详细信息请参阅QRhiSwapChain::currentPixelSize() 和QRhiSwapChain::surfacePixelSize()。

内置了对高DPI的支持:正如名称所示,尺寸始终以像素为单位,并会考虑窗口特有的scale factor 。在QRhi (以及3D API)层面上,不存在高DPI缩放的概念,所有内容始终以像素为单位。 这意味着,当QWindow 的size() 为1280x720且devicePixelRatio()为2时,其作为渲染目标(swapchain)的(像素)尺寸即为2560x1440。

    // If the window got resized or newly exposed, resize the swapchain. (the
    // newly-exposed case is not actually required by some platforms, but is
    // here for robustness and portability)
    //
    // 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;
    }

渲染循环

应用程序会持续进行渲染,其频率受呈现速率(vsync)限制。当当前录制的帧已提交后,通过在 RhiWindow::render() 中调用requestUpdate() 来确保这一点。

    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.
    requestUpdate();
}

这最终会触发一个UpdateRequest 事件。该事件在event()的重新实现中进行处理。

bool RhiWindow::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;

    default:
        break;
    }

    return QWindow::event(e);
}

资源和管道设置

该应用程序记录了一个渲染过程,该过程会发出两个绘制调用,并使用两条不同的图形管道。其中一条是“背景”管道,其纹理包含由QPainter 生成的图像;随后,在该图像上方渲染一个三角形,并启用混合效果。

该三角形所使用的顶点缓冲区和统一缓冲区创建方式如下。由于着色器在统一块中指定了mat4 和float 成员,因此统一缓冲区的大小为68字节。 请注意std140 的布局规则。在本例中这不会造成问题,因为紧跟在mat4 之后的float 成员具有正确的对齐方式且无需额外填充,但在其他应用中(尤其是处理vec2 或vec3 等类型时)这可能会变得重要。 如有疑问,建议查阅QShaderDescription 中的QShader 部分;或者,更便捷的做法是,使用qsb 工具对.qsb 文件运行-d 参数,以可读格式查看元数据。生成的信息包括(但不限于)各统一块成员的偏移量、大小以及每个统一块的总大小(以字节为单位)。

void HelloWindow::customInit()
{
    m_initialUpdates = m_rhi->nextResourceUpdateBatch();

    m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
    m_vbuf->create();
    m_initialUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);

    static const quint32 UBUF_SIZE = 68;
    m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
    m_ubuf->create();

顶点着色器和片段着色器都需要在绑定点 0 处有一个统一缓冲区。这由QRhiShaderResourceBindings 对象来保证。随后,图形管道将根据着色器以及一些附加信息进行配置。 该示例还依赖于一些便捷的默认设置,例如基元拓扑为“Triangles ”,但由于这是默认值,因此未进行显式设置。更多详细信息请参阅QRhiGraphicsPipeline 。

除了指定拓扑和各种状态外,管道还必须与以下内容相关联:

   m_colorTriSrb.reset(m_rhi->newShaderResourceBindings());
   static const QRhiShaderResourceBinding::StageFlags visibility =
           QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
   m_colorTriSrb->setBindings({
           QRhiShaderResourceBinding::uniformBuffer(0, visibility, m_ubuf.get())
   });
   m_colorTriSrb->create();

   m_colorPipeline.reset(m_rhi->newGraphicsPipeline());
   // Enable depth testing; not quite needed for a simple triangle, but we
   // have a depth-stencil buffer so why not.
   m_colorPipeline->setDepthTest(true);
   m_colorPipeline->setDepthWrite(true);
   // Blend factors default to One, OneOneMinusSrcAlpha, which is convenient.
   QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
   premulAlphaBlend.enable = true;
   m_colorPipeline->setTargetBlends({ premulAlphaBlend });
   m_colorPipeline->setShaderStages({
       { QRhiShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) },
       { QRhiShaderStage::Fragment, getShader(QLatin1String(":/color.frag.qsb")) }
   });
   QRhiVertexInputLayout inputLayout;
   inputLayout.setBindings({
       { 5 * sizeof(float) }
   });
   inputLayout.setAttributes({
       { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
       { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
   });
   m_colorPipeline->setVertexInputLayout(inputLayout);
   m_colorPipeline->setShaderResourceBindings(m_colorTriSrb.get());
   m_colorPipeline->setRenderPassDescriptor(m_rp.get());
   m_colorPipeline->create();

getShader() 是一个辅助函数,用于加载.qsb 文件并从中反序列化出QShader 对象。

static QShader getShader(const QString &name)
{
    QFile f(name);
    if (f.open(QIODevice::ReadOnly))
        return QShader::fromSerialized(f.readAll());

    return QShader();
}

color.vert 着色器将以下内容指定为顶点输入:

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

然而,C++ 代码提供的顶点数据采用 2 个浮点数表示位置,并交错排列 3 个浮点数表示颜色。(每个顶点的坐标为:x 、y 、r 、g 、b )这就是为什么步长为5 * sizeof(float) ,且位置 0 和 1 的输入分别指定为Float2 和Float3 的原因。 这种写法是有效的,vec4 位置的z 和w 将自动被设置。

渲染

通过调用QRhi::beginFrame() 开始录制一帧,通过调用QRhi::endFrame() 结束录制。

    QRhi::FrameOpResult result= m_rhi->beginFrame(m_sc.get());
    if(result==QRhi::FrameOpSwapChainOutOfDate) {
        resizeSwapChain();
        if(!m_hasSwapChain)
            return;
        result= m_rhi->beginFrame(m_sc.get());
    }
    if(result!=QRhi::FrameOpSuccess) {
        qWarning("beginFrame failed with %d, will retry", result);
        requestUpdate();
        return;
    }

    customRender();

应用程序中的一些资源(缓冲区、纹理)包含静态数据,这意味着其内容永远不会改变。例如,顶点缓冲区的内容是在初始化阶段提供的,此后不会发生改变。这些数据更新操作会被记录在 `m_initialUpdates` 中。如果尚未完成,该资源更新批处理中的命令将被合并到每帧批处理中。

void HelloWindow::customRender()
{
    QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();

    if (m_initialUpdates) {
        resourceUpdates->merge(m_initialUpdates);
        m_initialUpdates->release();
        m_initialUpdates = nullptr;
    }

必须存在每帧资源更新批处理,因为包含模型视图投影矩阵和不透明度的统一缓冲区内容会在每一帧发生变化。

    m_rotation += 1.0f;
    QMatrix4x4 modelViewProjection = m_viewProjection;
    modelViewProjection.rotate(m_rotation, 0, 1, 0);
    resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());
    m_opacity += m_opacityDir * 0.005f;
    if (m_opacity < 0.0f || m_opacity > 1.0f) {
        m_opacityDir *= -1;
        m_opacity = qBound(0.0f, m_opacity, 1.0f);
    }
    resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity);

要开始录制渲染通道,需查询QRhiCommandBuffer 并确定输出尺寸,这将有助于设置视口,并在需要时调整全屏纹理的大小。

    QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
    const QSize outputSizeInPixels = m_sc->currentPixelSize();

开始渲染通道意味着要清空渲染目标的颜色和深度-模板缓冲区(除非渲染目标标志另有说明,但这仅适用于基于纹理的渲染目标)。 在此,我们将颜色设为黑色,深度设为 1.0f,模板设为 0(未使用)。最后一个参数resourceUpdates 用于确保记录在批处理中的数据更新命令被提交。此外,我们也可以使用QRhiCommandBuffer::resourceUpdate() 代替。该渲染通道的目标是一个交换链,因此需要调用currentFrameRenderTarget() 来获取一个有效的QRhiRenderTarget 。

    cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);

记录三角形的绘制调用非常简单:设置渲染管线、设置着色器资源、设置顶点/索引缓冲区,然后记录绘制调用。这里我们使用一个仅包含 3 个顶点的非索引绘制调用。

    cb->setGraphicsPipeline(m_colorPipeline.get());
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
    cb->setVertexInput(0, 1, &vbufBinding);
    cb->draw(3);

    cb->endPass();

setShaderResources() 调用未提供任何参数,这意味着将使用m_colorTriSrb ,因为该资源与当前活动的QRhiGraphicsPipeline (m_colorPipeline )相关联。

我们不会深入探讨全屏背景图像渲染的细节。相关内容请参阅示例源代码。不过,值得注意的是“调整”纹理或缓冲区资源大小的常见模式。 并不存在改变现有原生资源大小的操作,因此更改纹理或缓冲区大小后,必须调用 create() 方法,以释放并重新创建底层的原生资源。为了确保QRhiTexture 始终具有所需的大小,应用程序实现了以下逻辑。 请注意,m_texture 在窗口的整个生命周期内都保持有效,这意味着对其的对象引用(例如在QRhiShaderResourceBindings 中)始终有效。只有底层的原生资源会随着时间推移而动态生成和释放。

另请注意,我们为图像设置了与绘制目标窗口相匹配的设备像素比(DPR)。这确保了绘制代码能够与 DPR 无关,无论 DPR 如何都能生成相同的布局,同时还能利用额外的像素来提高保真度。

void HelloWindow::ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u)
{
    if (m_texture && m_texture->pixelSize() == pixelSize)
        return;

    if (!m_texture)
        m_texture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, pixelSize));
    else
        m_texture->setPixelSize(pixelSize);

    m_texture->create();

    QImage image(pixelSize, QImage::Format_RGBA8888_Premultiplied);
    image.setDevicePixelRatio(devicePixelRatio());

一旦生成QImage ,且基于QPainter 的绘制操作完成后,我们会使用uploadTexture()在资源更新批处理中记录一次纹理上传:

    u->uploadTexture(m_texture.get(), image);

示例项目 @ code.qt.io

另请参阅 QRhi 、QRhiSwapChain 、QWindow 、QRhiCommandBuffer 、QRhiResourceUpdateBatch 、QRhiBuffer 以及QRhiTexture 。

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.