Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "rhi_support_p.h"
#include <QtMultimedia/private/qvideotexturehelper_p.h>
#include <QtMultimedia/qvideoframeformat.h>
#include <QtCore/qfile.h>
QT_BEGIN_NAMESPACE
namespace QtMultimediaTest {
using namespace Qt::Literals;
std::unique_ptr<QRhi> createNullRhi()
{
QRhiNullInitParams params;
return std::unique_ptr<QRhi>{ QRhi::create(QRhi::Null, ¶ms) };
}
#if QT_CONFIG(opengl)
q23::expected<OffscreenGlRhi, QString> createOffscreenGlRhi()
{
std::unique_ptr<QOffscreenSurface> fallbackSurface{
QRhiGles2InitParams::newFallbackSurface(),
};
QRhiGles2InitParams glParams;
glParams.format = QSurfaceFormat::defaultFormat();
glParams.fallbackSurface = fallbackSurface.get();
std::unique_ptr<QRhi> rhi{
QRhi::create(QRhi::OpenGLES2, &glParams),
};
if (!rhi)
return q23::unexpected(u"QRhi::create failed"_s);
return OffscreenGlRhi{
std::move(fallbackSurface),
std::move(rhi),
};
}
std::unique_ptr<QRhi> createGles2Rhi(QOffscreenSurface *fallbackSurface)
{
QRhiGles2InitParams params;
params.fallbackSurface = fallbackSurface;
return std::unique_ptr<QRhi>{ QRhi::create(QRhi::OpenGLES2, ¶ms) };
}
#endif
#if QT_CONFIG(vulkan)
q23::expected<OffscreenVulkanRhi, QString>
createOffscreenVulkanRhi(const QByteArrayList &deviceExtensions)
{
auto vulkanInstance = std::make_unique<QVulkanInstance>();
vulkanInstance->setApiVersion(QVersionNumber(1, 1));
vulkanInstance->setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
if (!vulkanInstance->create())
return q23::unexpected(u"QVulkanInstance::create failed"_s);
QRhiVulkanInitParams params;
params.inst = vulkanInstance.get();
params.deviceExtensions = deviceExtensions;
std::unique_ptr<QRhi> rhi{
QRhi::create(QRhi::Vulkan, ¶ms),
};
if (!rhi) {
return q23::unexpected(u"QRhi::create failed"_s);
}
return OffscreenVulkanRhi{
std::move(vulkanInstance),
std::move(rhi),
};
}
#endif
#if QT_CONFIG(metal)
std::unique_ptr<QRhi> createOffscreenMetalRhi()
{
QRhiMetalInitParams metalParams;
return std::unique_ptr<QRhi>(QRhi::create(QRhi::Metal, &metalParams));
}
#endif
#if defined(Q_OS_WIN)
std::unique_ptr<QRhi> createOffscreenD3D11Rhi()
{
QRhiD3D11InitParams params;
return std::unique_ptr<QRhi>(QRhi::create(QRhi::D3D11, ¶ms));
}
std::unique_ptr<QRhi> createWarpD3D11Rhi(const QWindowsD3D11TestDeviceContext &warpDevice)
{
QRhiD3D11NativeHandles nativeHandles;
nativeHandles.dev = warpDevice.device.Get();
nativeHandles.context = warpDevice.context.Get();
QRhiD3D11InitParams params;
return std::unique_ptr<QRhi>{ QRhi::create(QRhi::D3D11, ¶ms, {}, &nativeHandles) };
}
std::unique_ptr<QRhi> createD3D12Rhi()
{
QRhiD3D12InitParams params;
return std::unique_ptr<QRhi>{ QRhi::create(QRhi::D3D12, ¶ms) };
}
#endif
q23::expected<QByteArray, QString> readBackPlane(QRhi &rhi, quint64 handle, QSize planeSize,
QRhiTexture::Format format)
{
std::unique_ptr<QRhiTexture> texture(
rhi.newTexture(format, planeSize, 1, QRhiTexture::UsedAsTransferSource));
if (!texture->createFrom(QRhiTexture::NativeTexture{ handle, 0 }))
return q23::unexpected(u"QRhiTexture::createFrom failed"_s);
QRhiResourceUpdateBatch *batch = rhi.nextResourceUpdateBatch();
QRhiReadbackResult result;
batch->readBackTexture(texture.get(), &result);
QRhiCommandBuffer *cb = nullptr;
if (rhi.beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess || !cb)
return q23::unexpected(u"QRhi::beginOffscreenFrame failed"_s);
cb->resourceUpdate(batch);
rhi.endOffscreenFrame();
return result.data;
}
#if QT_CONFIG(opengl)
namespace {
q23::expected<QShader, QString> loadShader(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return q23::unexpected(u"Could not open shader file: "_s + path);
QShader shader = QShader::fromSerialized(file.readAll());
if (!shader.isValid())
return q23::unexpected(u"Invalid/unsupported shader file: "_s + path);
return shader;
}
} // namespace
// Unlike readBackPlane(), EXTERNAL_OES textures can't be read back directly: they're only
// consumable by a shader (samplerExternalOES). Renders the texture through a full-screen quad
// using this module's own externalsampler shader into a plain RGBA8 target, then reads that back.
q23::expected<QByteArray, QString> readBackExternalOesTexture(QRhi &rhi, quint64 handle, QSize size)
{
std::unique_ptr<QRhiTexture> sourceTexture{
rhi.newTexture(QRhiTexture::RGBA8, size, 1, QRhiTexture::ExternalOES),
};
if (!sourceTexture->createFrom(QRhiTexture::NativeTexture{ handle, 0 }))
return q23::unexpected(u"Failed to wrap the native handle as an ExternalOES QRhiTexture"_s);
std::unique_ptr<QRhiTexture> targetTexture{
rhi.newTexture(QRhiTexture::RGBA8, size, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource),
};
if (!targetTexture->create())
return q23::unexpected(u"Failed to create the readback render target texture"_s);
QRhiColorAttachment colorAttachment(targetTexture.get());
std::unique_ptr<QRhiTextureRenderTarget> renderTarget{
rhi.newTextureRenderTarget(QRhiTextureRenderTargetDescription(colorAttachment)),
};
std::unique_ptr<QRhiRenderPassDescriptor> renderPassDescriptor{
renderTarget->newCompatibleRenderPassDescriptor(),
};
renderTarget->setRenderPassDescriptor(renderPassDescriptor.get());
if (!renderTarget->create())
return q23::unexpected(u"Failed to create the readback render target"_s);
const QVideoFrameFormat format(size, QVideoFrameFormat::Format_SamplerExternalOES);
auto vertexShader = loadShader(QVideoTextureHelper::vertexShaderFileName(format));
if (!vertexShader)
return q23::unexpected(vertexShader.error());
auto fragmentShader = loadShader(QVideoTextureHelper::fragmentShaderFileName(format, &rhi));
if (!fragmentShader)
return q23::unexpected(fragmentShader.error());
std::unique_ptr<QRhiSampler> sampler{
rhi.newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge),
};
if (!sampler->create())
return q23::unexpected(u"Failed to create the sampler"_s);
std::unique_ptr<QRhiBuffer> uniformBuffer{
rhi.newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(QVideoTextureHelper::UniformData)),
};
if (!uniformBuffer->create())
return q23::unexpected(u"Failed to create the uniform buffer"_s);
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi.newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(
0,
QRhiShaderResourceBinding::VertexStage
| QRhiShaderResourceBinding::FragmentStage,
uniformBuffer.get()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage,
sourceTexture.get(), sampler.get()),
});
if (!srb->create())
return q23::unexpected(u"Failed to create the shader resource bindings"_s);
// A single full-screen quad (vertexPosition: vec4, vertexTexCoord: vec2, interleaved),
// matching the vertex layout every one of this module's video shaders expects.
static constexpr float vertexData[] = {
// x, y, z, w, u, v
-1.f, -1.f, 0.f, 1.f, 0.f, 0.f, //
1.f, -1.f, 0.f, 1.f, 1.f, 0.f, //
-1.f, 1.f, 0.f, 1.f, 0.f, 1.f, //
1.f, 1.f, 0.f, 1.f, 1.f, 1.f,
};
std::unique_ptr<QRhiBuffer> vertexBuffer{
rhi.newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)),
};
if (!vertexBuffer->create())
return q23::unexpected(u"Failed to create the vertex buffer"_s);
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 6 * sizeof(float) } });
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float4, 0 },
{ 0, 1, QRhiVertexInputAttribute::Float2, 4 * sizeof(float) },
});
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi.newGraphicsPipeline());
pipeline->setShaderStages({
{ QRhiShaderStage::Vertex, *vertexShader },
{ QRhiShaderStage::Fragment, *fragmentShader },
});
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.get());
pipeline->setRenderPassDescriptor(renderPassDescriptor.get());
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
if (!pipeline->create())
return q23::unexpected(u"Failed to create the graphics pipeline"_s);
QRhiCommandBuffer *cb = nullptr;
if (rhi.beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess || !cb)
return q23::unexpected(u"QRhi::beginOffscreenFrame failed"_s);
QRhiResourceUpdateBatch *batch = rhi.nextResourceUpdateBatch();
batch->uploadStaticBuffer(vertexBuffer.get(), vertexData);
QVideoTextureHelper::UniformData uniformData{};
const QMatrix4x4 identity;
memcpy(uniformData.transformMatrix, identity.constData(), sizeof(uniformData.transformMatrix));
memcpy(uniformData.colorMatrix, identity.constData(), sizeof(uniformData.colorMatrix));
uniformData.opacity = 1.f;
batch->updateDynamicBuffer(uniformBuffer.get(), 0, sizeof(uniformData), &uniformData);
cb->beginPass(renderTarget.get(), Qt::black, { 1.0f, 0 }, batch);
cb->setGraphicsPipeline(pipeline.get());
cb->setViewport({ 0, 0, float(size.width()), float(size.height()) });
cb->setShaderResources(srb.get());
const QRhiCommandBuffer::VertexInput vertexBinding(vertexBuffer.get(), 0);
cb->setVertexInput(0, 1, &vertexBinding);
cb->draw(4);
cb->endPass();
QRhiReadbackResult result;
QRhiResourceUpdateBatch *readbackBatch = rhi.nextResourceUpdateBatch();
readbackBatch->readBackTexture(targetTexture.get(), &result);
cb->resourceUpdate(readbackBatch);
rhi.endOffscreenFrame();
return result.data;
}
#endif // QT_CONFIG(opengl)
} // namespace QtMultimediaTest
QT_END_NAMESPACE