rhi: Add support for half precision vertex atttributes

Runtime support is indicated via QRhi::Feature::HalfAttributes.

OpenGL support is available in OpenGL 3.0+, OpenGL ES 3.0+, and in
implementations that support the extension GL_ARB_half_float_vertex.

Other RHI backends (Vulkan, Metal, D3D11, and D3D12) all support this
feature.

Note that D3D does not support the half3 type.  D3D backends pass half3
as half4.

tst_qrhi auto unit test included.

Change-Id: Ide05d7f62f6102ad5cae1b3681fdda98d52bca31
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io>
This commit is contained in:
Ben Fletcher 2023-02-20 21:25:03 -08:00
parent 742e79312f
commit 9ffa16baf0
14 changed files with 269 additions and 5 deletions

View File

@ -58,7 +58,8 @@ public:
TextureSwizzle = 0x01000000,
StandardDerivatives = 0x02000000,
ASTCTextureCompression = 0x04000000,
ETC2TextureCompression = 0x08000000
ETC2TextureCompression = 0x08000000,
HalfFloatVertex = 0x10000000
};
Q_DECLARE_FLAGS(OpenGLExtensions, OpenGLExtension)

View File

@ -346,6 +346,8 @@ static int qt_gl_resolve_extensions()
extensions |= QOpenGLExtensions::TextureSwizzle;
if (extensionMatcher.match("GL_OES_standard_derivatives"))
extensions |= QOpenGLExtensions::StandardDerivatives;
if (extensionMatcher.match("GL_ARB_half_float_vertex"))
extensions |= QOpenGLExtensions::HalfFloatVertex;
if (ctx->isOpenGLES()) {
if (format.majorVersion() >= 2)
@ -360,7 +362,8 @@ static int qt_gl_resolve_extensions()
| QOpenGLExtensions::FramebufferMultisample
| QOpenGLExtensions::Sized8Formats
| QOpenGLExtensions::StandardDerivatives
| QOpenGLExtensions::ETC2TextureCompression;
| QOpenGLExtensions::ETC2TextureCompression
| QOpenGLExtensions::HalfFloatVertex;
#ifndef Q_OS_WASM
// WebGL 2.0 specification explicitly does not support texture swizzles
// https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.19

View File

@ -740,6 +740,16 @@ Q_LOGGING_CATEGORY(QRHI_LOG_INFO, "qt.rhi.general")
unsupported on backends that do not report support for
\l{OneDimensionalTextures}, and Metal.
\value HalfAttributes Indicates that specifying input attributes with half
precision (16bit) floating point types for a shader pipeline is supported.
When not supported, build() will succeed but just show a warning message
and the values of the target attributes will be broken. In practice this
feature will be unsupported in some OpenGL ES 2.0 and OpenGL 2.x
implementations. Note that while D3D does support half precision input
attributes, it does not support the half3 type. The D3D backends pass
half3 attributes as half4. To ensure cross platform compatibility, half3
inputs should be padded to 8 bytes.
*/
/*!
@ -1287,6 +1297,16 @@ QDebug operator<<(QDebug dbg, const QRhiVertexInputBinding &b)
\value SInt3 Three component signed integer vector
\value SInt2 Two component signed integer vector
\value SInt Signed integer
\value Half4 Four component half precision (16bit) float vector
\value Half3 Three component half precision (16bit) float vector
\value Half2 Two component half precision (16bit) float vector
\value Half half precision (16bit) float
\note Support for half precision floating point attributes is indicated at
run time by the QRhi::Feature::HalfAttributes feature flag. Note that D3D
supports half input attributes, but does not support the Half3 type. The
D3D backends pass through Half3 as Half4. To ensure cross platform
compatibility, Half3 inputs should be padded to 8 bytes.
*/
/*!
@ -1419,6 +1439,15 @@ quint32 QRhiImplementation::byteSizePerVertexForVertexInputFormat(QRhiVertexInpu
case QRhiVertexInputAttribute::SInt:
return sizeof(qint32);
case QRhiVertexInputAttribute::Half4:
return 4 * sizeof(qfloat16);
case QRhiVertexInputAttribute::Half3:
return 4 * sizeof(qfloat16); // half3 still takes 8 bytes
case QRhiVertexInputAttribute::Half2:
return 2 * sizeof(qfloat16);
case QRhiVertexInputAttribute::Half:
return sizeof(qfloat16);
default:
Q_UNREACHABLE_RETURN(1);
}

View File

@ -247,7 +247,11 @@ public:
SInt4,
SInt3,
SInt2,
SInt
SInt,
Half4,
Half3,
Half2,
Half
};
QRhiVertexInputAttribute() = default;
@ -1819,7 +1823,8 @@ public:
TextureArrayRange,
NonFillPolygonMode,
OneDimensionalTextures,
OneDimensionalTextureMipmaps
OneDimensionalTextureMipmaps,
HalfAttributes
};
enum BeginFrameFlag {

View File

@ -528,6 +528,8 @@ bool QRhiD3D11::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::OneDimensionalTextureMipmaps:
return true;
case QRhi::HalfAttributes:
return true;
default:
Q_UNREACHABLE();
return false;
@ -4038,6 +4040,14 @@ static inline DXGI_FORMAT toD3DAttributeFormat(QRhiVertexInputAttribute::Format
return DXGI_FORMAT_R32G32_SINT;
case QRhiVertexInputAttribute::SInt:
return DXGI_FORMAT_R32_SINT;
case QRhiVertexInputAttribute::Half4:
// Note: D3D does not support half3. Pass through half3 as half4.
case QRhiVertexInputAttribute::Half3:
return DXGI_FORMAT_R16G16B16A16_FLOAT;
case QRhiVertexInputAttribute::Half2:
return DXGI_FORMAT_R16G16_FLOAT;
case QRhiVertexInputAttribute::Half:
return DXGI_FORMAT_R16_FLOAT;
default:
Q_UNREACHABLE();
return DXGI_FORMAT_R32G32B32A32_FLOAT;

View File

@ -583,6 +583,8 @@ bool QRhiD3D12::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::OneDimensionalTextureMipmaps:
return false;
case QRhi::HalfAttributes:
return true;
}
return false;
}
@ -5053,6 +5055,14 @@ static inline DXGI_FORMAT toD3DAttributeFormat(QRhiVertexInputAttribute::Format
return DXGI_FORMAT_R32G32_SINT;
case QRhiVertexInputAttribute::SInt:
return DXGI_FORMAT_R32_SINT;
case QRhiVertexInputAttribute::Half4:
// Note: D3D does not support half3. Pass through half3 as half4.
case QRhiVertexInputAttribute::Half3:
return DXGI_FORMAT_R16G16B16A16_FLOAT;
case QRhiVertexInputAttribute::Half2:
return DXGI_FORMAT_R16G16_FLOAT;
case QRhiVertexInputAttribute::Half:
return DXGI_FORMAT_R16_FLOAT;
}
Q_UNREACHABLE_RETURN(DXGI_FORMAT_R32G32B32A32_FLOAT);
}

View File

@ -450,6 +450,10 @@ QT_BEGIN_NAMESPACE
# define GL_TEXTURE_1D_ARRAY 0x8C18
#endif
#ifndef GL_HALF_FLOAT
#define GL_HALF_FLOAT 0x140B
#endif
/*!
Constructs a new QRhiGles2InitParams.
@ -959,6 +963,8 @@ bool QRhiGles2::create(QRhi::Flags flags)
if (!caps.gles && (caps.ctxMajor > 3 || (caps.ctxMajor == 3 && caps.ctxMinor >= 2)))
f->glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
caps.halfAttributes = f->hasOpenGLExtension(QOpenGLExtensions::HalfFloatVertex);
nativeHandlesStruct.context = ctx;
contextLost = false;
@ -1315,6 +1321,8 @@ bool QRhiGles2::isFeatureSupported(QRhi::Feature feature) const
return caps.texture1D;
case QRhi::OneDimensionalTextureMipmaps:
return caps.texture1D;
case QRhi::HalfAttributes:
return caps.halfAttributes;
default:
Q_UNREACHABLE_RETURN(false);
}
@ -2939,6 +2947,22 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
type = GL_INT;
size = 1;
break;
case QRhiVertexInputAttribute::Half4:
type = GL_HALF_FLOAT;
size = 4;
break;
case QRhiVertexInputAttribute::Half3:
type = GL_HALF_FLOAT;
size = 3;
break;
case QRhiVertexInputAttribute::Half2:
type = GL_HALF_FLOAT;
size = 2;
break;
case QRhiVertexInputAttribute::Half:
type = GL_HALF_FLOAT;
size = 1;
break;
default:
break;
}

View File

@ -961,7 +961,8 @@ public:
tessellation(false),
geometryShader(false),
texture1D(false),
hasDrawBuffersFunc(false)
hasDrawBuffersFunc(false),
halfAttributes(false)
{ }
int ctxMajor;
int ctxMinor;
@ -1014,6 +1015,7 @@ public:
uint geometryShader : 1;
uint texture1D : 1;
uint hasDrawBuffersFunc : 1;
uint halfAttributes : 1;
} caps;
QGles2SwapChain *currentSwapChain = nullptr;
QSet<GLint> supportedCompressedFormats;

View File

@ -794,6 +794,8 @@ bool QRhiMetal::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::OneDimensionalTextureMipmaps:
return false;
case QRhi::HalfAttributes:
return true;
default:
Q_UNREACHABLE();
return false;
@ -4297,6 +4299,14 @@ static inline MTLVertexFormat toMetalAttributeFormat(QRhiVertexInputAttribute::F
return MTLVertexFormatInt2;
case QRhiVertexInputAttribute::SInt:
return MTLVertexFormatInt;
case QRhiVertexInputAttribute::Half4:
return MTLVertexFormatHalf4;
case QRhiVertexInputAttribute::Half3:
return MTLVertexFormatHalf3;
case QRhiVertexInputAttribute::Half2:
return MTLVertexFormatHalf2;
case QRhiVertexInputAttribute::Half:
return MTLVertexFormatHalf;
default:
Q_UNREACHABLE();
return MTLVertexFormatFloat4;

View File

@ -4265,6 +4265,8 @@ bool QRhiVulkan::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::OneDimensionalTextureMipmaps:
return true;
case QRhi::HalfAttributes:
return true;
default:
Q_UNREACHABLE_RETURN(false);
}
@ -5279,6 +5281,14 @@ static inline VkFormat toVkAttributeFormat(QRhiVertexInputAttribute::Format form
return VK_FORMAT_R32G32_SINT;
case QRhiVertexInputAttribute::SInt:
return VK_FORMAT_R32_SINT;
case QRhiVertexInputAttribute::Half4:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case QRhiVertexInputAttribute::Half3:
return VK_FORMAT_R16G16B16_SFLOAT;
case QRhiVertexInputAttribute::Half2:
return VK_FORMAT_R16G16_SFLOAT;
case QRhiVertexInputAttribute::Half:
return VK_FORMAT_R16_SFLOAT;
default:
Q_UNREACHABLE_RETURN(VK_FORMAT_R32G32B32A32_SFLOAT);
}

View File

@ -21,3 +21,4 @@ qsb --glsl 320es,430 --msl 12 --tess-mode triangles storagebuffer_runtime.tesc -
qsb --glsl 320es,430 --msl 12 --tess-vertex-count 3 storagebuffer_runtime.tese -o storagebuffer_runtime.tese.qsb
qsb --glsl 320es,430 --msl 12 storagebuffer_runtime.frag -o storagebuffer_runtime.frag.qsb
qsb --glsl 320es,430 --hlsl 50 -c --msl 12 storagebuffer_runtime.comp -o storagebuffer_runtime.comp.qsb
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o half.vert.qsb half.vert

View File

@ -0,0 +1,10 @@
#version 440
layout(location = 0) in vec3 position;
out gl_PerVertex { vec4 gl_Position; };
void main()
{
gl_Position = vec4(position, 1.0);
}

Binary file not shown.

View File

@ -152,6 +152,9 @@ private slots:
void storageBufferRuntimeSizeGraphics_data();
void storageBufferRuntimeSizeGraphics();
void halfPrecisionAttributes_data();
void halfPrecisionAttributes();
private:
void setWindowType(QWindow *window, QRhi::Implementation impl);
@ -6190,5 +6193,151 @@ void tst_QRhi::storageBufferRuntimeSizeGraphics()
QCOMPARE(result.pixel(32, 32), qRgb(red, green, blue));
}
void tst_QRhi::halfPrecisionAttributes_data()
{
rhiTestData();
}
void tst_QRhi::halfPrecisionAttributes()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
if (!rhi->isFeatureSupported(QRhi::HalfAttributes)) {
QVERIFY(rhi->backend() != QRhi::Vulkan);
QVERIFY(rhi->backend() != QRhi::Metal);
QVERIFY(rhi->backend() != QRhi::D3D11);
QVERIFY(rhi->backend() != QRhi::D3D12);
QSKIP("Half precision vertex attributes are not supported with this graphics API, skipping test");
}
const QSize outputSize(1920, 1080);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
//
// This test uses half3 vertices
//
// Note: D3D does not support half3 - rhi passes it through as half4. Because of this, D3D will
// report the following warning and error if we don't take precautions:
//
// D3D11 WARNING: ID3D11DeviceContext::Draw: Input vertex slot 0 has stride 6 which is less than
// the minimum stride logically expected from the current Input Layout (8 bytes). This is OK, as
// hardware is perfectly capable of reading overlapping data. However the developer probably did
// not intend to make use of this behavior. [ EXECUTION WARNING #355:
// DEVICE_DRAW_VERTEX_BUFFER_STRIDE_TOO_SMALL]
//
// D3D11 ERROR: ID3D11DeviceContext::Draw: Vertex Buffer Stride (6) at the input vertex slot 0
// is not aligned properly. The current Input Layout imposes an alignment of (4) because of the
// Formats used with this slot. [ EXECUTION ERROR #367: DEVICE_DRAW_VERTEX_STRIDE_UNALIGNED]
//
// The same warning and error are produced for D3D12. The rendered output is correct despite
// the warning and error.
//
// To avoid these errors, we pad the vertices to 8 byte stride.
//
static const qfloat16 vertices[] = {
-1.0, -1.0, 0.0, 0.0,
1.0, -1.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
QVERIFY(vbuf->create());
updates->uploadStaticBuffer(vbuf.data(), vertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/half.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 4 * sizeof(qfloat16) } }); // 8 byte vertex stride for D3D
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Half3, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
cb->setVertexInput(0, 1, &vbindings);
cb->draw(3);
QRhiReadbackResult readResult;
QImage result;
readResult.completed = [&readResult, &result] {
result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied); // non-owning, no copy needed because readResult outlives result
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
// Offscreen frames are synchronous, so the readback is guaranteed to
// complete at this point. This would not be the case with swapchain-based
// frames.
QCOMPARE(result.size(), texture->pixelSize());
if (impl == QRhi::Null)
return;
// Now we have a red rectangle on blue background.
const int y = 100;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int x = result.width() - 1;
int redCount = 0;
int blueCount = 0;
const int maxFuzz = 1;
while (x-- >= 0) {
const QRgb c(*p++);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL("Encountered a pixel that is neither red or blue");
}
QCOMPARE(redCount + blueCount, texture->pixelSize().width());
QVERIFY(redCount != 0);
QVERIFY(blueCount != 0);
// The triangle is "pointing up" in the resulting image with OpenGL
// (because Y is up both in normalized device coordinates and in images)
// and Vulkan (because Y is down in both and the vertex data was specified
// with Y up in mind), but "pointing down" with D3D (because Y is up in NDC
// but down in images).
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
QVERIFY(redCount < blueCount);
else
QVERIFY(redCount > blueCount);
}
#include <tst_qrhi.moc>
QTEST_MAIN(tst_QRhi)