[vlc-commits] [Git][videolan/vlc][master] 7 commits: qt: fix batch rendering when cropping is used in `ImageExt`

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Thu Jun 18 15:51:13 UTC 2026



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
a63640ec by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qt: fix batch rendering when cropping is used in `ImageExt`

Non-special uniform values must be matching for batched
rendering to work. If we calculate the crop rate in
`ShaderEffect` as a property, since we depend on the
sub-texture size (implicit size), the values would differ
depending on the texture even if these textures are in
the atlas.

The majority of cropping calculation, such as the
normalizations are already in the fragment shader, we
simply move the following to the fragment shader:

```qml
const implicitRatio = implicitWidth / implicitHeight
const ratio = width / height

if (ratio > implicitRatio)
  ret.height = (implicitHeight - implicitWidth) / 2 / implicitHeight
else if (ratio < implicitRatio)
  ret.width = (implicitWidth - implicitHeight) / 2 / implicitWidth
```

Granted, this introduces new branches in the fragment
shader, but these depend on uniform values, so this is
not really a big concern.

This also requires using texture provider observer in
`ImageExt` because we can not use `textureSize()` in
the shader due to support for GLSL 1.20/ESSL 1.0.

Although I would prefer not having to use it here, not
only for maintenance burden but also the induced delay
for cropping due to native texture size roundtrip between
the scene graph and QML engine, it also provides these
benefits:

- Better determination for blending, since now we can
  probe if the texture has alpha channel through the
  observer.
- If source is an arbitrary texture provider, no need
  to provide `status` or implicit (texture size) size.
  Currently `EnhancedImageExt` must be used in this
  case.

- - - - -
d872e23a by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qml: make use of the texture provider observer when adjusting blending in `ImageExt`

Now that we have `TextureProviderObserver` here (since cropping now requires
forwarding native texture size to the shader as we can't use `textureSize()`),
we can as well make use of `hasAlphaChannel` to decide better on whether blending
is necessary.

- - - - -
620594d8 by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qml: no need to adjust `blending` in `EnhancedImageExt` anymore

This is done in `ImageExt` now, there is no need to do the same
in `EnhancedImageExt`

- - - - -
a3e2ee91 by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qml: no need to require source texture provider to provide `status` in `ImageExt`

- - - - -
c58d3c6b by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qml: no need to provide `status` in `EnhancedImageExt` anymore

This is done in `ImageExt` now, there is no need to do the same
in `EnhancedImageExt`.

- - - - -
ded6dd36 by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qml: no need to require source texture provider to provide implicit size in `ImageExt`

- - - - -
c27b2b2e by Fatih Uzunoglu at 2026-06-18T17:39:06+02:00
qml: no need to provide implicit size in `EnhancedImageExt` anymore

This is done in `ImageExt` now, there is no need to do the same
in `EnhancedImageExt`.

- - - - -


4 changed files:

- modules/gui/qt/shaders/SDFAARoundedTexture.frag
- modules/gui/qt/shaders/SDFAARoundedTexture_cropsupport_bordersupport.frag
- modules/gui/qt/widgets/qml/EnhancedImageExt.qml
- modules/gui/qt/widgets/qml/ImageExt.qml


Changes:

=====================================
modules/gui/qt/shaders/SDFAARoundedTexture.frag
=====================================
@@ -48,7 +48,8 @@ layout(std140, binding = 0) uniform buf {
     int antialiasing; // WARNING: intentionally not a boolean
 #endif
 #ifdef CROP_SUPPORT
-    vec2 cropRate;
+    int shouldCrop; // WARNING: intentionally not a boolean
+    vec2 sourceTextureSize; // TODO: Ditch targeting GLSL 1.20/ESSL 1.0, and use `textureSize()` instead.
 #endif
 #ifdef BACKGROUND_SUPPORT
     vec4 backgroundColor;
@@ -108,42 +109,54 @@ void main()
         dist = sdRoundBox(p, vec2(size.x / size.y, 1.0), vec4(radiusTopRight, radiusBottomRight, radiusTopLeft, radiusBottomLeft));
     }
 
-#ifdef CROP_SUPPORT
-    vec2 texCoord;
+    vec4 texel;
 
-    if (cropRate.x > 0.0)
+#ifdef CROP_SUPPORT
+    if (shouldCrop != 0)
     {
-        float normalCropRate = qt_SubRect_source.z * cropRate.x;
+        vec2 texCoord;
 
-        float k = qt_SubRect_source.z + qt_SubRect_source.x - normalCropRate;
-        float l = qt_SubRect_source.x + normalCropRate;
+        vec2 denormalSubTextureSize = vec2(sourceTextureSize.x * qt_SubRect_source.z, sourceTextureSize.y * qt_SubRect_source.w);
 
-        texCoord.x = (k - l) / (qt_SubRect_source.z) * (qt_TexCoord0.x - qt_SubRect_source.x) + l;
-    }
-    else
-    {
-        texCoord.x = qt_TexCoord0.x;
-    }
+        float implicitRatio = denormalSubTextureSize.x / denormalSubTextureSize.y;
+        float ratio = size.x / size.y;
 
-    if (cropRate.y > 0.0)
-    {
-        float normalCropRate = qt_SubRect_source.w * cropRate.y;
+        if ((implicitRatio > 1.0) && (ratio < implicitRatio))
+        {
+            float cropRate = (denormalSubTextureSize.x - denormalSubTextureSize.y) / 2 / denormalSubTextureSize.x;
+            float normalCropRate = qt_SubRect_source.z * cropRate;
+
+            float k = qt_SubRect_source.z + qt_SubRect_source.x - normalCropRate;
+            float l = qt_SubRect_source.x + normalCropRate;
 
-        float k = qt_SubRect_source.w + qt_SubRect_source.y - normalCropRate;
-        float l = qt_SubRect_source.y + normalCropRate;
+            texCoord.x = (k - l) / (qt_SubRect_source.z) * (qt_TexCoord0.x - qt_SubRect_source.x) + l;
+            texCoord.y = qt_TexCoord0.y;
+        }
+        else if ((implicitRatio < 1.0) && (ratio > implicitRatio))
+        {
+            float cropRate = (denormalSubTextureSize.y - denormalSubTextureSize.x) / 2 / denormalSubTextureSize.y;
+            float normalCropRate = qt_SubRect_source.w * cropRate;
 
-        texCoord.y = (k - l) / (qt_SubRect_source.w) * (qt_TexCoord0.y - qt_SubRect_source.y) + l;
+            float k = qt_SubRect_source.w + qt_SubRect_source.y - normalCropRate;
+            float l = qt_SubRect_source.y + normalCropRate;
+
+            texCoord.y = (k - l) / (qt_SubRect_source.w) * (qt_TexCoord0.y - qt_SubRect_source.y) + l;
+            texCoord.x = qt_TexCoord0.x;
+        }
+        else
+        {
+            texCoord.x = qt_TexCoord0.x;
+            texCoord.y = qt_TexCoord0.y;
+        }
+
+        texel = texture(source, texCoord);
     }
     else
+#endif
     {
-        texCoord.y = qt_TexCoord0.y;
+        texel = texture(source, qt_TexCoord0);
     }
 
-    vec4 texel = texture(source, texCoord);
-#else
-    vec4 texel = texture(source, qt_TexCoord0);
-#endif
-
 #ifdef BACKGROUND_SUPPORT
     // Source over blending (S + D * (1 - S.a)):
     texel = texel + backgroundColor * (1.0 - texel.a);


=====================================
modules/gui/qt/shaders/SDFAARoundedTexture_cropsupport_bordersupport.frag
=====================================
@@ -61,7 +61,8 @@ layout(std140, binding = 0) uniform buf {
     int antialiasing; // WARNING: intentionally not a boolean
 #endif
 #ifdef CROP_SUPPORT
-    vec2 cropRate;
+    int shouldCrop; // WARNING: intentionally not a boolean
+    vec2 sourceTextureSize; // TODO: Ditch targeting GLSL 1.20/ESSL 1.0, and use `textureSize()` instead.
 #endif
 #ifdef BACKGROUND_SUPPORT
     vec4 backgroundColor;
@@ -121,42 +122,54 @@ void main()
         dist = sdRoundBox(p, vec2(size.x / size.y, 1.0), vec4(radiusTopRight, radiusBottomRight, radiusTopLeft, radiusBottomLeft));
     }
 
-#ifdef CROP_SUPPORT
-    vec2 texCoord;
+    vec4 texel;
 
-    if (cropRate.x > 0.0)
+#ifdef CROP_SUPPORT
+    if (shouldCrop != 0)
     {
-        float normalCropRate = qt_SubRect_source.z * cropRate.x;
+        vec2 texCoord;
 
-        float k = qt_SubRect_source.z + qt_SubRect_source.x - normalCropRate;
-        float l = qt_SubRect_source.x + normalCropRate;
+        vec2 denormalSubTextureSize = vec2(sourceTextureSize.x * qt_SubRect_source.z, sourceTextureSize.y * qt_SubRect_source.w);
 
-        texCoord.x = (k - l) / (qt_SubRect_source.z) * (qt_TexCoord0.x - qt_SubRect_source.x) + l;
-    }
-    else
-    {
-        texCoord.x = qt_TexCoord0.x;
-    }
+        float implicitRatio = denormalSubTextureSize.x / denormalSubTextureSize.y;
+        float ratio = size.x / size.y;
 
-    if (cropRate.y > 0.0)
-    {
-        float normalCropRate = qt_SubRect_source.w * cropRate.y;
+        if ((implicitRatio > 1.0) && (ratio < implicitRatio))
+        {
+            float cropRate = (denormalSubTextureSize.x - denormalSubTextureSize.y) / 2 / denormalSubTextureSize.x;
+            float normalCropRate = qt_SubRect_source.z * cropRate;
+
+            float k = qt_SubRect_source.z + qt_SubRect_source.x - normalCropRate;
+            float l = qt_SubRect_source.x + normalCropRate;
 
-        float k = qt_SubRect_source.w + qt_SubRect_source.y - normalCropRate;
-        float l = qt_SubRect_source.y + normalCropRate;
+            texCoord.x = (k - l) / (qt_SubRect_source.z) * (qt_TexCoord0.x - qt_SubRect_source.x) + l;
+            texCoord.y = qt_TexCoord0.y;
+        }
+        else if ((implicitRatio < 1.0) && (ratio > implicitRatio))
+        {
+            float cropRate = (denormalSubTextureSize.y - denormalSubTextureSize.x) / 2 / denormalSubTextureSize.y;
+            float normalCropRate = qt_SubRect_source.w * cropRate;
 
-        texCoord.y = (k - l) / (qt_SubRect_source.w) * (qt_TexCoord0.y - qt_SubRect_source.y) + l;
+            float k = qt_SubRect_source.w + qt_SubRect_source.y - normalCropRate;
+            float l = qt_SubRect_source.y + normalCropRate;
+
+            texCoord.y = (k - l) / (qt_SubRect_source.w) * (qt_TexCoord0.y - qt_SubRect_source.y) + l;
+            texCoord.x = qt_TexCoord0.x;
+        }
+        else
+        {
+            texCoord.x = qt_TexCoord0.x;
+            texCoord.y = qt_TexCoord0.y;
+        }
+
+        texel = texture(source, texCoord);
     }
     else
+#endif
     {
-        texCoord.y = qt_TexCoord0.y;
+        texel = texture(source, qt_TexCoord0);
     }
 
-    vec4 texel = texture(source, texCoord);
-#else
-    vec4 texel = texture(source, qt_TexCoord0);
-#endif
-
 #ifdef BACKGROUND_SUPPORT
     // Source over blending (S + D * (1 - S.a)):
     texel = texel + backgroundColor * (1.0 - texel.a);


=====================================
modules/gui/qt/widgets/qml/EnhancedImageExt.qml
=====================================
@@ -45,22 +45,6 @@ ImageExt {
     // No need to load images in this case:
     loadImages: (targetTextureProvider === root.sourceTextureProviderItem)
 
-    blending: {
-        if (effectiveRadius > 0.0)
-            return true // Outside the radius is always transparent, need blending
-
-        if (effectiveBackgroundColor.a > (1.0 - Number.EPSILON))
-            return false // If background color is opaque, no need for blending
-
-        if (textureProviderItem === textureProviderIndirection) {
-            console.assert(observer.source === textureProviderIndirection)
-            if (!observer.hasAlphaChannel)
-                return false // If the texture is opaque, no need for blending
-        }
-
-        return true
-    }
-
     /// <debug>
     readonly property QtObject _sourceWindow: (targetTextureProvider?.Window.window ?? null)
     function _onWindowChanged() {
@@ -77,13 +61,6 @@ ImageExt {
     TextureProviderIndirection {
         id: textureProviderIndirection
 
-        // `Image` interface, as `ImageExt` needs it:
-        readonly property int status: (source instanceof Image ? ((source.status === Image.Ready && observer.isValid) ? Image.Ready : Image.Loading)
-                                                               : (observer.isValid ? Image.Ready : Image.Null))
-
-        implicitWidth: (source instanceof Image) ? source.implicitWidth : textureSize.width
-        implicitHeight: (source instanceof Image) ? source.implicitHeight : textureSize.height
-
         readonly property bool sourceNeedsTiling: (root.fillMode === Image.Tile ||
                                                    root.fillMode === Image.TileVertically ||
                                                    root.fillMode === Image.TileHorizontally)
@@ -94,13 +71,5 @@ ImageExt {
         verticalWrapMode: sourceNeedsTiling ? TextureProviderIndirection.Repeat : TextureProviderIndirection.ClampToEdge
 
         textureSubRect: sourceNeedsTiling ? Qt.rect(0, 0, root.paintedWidth, root.paintedHeight) : undefined
-
-        readonly property size textureSize: observer.textureSize
-
-        TextureProviderObserver {
-            id: observer
-            source: textureProviderIndirection
-            notifyAllChanges: (root.visible && textureProviderIndirection.source)
-        }
     }
 }


=====================================
modules/gui/qt/widgets/qml/ImageExt.qml
=====================================
@@ -17,6 +17,8 @@
  *****************************************************************************/
 import QtQuick
 
+import VLC.Util
+
 // NOTE: ImageExt behaves exactly like Image, except when at least one of these features are used:
 //       - `PreserveAspectCrop` fill mode without requiring a clip node.
 //       - Rounded rectangular shaping.
@@ -47,7 +49,17 @@ Item {
     property url source
     property alias sourceSize: image.sourceSize
     property alias sourceClipRect: image.sourceClipRect
-    readonly property int status: shaderEffect.source.status
+    readonly property int status: {
+        if (effectiveTextureProviderItem instanceof Image) {
+            return effectiveTextureProviderItem.status
+        } else {
+            if (tpObserver.isValid)
+                return Image.Ready
+            else
+                return Image.Null
+        }
+    }
+
     property alias shaderStatus: shaderEffect.status
     property alias cache: image.cache
 
@@ -63,10 +75,7 @@ Item {
     // WARNING: Consumers who downcast this item to `Image` are doing this on their own
     //          discretion. It is discouraged, but not forbidden (or evil).
     readonly property Item sourceTextureProviderItem: image
-    // NOTE:    It is allowed to adjust this property to use an external texture provider, where
-    //          in that case the texture provider item should provide a subset of `Image`'s
-    //          interface, that is, provide `status` and `implicitWidth`/`implicitHeight` that
-    //          reflect the texture size.
+    // NOTE:    It is allowed to adjust this property to use an external texture provider.
     // NOTE:    If `textureProviderItem` is set to `null`, the default `Image` is going to be
     //          used as fallback. For that reason, it is recommended to keep providing the
     //          `source` url if there is a possibility of `textureProviderItem` being `null`
@@ -123,7 +132,18 @@ Item {
     readonly property color effectiveBackgroundColor: shaderEffect.visible ? backgroundColor : "transparent"
 
     property alias blending: shaderEffect.blending
-    blending: (effectiveRadius > 0.0) || (effectiveBackgroundColor.a < 1.0)
+    blending: {
+        if (effectiveRadius > 0.0)
+            return true // Outside the radius is always transparent, need blending
+
+        if (effectiveBackgroundColor.a > (1.0 - Number.EPSILON))
+            return false // If background color is opaque, no need for blending
+
+        if (!tpObserver.hasAlphaChannel)
+            return false // If the texture is opaque, no need for blending
+
+        return true
+    }
 
     // Border:
     // NOTE: The border is an overlay for the texture (the
@@ -148,13 +168,18 @@ Item {
         anchors.alignWhenCentered: true
         anchors.centerIn: parent
 
-        implicitWidth: (source.status === Image.Ready) ? source.implicitWidth : 64
-        implicitHeight: (source.status === Image.Ready) ? source.implicitHeight : 64
+        implicitWidth: (source instanceof Image) ? source.implicitWidth : textureSize.width
+        implicitHeight: (source instanceof Image) ? source.implicitHeight : textureSize.height
+
+        // WARNING: Do not put this into the uniform block of the shader,
+        //          since it would break batch rendering as it reflects
+        //          the sub-texture size.
+        readonly property size textureSize: tpObserver.textureSize
 
         width: paintedSize.width
         height: paintedSize.height
 
-        visible: (source.status === Image.Ready) &&
+        visible: (root.status === Image.Ready) &&
                  (GraphicsInfo.shaderType === GraphicsInfo.RhiShader) &&
                  (root.radius > 0.0 ||
                   root.borderWidth > 0 ||
@@ -186,37 +211,31 @@ Item {
         readonly property double softEdgeMin: -1. / Math.min(width, height)
         readonly property double softEdgeMax: -softEdgeMin
 
-        readonly property size cropRate: {
-            let ret = Qt.size(0.0, 0.0)
-
-            // No need to calculate if PreserveAspectCrop is not used
-            if (root.fillMode !== Image.PreserveAspectCrop)
-                return ret
-
-            // No need to calculate if image is not ready
-            if (source.status !== Image.Ready)
-                return ret
-
-            const implicitRatio = implicitWidth / implicitHeight
-            const ratio = width / height
-
-            if (ratio > implicitRatio)
-                ret.height = (implicitHeight - implicitWidth) / 2 / implicitHeight
-            else if (ratio < implicitRatio)
-                ret.width = (implicitWidth - implicitHeight) / 2 / implicitWidth
-
-            return ret
-        }
-
+        // Previously we were calculating the crop rate here based on the sub-texture
+        // size (implicit size), but that was breaking batch rendering. So instead,
+        // we now calculate the crop rate in fragment shader. We can not do that in
+        // the vertex shader either because having a custom vertex shader also breaks
+        // batch rendering in `ShaderEffect`.
+        readonly property bool shouldCrop: (root.fillMode === Image.PreserveAspectCrop)
+
+        // Native texture size (atlas size if texture is in the atlas):
+        // TODO: We could use `textureSize()` and get rid of this, but we
+        //       can not because we are targeting GLSL 1.20/ESSL 1.0, even
+        //       though the shader is written in GLSL 4.40.
+        readonly property size sourceTextureSize: tpObserver.nativeTextureSize
+
+        // WARNING: Do not put this into the uniform block of the shader,
+        //          since it depends on the implicit size, it would break
+        //          batch rendering. This is a concern for delegates.
         readonly property size paintedSize: {
             let ret = Qt.size(0.0, 0.0)
 
             // No need to calculate if the texture is not ready:
-            if (source.status !== Image.Ready)
+            if (root.status !== Image.Ready)
                 return ret
 
             // NOTE: Calculations are based on `QQuickImage`,
-            //       except preserve aspect crop (see `cropRate`).
+            //       except preserve aspect crop.
             if (root.fillMode === Image.PreserveAspectCrop) {
                 return Qt.size(root.width, root.height)
             } else if (root.fillMode === Image.PreserveAspectFit) {
@@ -255,7 +274,7 @@ Item {
         }
 
         // (2 / width) seems to be a good coefficient to make it similar to `Rectangle.border`:
-        readonly property double borderRange: (source.status === Image.Ready) ? (root.borderWidth / width * 2.) : 0.0 // no need for outlining if there is no image (nothing to outline)
+        readonly property double borderRange: (root.status === Image.Ready) ? (root.borderWidth / width * 2.) : 0.0 // no need for outlining if there is no image (nothing to outline)
         readonly property color borderColor: root.borderColor
 
         // QQuickImage as texture provider, no need for ShaderEffectSource.
@@ -263,9 +282,15 @@ Item {
         // so that we can make use of our custom shader.
         readonly property Item source: root.textureProviderItem ?? image
 
-        fragmentShader: (cropRate.width > 0.0 || cropRate.height > 0.0) || (root.borderWidth > 0) ? "qrc:///shaders/SDFAARoundedTexture_cropsupport_bordersupport.frag.qsb"
-                                                                                                  : "qrc:///shaders/SDFAARoundedTexture.frag.qsb"
+        fragmentShader: (shouldCrop || (borderRange > 0)) ? "qrc:///shaders/SDFAARoundedTexture_cropsupport_bordersupport.frag.qsb"
+                                                          : "qrc:///shaders/SDFAARoundedTexture.frag.qsb"
 
+        TextureProviderObserver {
+            id: tpObserver
+
+            source: shaderEffect.source
+            notifyAllChanges: shaderEffect.visible
+        }
     }
 
     Image {



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/6ab69ed6e523a44dfa7fd51d99f6b6be9cd3b8ba...c27b2b2e6ce0cc5edb9fbb3fd4628200808d9e01

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/6ab69ed6e523a44dfa7fd51d99f6b6be9cd3b8ba...c27b2b2e6ce0cc5edb9fbb3fd4628200808d9e01
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list