From 98c48c25e2257d8440e4452bd8b91c338244ee46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sat, 25 Jul 2026 00:29:38 -0400 Subject: [PATCH] gfx: stop asking for a BGRA8 texture where the backend has no such format Uploading an image on WebAssembly raised WebGL: INVALID_ENUM: texSubImage2D: invalid format from QRhiGles2::executeCommandBuffer, so most library shaders that use an image rendered nothing. QRhiGles2::toGlTextureFormat() sets glformat to GL_BGRA for QRhiTexture::BGRA8 unconditionally; caps.bgraExternalFormat only gates isTextureFormatSupported(), which none of these call sites checked. WebGL has no BGRA format at all -- EXT_texture_format_BGRA8888 is not exposed there -- so both texImage2D and texSubImage2D reject it. Verified in the browser against a plain WebGL2 context: of the formats this code uses, only BGRA8 fails, while RGBA8, R8, R32F, RGBA16F and RGBA32F are all accepted. Ask the QRhi whether it really has BGRA8, and fall back to RGBA8 when it does not. This is a runtime decision rather than a wasm ifdef, so any other backend missing the format is covered too, and desktop keeps using BGRA8 and its existing QImage::Format_ARGB32 data unchanged. QRhi uploads a QImage's bits as-is, so the image has to be in whichever byte order the texture ended up with; adaptImageFormat() converts it, preserving premultiplication so only the channel order changes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019oPj1zRcxSQX7FNni7EHM6 --- .../score-plugin-gfx/Gfx/Graph/ImageNode.cpp | 6 ++--- .../score-plugin-gfx/Gfx/Graph/RenderList.cpp | 25 ++++++++++++++++++- .../score-plugin-gfx/Gfx/Graph/RenderList.hpp | 19 ++++++++++++++ .../score-plugin-gfx/Gfx/Graph/TextNode.cpp | 6 +++-- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/ImageNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/ImageNode.cpp index d7d6608329..9c42fafa48 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/ImageNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/ImageNode.cpp @@ -365,7 +365,7 @@ class ImagesNode::PreloadedRenderer : public GenericNodeRenderer if(m_textures.size() <= i) { QRhiTexture* tex = tex - = rhi.newTexture(QRhiTexture::BGRA8, tex_size, 1, QRhiTexture::Flag{}); + = rhi.newTexture(imageTextureFormat(rhi), tex_size, 1, QRhiTexture::Flag{}); tex->setName("ImagesNode::tex"); tex->create(); m_textures.push_back(tex); @@ -702,7 +702,7 @@ class ImagesNode::OnTheFlyRenderer : public GenericNodeRenderer maxSize.setHeight(std::max(maxSize.height(), sz.height())); } - auto tex = rhi.newTexture(QRhiTexture::BGRA8, maxSize, 1, QRhiTexture::Flag{}); + auto tex = rhi.newTexture(imageTextureFormat(rhi), maxSize, 1, QRhiTexture::Flag{}); tex->setName("OnTheFlyRenderer::tex"); tex->create(); @@ -945,7 +945,7 @@ class FullScreenImageNode::Renderer : public GenericNodeRenderer // Create GPU textures for the image const QSize sz = n.m_image.size(); auto tex = rhi.newTexture( - QRhiTexture::BGRA8, QSize{sz.width(), sz.height()}, 1, QRhiTexture::Flag{}); + imageTextureFormat(rhi), QSize{sz.width(), sz.height()}, 1, QRhiTexture::Flag{}); tex->setName("FullScreenImageNode::tex"); tex->create(); diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp index 768b003273..443be84b5e 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp @@ -288,10 +288,33 @@ BufferView RenderList::bufferForOutput(const Edge& edge) const noexcept return {}; } +QRhiTexture::Format imageTextureFormat(const QRhi& rhi) noexcept +{ + return rhi.isTextureFormatSupported(QRhiTexture::BGRA8) ? QRhiTexture::BGRA8 + : QRhiTexture::RGBA8; +} + +QImage adaptImageFormat(QImage img, QRhiTexture::Format fmt) +{ + const bool premultiplied = img.format() == QImage::Format_ARGB32_Premultiplied + || img.format() == QImage::Format_RGBA8888_Premultiplied; + + QImage::Format wanted{}; + if(fmt == QRhiTexture::BGRA8) + wanted = premultiplied ? QImage::Format_ARGB32_Premultiplied : QImage::Format_ARGB32; + else + wanted = premultiplied ? QImage::Format_RGBA8888_Premultiplied + : QImage::Format_RGBA8888; + + if(img.format() != wanted) + img.convertTo(wanted); + return img; +} + QImage RenderList::adaptImage(const QImage& frame) { auto res = resizeTexture(frame, m_minTexSize, m_maxTexSize); - return res; + return adaptImageFormat(std::move(res), imageTextureFormat(*state.rhi)); //if(m_flip) // res = std::move(res).mirrored(); //return res; diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.hpp index 94a0aa6ae3..bbff50975a 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.hpp @@ -197,4 +197,23 @@ class SCORE_PLUGIN_GFX_EXPORT RenderList bool m_ready{}; bool m_built{}; }; + +/** + * @brief Texture format to use for images uploaded from the CPU + * + * BGRA8 where the backend has it, RGBA8 otherwise. QRhiGles2 asks for + * GL_BGRA whenever a texture is BGRA8, and WebGL has no such format at all, + * so uploading one there is an INVALID_ENUM and the image never appears. + */ +SCORE_PLUGIN_GFX_EXPORT +QRhiTexture::Format imageTextureFormat(const QRhi& rhi) noexcept; + +/** + * @brief Put an image in the byte order imageTextureFormat() asks for + * + * QRhi uploads a QImage's bits as-is, so the two have to agree. + * Premultiplication is preserved: only the channel order changes. + */ +SCORE_PLUGIN_GFX_EXPORT +QImage adaptImageFormat(QImage img, QRhiTexture::Format fmt); } diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/TextNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/TextNode.cpp index b0c5c7f7ed..13ca25ea4b 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/TextNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/TextNode.cpp @@ -125,7 +125,7 @@ class TextNode::Renderer : public GenericNodeRenderer QRhi& rhi = *renderer.state.rhi; { - auto tex = rhi.newTexture(QRhiTexture::BGRA8, sz, 1, QRhiTexture::Flag{}); + auto tex = rhi.newTexture(imageTextureFormat(rhi), sz, 1, QRhiTexture::Flag{}); tex->setName("TextNode::tex"); tex->create(); @@ -167,7 +167,9 @@ class TextNode::Renderer : public GenericNodeRenderer if(!m_uploaded) { - res.uploadTexture(m_textures[0].second, m_img); + res.uploadTexture( + m_textures[0].second, + adaptImageFormat(m_img, imageTextureFormat(*renderer.state.rhi))); m_uploaded = true; }