Skip to content

gfx: fall back to RGBA8 where the backend has no BGRA8 (fixes images on wasm)#2147

Merged
jcelerier merged 1 commit into
masterfrom
gfx-bgra-fallback
Jul 26, 2026
Merged

gfx: fall back to RGBA8 where the backend has no BGRA8 (fixes images on wasm)#2147
jcelerier merged 1 commit into
masterfrom
gfx-bgra-fallback

Conversation

@jcelerier

Copy link
Copy Markdown
Member

On WebAssembly, uploading any image raised

WebGL: INVALID_ENUM: texSubImage2D: invalid format
  QRhiGles2::executeCommandBuffer -> QRhiGles2::endFrame -> score::gfx::Window::render

so most library shaders that use an image rendered nothing.

Cause

QRhiGles2::toGlTextureFormat() sets glformat = GL_BGRA for QRhiTexture::BGRA8 unconditionally (qrhigles2.cpp:1395). 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 — so both texImage2D and texSubImage2D reject it.

Reproduced against a plain WebGL2 context, replicating the exact (internalformat, format, type) triples Qt produces:

format texImage2D texSubImage2D
RGBA8 ok ok
BGRA8 INVALID_ENUM INVALID_ENUM
R8 ok ok
R32F ok ok
RGBA16F ok ok
RGBA32F ok ok

Only BGRA8 fails; the context is WebGL2 / GLSL ES 3.00, so GL_RED and the float formats are all fine.

Fix

Ask the QRhi whether it actually has BGRA8 and fall back to RGBA8 when it does not. A runtime check rather than a wasm #ifdef, so any other backend missing the format is covered; desktop keeps BGRA8 and its existing QImage::Format_ARGB32 data unchanged.

QRhi uploads a QImage's bits as-is, so adaptImageFormat() puts the image in whichever byte order the texture ended up with, preserving premultiplication so only channel order changes.

Four call sites: ImageNode ×3, TextNode ×1. No shader changes.

Verification

  • The format matrix above is reproducible in seconds and is what identifies the failing enum.
  • Builds clean for wasm.
  • Not yet confirmed end to end: I could not get a shader rendering into a GL output under headless automation (no WebGL context is created until a gfx output exists), so that a library shader now displays its image is still unverified by me.

…rmat

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oPj1zRcxSQX7FNni7EHM6
@jcelerier

Copy link
Copy Markdown
Member Author

Verified end-to-end on wasm: the INVALID_ENUM is gone

Driven headlessly through CDP: open the process library → create an interval → double-click Visuals > Textures > Text → add a Window device in Background mode → set the process's texture outlet Outputs to Window → play. GL calls are wrapped so every texImage2D / texSubImage2D / texStorage2D is recorded with its enum names and any error it raised.

Before (no fix):

calls: texImage2D:RGBA,0x80e1,UNSIGNED_BYTE   x4     (0x80e1 = GL_BGRA)
       texSubImage2D:0x80e1,UNSIGNED_BYTE     x2
       texStorage2D:RGBA8                     x4
errors: texImage2D:RGBA,0x80e1,UNSIGNED_BYTE -> INVALID_ENUM   x4
        texSubImage2D:0x80e1,UNSIGNED_BYTE   -> INVALID_ENUM   x2
        (all at 1920x1080, i.e. the TextNode texture)

After (this PR's commit):

calls: texImage2D:RGBA,RGBA,UNSIGNED_BYTE     x11
       texStorage2D:RGBA8                     x5
       texSubImage2D:RGBA,UNSIGNED_BYTE       x2
errors: {}   <- none

Same scenario, same machine, same headless Chrome. The two texSubImage2D that used to fail with GL_BGRA are the TextNode image upload, and they now go through as GL_RGBA with no error. The context reports EXT_texture_format_BGRA8888: false, confirming WebGL has no BGRA at all.

Important caveat about which builds those numbers come from

This PR cannot be observed on wasm on its own. Built from gfx-bgra-fallback (master + the fix), the gfx stack never renders anything at all: I added a Window device, confirmed via ?gfxlog that the swapchain and render list are built (recreateOutputRenderList: canRender=true, ScreenNode::createRenderer rtSize=QSize(1600,857)), and still got zero texture uploads and a black output. That is the separate set of wasm gfx bugs fixed in #2144.

So the A/B above was run on two purpose-built images:

(both deployed under the usual test server; the branch itself was not modified, and the temporary merge branch has been deleted). Worth knowing for whoever merges: #2147 is only user-visible on wasm once #2144 is in.

Content rendering

The render pipeline runs and the background renderer takes over the timeline background, but I could not get the glyphs themselves to show up in a screenshot even at 300pt, so I am not claiming visual confirmation — only that the uploads now succeed and the graph renders without error. Worth a human eyeball on a real shader.

Channel order — checked in code, not visually

No swap risk in the four sites: every ImageNode upload goes through RenderList::adaptImage (ImageNode.cpp:489, 509, 521, 823, 977), which now ends in adaptImageFormat, and TextNode calls adaptImageFormat directly. The mapping is right: BGRA8 -> Format_ARGB32 (bytes B,G,R,A LE) and RGBA8 -> Format_RGBA8888 (bytes R,G,B,A). As a side effect this also fixes a latent desktop bug: adaptImage previously returned whatever format the source had, so an RGBA8888 source uploaded into a BGRA8 texture was already channel-swapped.

Sites this PR does not cover

Not blockers for the stated scope, but they are the same bug and will bite on wasm:

  • Gfx/Graph/decoders/GPUVideoDecoderFactory.cpp:134,137AV_PIX_FMT_BGR0 / AV_PIX_FMT_BGRA map to QRhiTexture::BGRA8 and the decoded frame is uploaded straight in. Any video whose decoded format is BGRA will fail identically on wasm. Fixable the same way plus a tex.bgra swizzle in the decoder's shader string, but I could not test video here.
  • Gfx/Graph/RenderedCSFNode.cpp:31 and Crousti/TextureFormat.hpp:204,346,444 — these honour a BGRA8/bgra format string declared by a CSF shader or an avnd port, so they are opt-in rather than something score picks by itself; still unsupported on wasm.
  • WindowCaptureNode.cpp:78 and the Spout paths are desktop/Windows-only, so they are fine.

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 15.31% (-0.001%) from 15.311% — gfx-bgra-fallback into master

@jcelerier
jcelerier merged commit 6e2e1f0 into master Jul 26, 2026
52 of 59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants