RHI 窗口示例

本例说明如何使用QRhi 创建基于QWindow 的最小应用程序。

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

使用QRhi 有多种方法,这里的示例展示了最底层的方法:以QWindow 为目标,同时不以任何形式使用Qt QuickQt Quick 3D 或 Widgets,并在应用程序中设置所有渲染和窗口基础架构。

相比之下,当使用Qt QuickQt Quick 3D 编写 QML 应用程序,并希望在其中添加基于QRhi 的渲染时,这样的应用程序将依赖于Qt Quick 已经初始化的窗口和渲染基础架构,而且很可能会从QQuickWindow 中查询现有的QRhi 实例。在这里,处理QRhi::create()、平台/API 具体细节(如Vulkan instances )或正确处理expose 和窗口的大小调整事件都由Qt Quick 管理。而在本例中,所有这些都由应用程序自己管理和处理。

注: 特别是对于基于QWidget 的应用程序,应注意QWidget::createWindowContainer() 允许在基于 widget 的用户界面中嵌入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,表示金属
  • -v或-vulkan,用于 Vulkan
  • -g 或 -opengl 表示 OpenGL 或 OpenGL ES
  • -n 或 -null 表示Null backend

构建系统注释

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

为了访问 RHI API(所有 Qt 应用程序均可使用,但兼容承诺有限),target_link_libraries CMake 命令列出了Qt6::GuiPrivate 。这就是#include <rhi/qrhi.h> include 语句能成功编译的原因。

特性

应用程序具有以下功能

  • 可调整大小的QWindow
  • 交换链和深度模板缓冲区,可根据窗口大小进行适当调整、
  • 根据QExposeEventQPlatformSurfaceEvent 等事件在适当的时间进行初始化、渲染和拆卸的逻辑、
  • 渲染一个全屏纹理四边形,使用的纹理内容是通过QPainterQImage 中生成的(使用光栅绘画引擎,即图像像素数据的生成全部基于 CPU,然后将数据上载到 GPU 纹理中)、
  • 使用透视投影渲染一个启用了混合和深度测试的三角形,同时应用每帧都会改变的模型变换、
  • 使用requestUpdate() 实现高效的跨平台渲染循环。

着色器

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

  • 一对用于全屏四边形,不使用顶点输入,片段着色器对纹理进行采样 (quad.vert,quad.frag)。另一对用于三角形,顶点位置和颜色由顶点缓冲器提供,模型视图-投影矩阵由模型视图-投影矩阵提供、
  • 另一对用于三角形,顶点位置和颜色在顶点缓冲区中提供,模型视图投影矩阵在统一缓冲区中提供 (color.vert,color.frag) 。

着色器是以兼容 Vulkan 的 GLSL 源代码编写的。

由于是Qt GUI 模块示例,本示例不能依赖QtXML Shader Tools 模块。这意味着无法使用 qt_add_shaders 等 CMake 辅助函数。因此,该示例在shaders/prebuilt 文件夹中包含了预处理后的.qsb 文件,这些文件通过qt_add_resources 简单地包含在可执行文件中。一般不建议应用程序采用这种方法,而应考虑使用qt_add_shaders,这样就无需手动生成和管理.qsb 文件。

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

特定于 API 的初始化

对于某些 3D API,main() 函数必须执行适当的 API 特定初始化,例如在使用 Vulkan 时创建QVulkanInstance 。对于 OpenGL,我们必须确保深度缓冲区可用,这可通过QSurfaceFormat 完成。这些步骤不在QRhi 的范围内,因为用于 OpenGL 或 Vulkan 的QRhi 后端是建立在现有 Qt 设施(如QOpenGLContextQVulkanInstance )之上的。

  // 对于 其他 API(QRhiRenderBuffer 等),这将由应用程序控制, 因此无需进行特殊设置。  QSurfaceFormatfmt; fmt.setDepthBufferSize(24); fmt.setStencilBufferSize(8);// 特殊情况下,macOS 允许使用 OpenGL。 // (默认的 Metal 是推荐的方法) // gl_VertexID 是 GLSL 130 功能,因此 在 macOS 上 默认的 OpenGL 2.1 上下文 // 是 不够的。#ifdef Q_OS_MACOSfmt.setVersion(4, 1); fmt.setProfile(QSurfaceFormat::CoreProfile);#endif  QSurfaceFormat::setDefaultFormat(fmt);// For Vulkan .  QVulkanInstanceinst;if(graphicsApi==QRhi::Vulkan) {// 如果可用,请求验证。inst.setLayers({"VK_LAYER_KHRONOS_validation"});// Play nice with QRhi.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,需要向QRhi::create() 传递相应的 InitParams 结构。例如,对于 OpenGL,应用程序必须创建一个QOffscreenSurface (或其他QSurface ),并提供给QRhi 使用。对于 Vulkan,则需要一个成功初始化的QVulkanInstance 。其他如 Direct 3D 或 Metal 则无需额外信息即可初始化。

voidRhiWindow::init() {如果m_graphicsApi==QRhi::Null) { QRhiNullInitParamsparams; m_rhi.reset(QRhi::create(QRhi::Null, &params)); }#if QT_CONFIG(opengl) if(m_graphicsApi==::OpenGLES2) 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) { QRhiVulkanInitParamsparams; params.inst=vulkanInstance(); params.window= this; m_rhi.reset(QRhi::create(QRhi::Vulkan, &params)); }#endif#ifdef Q_OS_WIN if(m_graphicsApi== ::D3D11) { Q_rhi.reset( 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== ::Metal) { m_rhi.reset(QRhi::Metal) { QRhiMetalInitParamsparams; m_rhi.reset(QRhi::create(QRhi::Metal, &params)); }#endif if(!m_rhi)        qFatal("Failed to create RHI backend");

除此以外,其他所有内容,包括所有渲染代码,都是完全跨平台的,没有针对任何 3D API 的分支或条件。

公开事件

renderable 的具体含义因平台而异。例如,在 macOS 上,完全遮挡(完全位于其他窗口后面)的窗口是不可渲染的,而在 Windows 上,遮挡没有任何意义。幸运的是,应用程序不需要这方面的特殊知识:Qt 的平台插件抽象了暴露事件背后的差异。不过,exposeEvent() 的重新实现还需要注意,输出大小为空(例如宽度和高度均为 0)也应视为不可渲染的情况。例如,在 Windows 系统中,最小化窗口时就会出现这种情况。因此,需要基于QRhiSwapChain::surfacePixelSize() 进行检查。

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

在响应requestUpdate() 生成的UpdateRequest 事件时调用的 RhiWindow::render() 中,进行了以下检查,以防止在交换链初始化失败或窗口不可渲染时尝试渲染。

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

交换链、深度铅笔缓冲区和调整大小

要渲染到QWindow ,需要一个QRhiSwapChain 。此外,由于该应用演示了如何在图形流水线中启用深度测试,因此还创建了一个作为深度模板缓冲区的QRhiRenderBuffer 。在某些传统 3D API 中,管理窗口的深度/模版缓冲区是相应窗口系统接口 API 的一部分(EGL、WGL、GLX 等,这意味着深度/模版缓冲区与window surface 是隐式管理的),而在现代 API 中,管理基于窗口的渲染目标的深度/模版缓冲区与管理离屏渲染目标并无不同。QRhi 对此进行了抽象,但为了获得最佳性能,仍需说明QRhiRenderBufferused with together with a QRhiSwapChain

QRhiSwapChainQWindow 和深度/模版缓冲区相关联。

    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 的创建函数的语义略有不同。正如其名称createOrResize() 所示,只要知道输出窗口大小可能与交换链上次初始化时的大小不一致,就需要调用该函数。深度钢网的相关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 事件。这将在事件()的重新实现中进行处理。

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 生成的图像,然后在上面渲染一个三角形,并启用混合功能。

与三角形一起使用的顶点和均匀缓冲区是这样创建的。均匀缓冲区的大小为 68 字节,因为着色器在均匀块中指定了一个mat4 和一个float 成员。注意std140 布局规则。这在本例中并不奇怪,因为float 成员跟在mat4 后面,对齐方式正确,没有任何额外的填充,但在其他应用中,尤其是在处理vec2vec3 等类型时,可能会与此有关。如果有疑问,可以考虑检查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 的输入分别指定为Float2Float3 。这是有效的,vec4 位置的zw 将自动设置。

渲染

调用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!= = m_rhi->beginFrame(m_sc.get()); }if(result!= = m_rhi->beginFrame(m_sc.get()). 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 如何,都能生成相同的布局,同时还能利用额外的像素来提高逼真度。

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,QRhiBufferQRhiTexture

© 2025 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.