[vlc-commits] [Git][videolan/vlc][master] 4 commits: qml: implement `live` optimization hint in `DualKawaseBlur`
Felix Paul Kühne (@fkuehne)
gitlab at videolan.org
Mon Sep 22 16:57:42 UTC 2025
Felix Paul Kühne pushed to branch master at VideoLAN / VLC
Commits:
7eeab6c7 by Fatih Uzunoglu at 2025-09-22T15:10:40+00:00
qml: implement `live` optimization hint in `DualKawaseBlur`
- - - - -
9cc19cdb by Fatih Uzunoglu at 2025-09-22T15:10:40+00:00
qml: expose the source texture provider observer in `DualKawaseBlur`
- - - - -
9c0bfb07 by Fatih Uzunoglu at 2025-09-22T15:10:40+00:00
qml: disable `live` for the blur effect in player page
The source is static, we can use the optimization hint here.
- - - - -
298d1060 by Fatih Uzunoglu at 2025-09-22T15:10:40+00:00
qml: revise the todo regarding disabling `live` in blur effect in `ArtistTopBanner`
- - - - -
3 changed files:
- modules/gui/qt/medialibrary/qml/ArtistTopBanner.qml
- modules/gui/qt/player/qml/Player.qml
- modules/gui/qt/widgets/qml/DualKawaseBlur.qml
Changes:
=====================================
modules/gui/qt/medialibrary/qml/ArtistTopBanner.qml
=====================================
@@ -95,7 +95,7 @@ FocusScope {
anchors.left: parent.left
anchors.right: parent.right
- // TODO: Disable `live`, consider asynchronous loading and size changes.
+ // NOTE: No need to disable `live`, as this uses two pass mode so there is no video memory saving benefit.
// If source image is tiled, layering is necessary:
readonly property bool sourceNeedsLayering: (background.fillMode === Image.Tile)
=====================================
modules/gui/qt/player/qml/Player.qml
=====================================
@@ -290,7 +290,7 @@ FocusScope {
radius: 3
- // TODO: Disable `live`, consider asynchronous loading.
+ live: false
//destination aspect ratio
readonly property real dar: parent.width / parent.height
@@ -322,6 +322,33 @@ FocusScope {
mode: bgtheme.palette.isDark ? Widgets.FastBlend.Mode.Multiply // multiply makes darker
: Widgets.FastBlend.Mode.Screen // screen (inverse multiply) makes lighter
}
+
+ Component.onCompleted: {
+ // Blur layers are effect-size dependent, so once the user starts resizing the window (hence the effect),
+ // we should either momentarily turn on live, or repeatedly call `scheduleUpdate()`. Due to the optimization,
+ // calling `scheduleUpdate()` would continuously create and release intermediate layers, which would be a
+ // really bad idea. So instead, we turn on live and after some time passes turn it off again.
+ widthChanged.connect(liveTimer, liveTimer.transientTurnOnLive)
+ heightChanged.connect(liveTimer, liveTimer.transientTurnOnLive)
+ }
+
+ Timer {
+ id: liveTimer
+
+ repeat: false
+ interval: VLCStyle.duration_humanMoment
+
+ function transientTurnOnLive() {
+ if (!blurredBackground.sourceTextureIsValid)
+ return
+ blurredBackground.live = true
+ liveTimer.restart()
+ }
+
+ onTriggered: {
+ blurredBackground.live = false
+ }
+ }
}
}
@@ -385,18 +412,46 @@ FocusScope {
cache: false
asynchronous: true
- sourceSize: Qt.size(maximumSize, maximumSize)
-
- Accessible.role: Accessible.Graphic
- Accessible.name: qsTr("Cover")
-
onTargetSourceChanged: {
cover.source = targetSource
}
onStatusChanged: {
- if (status === Image.Error)
+ if (status === Image.Ready) {
+ // This also covers source (and other parameters) change and not only initial loading
+ if (blurredBackground.sourceTextureIsValid) {
+ // Possible image switch and stale texture (especially old Qt without patch c871a52), we
+ // should wait one frame for the texture to be updated to avoid applying blur on stale one.
+ blurredBackground.scheduleUpdate(true)
+ } else {
+ // If not valid, the blur effect is going to wait appropriately until valid itself:
+ // Initial case (such as switching to player page), or switching images with recent Qt.
+ blurredBackground.scheduleUpdate(false)
+ }
+ } else if (status === Image.Error) {
cover.source = VLCStyle.noArtAlbumCover
+ }
+ }
+
+ sourceSize: Qt.size(maximumSize, maximumSize)
+
+ Accessible.role: Accessible.Graphic
+ Accessible.name: qsTr("Cover")
+
+ Component.onCompleted: {
+ // After the update on source change, there can be another update when the mipmaps are generated.
+ // We intentionally do not wait for this, initially using non-mipmapped source should be okay. As
+ // the user should not be greeted with a black background until the mipmaps are ready, let alone
+ // the possibility of knowing if the mipmaps are actually going to be ready as expected.
+ // If the texture is not valid yet (which is signalled the latest), blur effect is going to queue
+ // an update itself similar to the case when the source itself changes, so we do not check validity
+ // of the texture here.
+ blurredBackground.sourceTextureProviderObserver.hasMipmapsChanged.connect(blurredBackground,
+ (hasMipmaps /*: bool */) => {
+ if (hasMipmaps) {
+ blurredBackground.scheduleUpdate()
+ }
+ })
}
Widgets.RoundedRectangleShadow {
=====================================
modules/gui/qt/widgets/qml/DualKawaseBlur.qml
=====================================
@@ -41,10 +41,12 @@ Item {
property int configuration: DualKawaseBlur.Configuration.FourPass
- // NOTE: This property is an optimization hint. When it is false, the result
- // may be cached, and the intermediate buffers for the blur passes may
- // be released.
- // TODO: This is pending implementation.
+ // NOTE: This property is also an optimization hint. When it is false, the
+ // intermediate buffers for the blur passes may be released (only
+ // the two intermediate layers in four pass mode, we must have one
+ // layer regardless of the mode, so optimization-wise it has no
+ // benefit in two pass mode thus should be used solely as behavior
+ // instead):
property bool live: true
// Do not hesitate to use an odd number for the radius, there is virtually
@@ -56,7 +58,7 @@ Item {
// used even if it is set false here. For that reason, it should not be
// necessary to check for opacity (well, accumulated opacity can not be
// checked directly in QML anyway).
- property bool blending: (!ds1SourceObserver.isValid || ds1SourceObserver.hasAlphaChannel)
+ property bool blending: (!sourceTextureIsValid || sourceTextureProviderObserver.hasAlphaChannel)
// source must be a texture provider item. Some items such as `Image` and
// `ShaderEffectSource` are inherently texture provider. Other items needs
@@ -72,6 +74,62 @@ Item {
// `QSGTextureView` can also be used instead of sub-texturing here.
property rect sourceRect
+ property alias sourceTextureProviderObserver: ds1SourceObserver // for accessory
+
+ readonly property bool sourceTextureIsValid: sourceTextureProviderObserver.isValid
+
+ onSourceTextureIsValidChanged: {
+ if (root.sourceTextureIsValid) {
+ if (root._queuedScheduledUpdate) {
+ root._queuedScheduledUpdate = false
+
+ // Normally it should be fine to call `scheduleUpdate()` directly for
+ // the initial layer, even though the subsequent layers must be chained
+ // for update scheduling regardless, but old Qt seems to want it:
+ root.scheduleUpdate(true)
+ }
+ }
+ }
+
+ property var /*QtWindow*/ _window: null // captured window used for chaining through `afterAnimating()`
+
+ property bool _queuedScheduledUpdate: false
+
+ function scheduleUpdate(onNextAfterAnimating /* : bool */ = false) {
+ if (live)
+ return // no-op
+
+ if (!root.sourceTextureIsValid) {
+ root._queuedScheduledUpdate = true // if source texture is not valid, delay the update until valid
+ return
+ }
+
+ if (root._window) {
+ // One possible case for this is that the mipmaps for the source texture were generated too fast, and
+ // the consumer wants to update the blur to make use of the mipmaps before the blur finished chained
+ // updates for the previous source texture which is the non-mipmapped version of the same texture.
+ console.debug(root, "scheduleUpdate(): There is an already ongoing chained update, re-scheduling...")
+ root._queuedScheduledUpdate = true
+ return
+ }
+
+ root._window = root.Window.window
+ if (onNextAfterAnimating) {
+ root._window.afterAnimating.connect(ds1layer, ds1layer.scheduleChainedUpdate)
+ } else {
+ ds1layer.scheduleChainedUpdate()
+ }
+ }
+
+ onLiveChanged: {
+ if (live) {
+ ds1layer.parent = root
+ ds2layer.inhibitParent = false
+ } else {
+ root.scheduleUpdate(false) // this triggers releasing intermediate layers (when applicable)
+ }
+ }
+
// TODO: Get rid of this in favor of GLSL 1.30's `textureSize()`
Connections {
target: root.Window.window
@@ -166,6 +224,32 @@ Item {
sourceItem: ds1
visible: false
smooth: true
+
+ live: root.live
+
+ function scheduleChainedUpdate() {
+ if (!ds1layer) // context is lost, Qt bug (reproduced with 6.2)
+ return
+
+ // Common for both four and two pass mode:
+ ds1layer.parent = root
+ ds1layer.scheduleUpdate()
+
+ if (root._window) {
+ root._window.afterAnimating.disconnect(ds1layer, ds1layer.scheduleChainedUpdate)
+
+ // In four pass mode, we can release the two intermediate layers:
+ if (root.configuration === DualKawaseBlur.Configuration.FourPass) {
+ // Scheduling update must be done sequentially for each layer in
+ // a chain. It seems that each layer needs one frame for it to be
+ // used as a source in another layer, so we can not schedule
+ // update for each layer at the same time:
+ root._window.afterAnimating.connect(ds2layer, ds2layer.scheduleChainedUpdate)
+ } else {
+ root._window = null
+ }
+ }
+ }
}
ShaderEffect {
@@ -175,7 +259,8 @@ Item {
width: ds1.width / 2
height: ds1.height / 2
- readonly property Item source: ds1layer
+ // Qt uses reference counting, otherwise ds1layer may not be released, even if it has no parent (see `QQuickItemPrivate::derefWindow()`):
+ readonly property Item source: ((root.configuration === DualKawaseBlur.Configuration.TwoPass) || !ds1layer.parent) ? null : ds1layer
property rect normalRect // not necessary here, added because of the warning
readonly property int radius: root.radius
@@ -193,7 +278,7 @@ Item {
visible: false
- fragmentShader: "qrc:///shaders/DualKawaseBlur_downsample.frag.qsb"
+ fragmentShader: source ? "qrc:///shaders/DualKawaseBlur_downsample.frag.qsb" : "" // to prevent warning if source becomes null
supportsAtlasTextures: true
@@ -208,9 +293,27 @@ Item {
// never visible and was never used as texture provider, it should have never allocated
// resources to begin with.
sourceItem: (root.configuration === DualKawaseBlur.Configuration.FourPass) ? ds2 : null
+ parent: (!inhibitParent && sourceItem) ? root : null // this seems necessary to release resources even if sourceItem becomes null (non-live case)
visible: false
smooth: true
+
+ live: root.live
+
+ property bool inhibitParent: false
+
+ function scheduleChainedUpdate() {
+ if (!ds2layer) // context is lost, Qt bug (reproduced with 6.2)
+ return
+
+ ds2layer.inhibitParent = false
+ ds2layer.scheduleUpdate()
+
+ if (root._window) {
+ root._window.afterAnimating.disconnect(ds2layer, ds2layer.scheduleChainedUpdate)
+ root._window.afterAnimating.connect(us1layer, us1layer.scheduleChainedUpdate)
+ }
+ }
}
ShaderEffect {
@@ -219,7 +322,8 @@ Item {
width: ds2.width * 2
height: ds2.height * 2
- readonly property Item source: ds2layer
+ // Qt uses reference counting, otherwise ds2layer may not be released, even if it has no parent (see `QQuickItemPrivate::derefWindow()`):
+ readonly property Item source: ((root.configuration === DualKawaseBlur.Configuration.TwoPass) || !ds2layer.parent) ? null : ds2layer
property rect normalRect // not necessary here, added because of the warning
readonly property int radius: root.radius
@@ -237,7 +341,7 @@ Item {
visible: false
- fragmentShader: "qrc:///shaders/DualKawaseBlur_upsample.frag.qsb"
+ fragmentShader: source ? "qrc:///shaders/DualKawaseBlur_upsample.frag.qsb" : "" // to prevent warning if source becomes null
supportsAtlasTextures: true
@@ -252,9 +356,47 @@ Item {
// never visible and was never used as texture provider, it should have never allocated
// resources to begin with.
sourceItem: (root.configuration === DualKawaseBlur.Configuration.FourPass) ? us1 : null
+ parent: sourceItem ? root : null // this seems necessary to release resources even if sourceItem becomes null (non-live case)
visible: false
smooth: true
+
+ live: root.live
+
+ function scheduleChainedUpdate() {
+ if (!us1layer) // context is lost, Qt bug (reproduced with 6.2)
+ return
+
+ us1layer.scheduleUpdate()
+
+ if (root._window) {
+ root._window.afterAnimating.disconnect(us1layer, us1layer.scheduleChainedUpdate)
+ root._window.afterAnimating.connect(us1layer, us1layer.releaseResourcesOfIntermediateLayers)
+ }
+ }
+
+ function releaseResourcesOfIntermediateLayers() {
+ if (!ds1layer || !ds2layer) // context is lost, Qt bug (reproduced with 6.2)
+ return
+
+ // Last layer is updated, now it is time to release the intermediate buffers:
+ console.debug(root, ": releasing intermediate layers, expect the video memory consumption to drop.")
+
+ // https://doc.qt.io/qt-6/qquickitem.html#graphics-resource-handling
+ ds1layer.parent = null
+ ds2layer.inhibitParent = true
+
+ if (root._window) {
+ root._window.afterAnimating.disconnect(us1layer, us1layer.releaseResourcesOfIntermediateLayers)
+ root._window = null
+ }
+
+ if (root._queuedScheduledUpdate) {
+ // Tried calling `scheduleUpdate()` before the ongoing chained updates completed.
+ root._queuedScheduledUpdate = false
+ root.scheduleUpdate(false)
+ }
+ }
}
ShaderEffect {
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/8deaea34ae3e0fbe2914eca6abc47d1adf37dcb3...298d10606b1a476ed97b42ff96dd429f13761918
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/8deaea34ae3e0fbe2914eca6abc47d1adf37dcb3...298d10606b1a476ed97b42ff96dd429f13761918
You're receiving this email because of your account on code.videolan.org.
VideoLAN code repository instance
More information about the vlc-commits
mailing list