[vlc-commits] [Git][videolan/vlc][master] 4 commits: qt: introduce property `isDynamic` in `TextureProviderObserver`
Steve Lhomme (@robUx4)
gitlab at videolan.org
Thu May 7 17:55:05 UTC 2026
Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits:
f450da04 by Fatih Uzunoglu at 2026-05-07T13:46:35+00:00
qt: introduce property `isDynamic` in `TextureProviderObserver`
It simply reflects whether the source texture is a `QSGDynamicTexture`
derivative (such as item layer). Since it is not expected the texture
type to be changed rapidly, the change notification of this property
is done regardless of `notifyAllChanges`.
- - - - -
edd1a1e0 by Fatih Uzunoglu at 2026-05-07T13:46:35+00:00
qt: notify all changes by default without event backlogging in `TextureProviderObserver`
We can do this if we can notify the changes in appropriate
time for dynamic textures (such as item layers).
As the comment notes, with this change if the texture is
dynamic, the notifications are signaled from the thread of
the window of the source texture provider item and only
done per frame. This is guaranteed to be the GUI thread,
but not necessarily the thread of items that make use of
the observer.
This is essentially moving what we explicitly do in QML
to do implicitly in `TextureProviderObserver`. The
subsequent patch is going to remove the explicit listening
for changes per frame from where they are used in QML.
- - - - -
4e887de6 by Fatih Uzunoglu at 2026-05-07T13:46:35+00:00
qml: rely on `notifyAllChanges` for the texture provider observer in `EnhancedImageExt`
We can now do this thanks to c6f45663.
- - - - -
32d35ba1 by Fatih Uzunoglu at 2026-05-07T13:46:35+00:00
qml: rely on `notifyAllChanges` for the texture provider observer in `DualKawaseBlur`
We can now do this thanks to c6f45663.
- - - - -
4 changed files:
- modules/gui/qt/util/textureproviderobserver.cpp
- modules/gui/qt/util/textureproviderobserver.hpp
- modules/gui/qt/widgets/qml/DualKawaseBlur.qml
- modules/gui/qt/widgets/qml/EnhancedImageExt.qml
Changes:
=====================================
modules/gui/qt/util/textureproviderobserver.cpp
=====================================
@@ -31,7 +31,8 @@
TextureProviderObserver::TextureProviderObserver(QObject *parent)
: QObject{parent}
{
-
+ connect(this, &TextureProviderObserver::notifyAllChangesChanged, this, &TextureProviderObserver::adjustSampleAndNotifyConnection);
+ connect(this, &TextureProviderObserver::isDynamicChanged, this, &TextureProviderObserver::adjustSampleAndNotifyConnection);
}
void TextureProviderObserver::setSource(const QQuickItem *source, bool enforce)
@@ -39,6 +40,13 @@ void TextureProviderObserver::setSource(const QQuickItem *source, bool enforce)
if (!enforce && (m_source == source))
return;
+ if (m_window)
+ {
+ disconnect(m_window, nullptr, this, nullptr);
+ m_window = nullptr;
+ m_windowSampleAndNotifyPropertiesConnection = {}; // Is this necessary?
+ }
+
if (m_source)
{
if (Q_LIKELY(m_provider))
@@ -66,6 +74,19 @@ void TextureProviderObserver::setSource(const QQuickItem *source, bool enforce)
{
assert(m_source->isTextureProvider());
+ {
+ m_window = m_source->window();
+
+ if (m_window)
+ adjustSampleAndNotifyConnection();
+
+ connect(m_source, &QQuickItem::windowChanged, this, [this]() {
+ assert(m_source);
+ m_window = m_source->window();
+ adjustSampleAndNotifyConnection();
+ });
+ }
+
const auto init = [this, enforce]() {
const auto window = m_source->window();
assert(window);
@@ -121,9 +142,6 @@ void TextureProviderObserver::setSource(const QQuickItem *source, bool enforce)
QSize TextureProviderObserver::textureSize() const
{
- // This is likely called in the QML/GUI thread.
- // QML/GUI thread can freely block the rendering thread to the extent the time is reasonable and a
- // fraction of `1/FPS`, because it is already throttled by v-sync (so it would just throttle less).
return m_textureSize.load(std::memory_order_acquire);
}
@@ -152,6 +170,11 @@ bool TextureProviderObserver::isValid() const
return m_isValid.load(std::memory_order_acquire);
}
+bool TextureProviderObserver::isDynamic() const
+{
+ return m_textureIsDynamic.load(std::memory_order_acquire);
+}
+
qint64 TextureProviderObserver::comparisonKey() const
{
return m_comparisonKey.load(std::memory_order_acquire);
@@ -173,16 +196,20 @@ void TextureProviderObserver::updateProperties()
if (m_provider)
{
- const bool notifyAllChanges = m_notifyAllChanges.load(memoryOrder);
-
if (const auto texture = m_provider->texture())
{
+ const bool textureIsDynamic = qobject_cast<QSGDynamicTexture*>(texture);
+ if (m_textureIsDynamic.exchange(textureIsDynamic, memoryOrder) != textureIsDynamic)
+ emit isDynamicChanged(textureIsDynamic);
+
+ QSize textureSize;
+ QSize nativeTextureSize;
{
// SG and native texture size
// SG texture size:
- const auto textureSize = texture->textureSize();
- if (notifyAllChanges)
+ textureSize = texture->textureSize();
+ if (!textureIsDynamic)
{
if (m_textureSize.exchange(textureSize, memoryOrder) != textureSize)
emit textureSizeChanged(textureSize);
@@ -200,7 +227,7 @@ void TextureProviderObserver::updateProperties()
const QSize size = {static_cast<int>(textureSize.width() / ntsr.width()),
static_cast<int>(textureSize.height() / ntsr.height())};
- if (notifyAllChanges)
+ if (!textureIsDynamic)
{
if (m_nativeTextureSize.exchange(size, memoryOrder) != size)
emit nativeTextureSizeChanged(size);
@@ -209,38 +236,41 @@ void TextureProviderObserver::updateProperties()
{
m_nativeTextureSize.store(size, memoryOrder);
}
+
+ return size;
};
#ifdef RHI_HEADER_AVAILABLE
const QRhiTexture* const rhiTexture = texture->rhiTexture();
if (Q_LIKELY(rhiTexture))
{
- const QSize size = rhiTexture->pixelSize();
- if (notifyAllChanges)
+ nativeTextureSize = rhiTexture->pixelSize();
+ if (!textureIsDynamic)
{
- if (m_nativeTextureSize.exchange(size, memoryOrder) != size)
- emit nativeTextureSizeChanged(size);
+ if (m_nativeTextureSize.exchange(nativeTextureSize, memoryOrder) != nativeTextureSize)
+ emit nativeTextureSizeChanged(nativeTextureSize);
}
else
{
- m_nativeTextureSize.store(size, memoryOrder);
+ m_nativeTextureSize.store(nativeTextureSize, memoryOrder);
}
}
else
{
- legacyUpdateNativeTextureSize();
+ nativeTextureSize = legacyUpdateNativeTextureSize();
}
#else
- legacyUpdateNativeTextureSize();
+ nativeTextureSize = legacyUpdateNativeTextureSize();
#endif
}
}
+ QRectF normalizedTextureSubRect;
{
// Normal rect
- const QRectF& normalizedTextureSubRect = texture->normalizedTextureSubRect();
+ normalizedTextureSubRect = texture->normalizedTextureSubRect();
- if (notifyAllChanges)
+ if (!textureIsDynamic)
{
if (m_normalizedTextureSubRect.exchange(normalizedTextureSubRect, memoryOrder) != normalizedTextureSubRect)
emit normalizedTextureSubRectChanged(normalizedTextureSubRect);
@@ -278,11 +308,12 @@ void TextureProviderObserver::updateProperties()
if (!m_isValid.exchange(true, memoryOrder))
emit isValidChanged(true);
+ qint64 comparisonKey;
{
// Comparison key
- const qint64 comparisonKey = texture->comparisonKey();
+ comparisonKey = texture->comparisonKey();
- if (notifyAllChanges)
+ if (!textureIsDynamic)
{
if (m_comparisonKey.exchange(comparisonKey, memoryOrder) != comparisonKey) {
emit comparisonKeyChanged(comparisonKey);
@@ -294,6 +325,14 @@ void TextureProviderObserver::updateProperties()
}
}
+ if (!m_notifyAllChanges.load(memoryOrder) || !textureIsDynamic)
+ {
+ m_oldTextureSize.store(textureSize, memoryOrder);
+ m_oldNativeTextureSize.store(nativeTextureSize, memoryOrder);
+ m_oldNormalizedTextureSubRect.store(normalizedTextureSubRect, memoryOrder);
+ m_oldComparisonKey.store(comparisonKey, memoryOrder);
+ }
+
return;
}
}
@@ -303,6 +342,9 @@ void TextureProviderObserver::updateProperties()
void TextureProviderObserver::resetProperties(std::memory_order memoryOrder)
{
+ if (m_textureIsDynamic.exchange(false, memoryOrder))
+ emit isDynamicChanged(false);
+
if (m_textureSize.exchange({}, memoryOrder) != QSize())
emit textureSizeChanged({});
@@ -326,4 +368,73 @@ void TextureProviderObserver::resetProperties(std::memory_order memoryOrder)
if (m_comparisonKey.exchange(-1, memoryOrder) != -1)
emit comparisonKeyChanged(-1);
+
+ if (!m_notifyAllChanges.load(memoryOrder) || !m_textureIsDynamic.load(memoryOrder))
+ {
+ m_oldTextureSize.store({}, memoryOrder);
+ m_oldNativeTextureSize.store({}, memoryOrder);
+ m_oldNormalizedTextureSubRect.store({}, memoryOrder);
+ m_oldComparisonKey.store(-1, memoryOrder);
+ }
+}
+
+void TextureProviderObserver::adjustSampleAndNotifyConnection()
+{
+ // This is likely called in the GUI thread.
+ // QML/GUI thread can freely block the rendering thread to the extent the time is reasonable and a
+ // fraction of `1/FPS`, because it is already throttled by v-sync (so it would just throttle less).
+
+ constexpr auto memoryOrder = std::memory_order_acquire;
+ if (m_notifyAllChanges.load(memoryOrder) && m_textureIsDynamic.load(memoryOrder))
+ {
+ if (disconnect(m_windowSampleAndNotifyPropertiesConnection))
+ m_windowSampleAndNotifyPropertiesConnection = {}; // Is this necessary?
+
+ if (m_window)
+ {
+ // `QQuickWindow::afterAnimating()` is signalled from the GUI thread:
+ m_windowSampleAndNotifyPropertiesConnection = connect(m_window,
+ &QQuickWindow::afterAnimating,
+ this,
+ &TextureProviderObserver::sampleAndNotifyProperties,
+ Qt::UniqueConnection);
+ }
+ }
+ else
+ {
+ if (disconnect(m_windowSampleAndNotifyPropertiesConnection))
+ m_windowSampleAndNotifyPropertiesConnection = {}; // Is this necessary?
+ }
+}
+
+void TextureProviderObserver::sampleAndNotifyProperties()
+{
+ // This is likely called in the GUI thread.
+ // QML/GUI thread can freely block the rendering thread to the extent the time is reasonable and a
+ // fraction of `1/FPS`, because it is already throttled by v-sync (so it would just throttle less).
+ constexpr auto memoryOrder = std::memory_order_acquire;
+
+ {
+ const auto textureSize = m_textureSize.load(memoryOrder);
+ if (m_oldTextureSize.exchange(textureSize, memoryOrder) != textureSize)
+ emit textureSizeChanged(textureSize);
+ }
+
+ {
+ const auto nativeTextureSize = m_nativeTextureSize.load(memoryOrder);
+ if (m_oldNativeTextureSize.exchange(nativeTextureSize, memoryOrder) != nativeTextureSize)
+ emit nativeTextureSizeChanged(nativeTextureSize);
+ }
+
+ {
+ const auto comparisonKey = m_comparisonKey.load(memoryOrder);
+ if (m_oldComparisonKey.exchange(comparisonKey, memoryOrder) != comparisonKey)
+ emit comparisonKeyChanged(comparisonKey);
+ }
+
+ {
+ const auto normalizedTextureSubRect = m_normalizedTextureSubRect.load(memoryOrder);
+ if (m_oldNormalizedTextureSubRect.exchange(normalizedTextureSubRect, memoryOrder) != normalizedTextureSubRect)
+ emit normalizedTextureSubRectChanged(normalizedTextureSubRect);
+ }
}
=====================================
modules/gui/qt/util/textureproviderobserver.hpp
=====================================
@@ -47,11 +47,24 @@ class TextureProviderObserver : public QObject
// to not conflict with the updates, if the properties must reflect the immediately up-to-date
// texture and the properties change each frame, as otherwise it might end up in a
// "forever chase"), so by the time the sampling is done the properties should be consistent.
- // NOTE: By default these properties are not notified, as dynamic textures such as layer may
- // change rapidly (even though throttled by v-sync in the rendering thread), and if
- // such signal is connected to a receiver that lives in the GUI thread, the queued
- // invocations can easily backlog. Similar to the high precision timer, we moved
- // away from event based approach in favor of sampling based approach here.
+ // NOTE: If the source texture is a dynamic texture (such as an item layer), these properties are
+ // notified at most per frame, provided that `notifyAllChanges` is set. This is done to prevent
+ // event backlogging. If the source texture is not a dynamic texture, these are notified as soon
+ // as applicable, where in that case `notifyAllChanges` is not required to be set. The method
+ // `::sampleAndNotifyProperties()` may also be called to notify changes at an arbitrary time.
+ // It is guaranteed that notifications are emitted from the GUI thread, unless an arbitrary
+ // `sampleAndNotifyProperties()` call was made in a different thread. Note that if the texture
+ // is a dynamic texture and `notifyAllChanges` is set, the notifications will be signalled from
+ // the thread of the window of the source texture provider item, particularly during
+ // `QQuickWindow::afterAnimating()`, which is guaranteed to be GUI thread but not necessarily
+ // the thread of the items that use this observer. For that reason it is not recommended to
+ // use the observer for an item that belongs to the same window of the source texture provider
+ // item in such a case (texture is dynamic and `notifyAllChanges` is set). Also note that if
+ // the source texture is of `QSGDynamicTexture` type but is not meant to be dynamic (such as
+ // an item layer with `live: false`), it should ideally be provided as a hint to the observer
+ // observer by unsetting `notifyAllChanges`, so that it does not do sampling per frame. That
+ // being said, since event backlogging is still not possible, such benefit is minimal and
+ // should only be done if it does not hinder maintenance.
Q_PROPERTY(bool notifyAllChanges MEMBER m_notifyAllChanges NOTIFY notifyAllChangesChanged FINAL)
Q_PROPERTY(QSize textureSize READ textureSize NOTIFY textureSizeChanged FINAL) // Scene graph texture size
Q_PROPERTY(QSize nativeTextureSize READ nativeTextureSize NOTIFY nativeTextureSizeChanged FINAL) // Native texture size (e.g. for atlas textures, the atlas size)
@@ -65,6 +78,7 @@ class TextureProviderObserver : public QObject
Q_PROPERTY(bool hasMipmaps READ hasMipmaps NOTIFY hasMipmapsChanged FINAL)
Q_PROPERTY(bool isAtlasTexture READ isAtlasTexture NOTIFY isAtlasTextureChanged FINAL)
Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged FINAL) // whether a texture is provided or not
+ Q_PROPERTY(bool isDynamic READ isDynamic NOTIFY isDynamicChanged FINAL) // whether the texture is `QSGDynamicTexture`
public:
explicit TextureProviderObserver(QObject *parent = nullptr);
@@ -82,6 +96,12 @@ public:
bool hasMipmaps() const;
bool isAtlasTexture() const;
bool isValid() const;
+ bool isDynamic() const;
+
+public slots:
+ // This is mostly relevant for dynamic textures, since for static textures
+ // notification is done regardless of sampling. This method is thread-safe.
+ void sampleAndNotifyProperties();
signals:
void notifyAllChangesChanged();
@@ -93,16 +113,20 @@ signals:
void hasMipmapsChanged(bool);
void isAtlasTextureChanged(bool);
void isValidChanged(bool);
+ void isDynamicChanged(bool);
void comparisonKeyChanged(qint64);
private slots:
void updateProperties();
void resetProperties(std::memory_order memoryOrder = std::memory_order_seq_cst);
+ // This must be called from the same thread the source item lives:
+ void adjustSampleAndNotifyConnection();
private:
QPointer<const QQuickItem> m_source;
QPointer<QSGTextureProvider> m_provider;
-
+ QPointer<QQuickWindow> m_window;
+ QMetaObject::Connection m_windowSampleAndNotifyPropertiesConnection;
// It is not clear when `QSGTextureProvider::textureChanged()` can be signalled.
// If it is only signalled during SG synchronization where Qt blocks the GUI thread,
// we do not need explicit synchronization (with atomic, or mutex) here. If it can be
@@ -112,11 +136,16 @@ private:
// where the SG synchronization would not be blocking the (GUI) thread where this
// observer lives.
- std::atomic<bool> m_notifyAllChanges = false;
+ std::atomic<bool> m_textureIsDynamic = false;
+ std::atomic<bool> m_notifyAllChanges = true;
std::atomic<QSize> m_textureSize {{}}; // invalid by default
+ std::atomic<QSize> m_oldTextureSize {{}}; // invalid by default
std::atomic<QSize> m_nativeTextureSize {{}}; // invalid by default
+ std::atomic<QSize> m_oldNativeTextureSize {{}}; // invalid by default
std::atomic<qint64> m_comparisonKey {-1};
+ std::atomic<qint64> m_oldComparisonKey {-1};
std::atomic<QRectF> m_normalizedTextureSubRect {{}}; // invalid by default
+ std::atomic<QRectF> m_oldNormalizedTextureSubRect {{}}; // invalid by default
std::atomic<bool> m_hasAlphaChannel = false;
std::atomic<bool> m_hasMipmaps = false;
=====================================
modules/gui/qt/widgets/qml/DualKawaseBlur.qml
=====================================
@@ -94,6 +94,19 @@ Item {
// it is wanted or necessary anyway.
property Item source
+ /// <debug>
+ readonly property QtObject _sourceWindow: (source?.Window.window ?? null)
+ function _onWindowChanged() {
+ console.assert((_sourceWindow && root.Window.window) ? (_sourceWindow === root.Window.window) : true)
+ }
+ on_SourceWindowChanged: {
+ _onWindowChanged()
+ }
+ Window.onWindowChanged: {
+ _onWindowChanged()
+ }
+ /// </debug>
+
// Arbitrary sub-texturing (no need to be set for atlas textures):
// `QSGTextureView` can also be used instead of sub-texturing here.
property rect sourceRect
@@ -135,7 +148,6 @@ Item {
: Qt.rect(0, 0, 0, 0)
property alias sourceTextureProviderObserver: ds1.tpObserver // for accessory
- sourceTextureProviderObserver.notifyAllChanges: !live
readonly property bool sourceTextureIsValid: sourceTextureProviderObserver.isValid
@@ -220,62 +232,23 @@ Item {
}
}
- // 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.
- Connections {
- target: root.Window.window
- enabled: root.visible && root.live
-
- function onAfterAnimating() {
- // Sampling point for getting the native texture sizes:
- // This is emitted from the GUI thread.
-
- // Unlike high resolution timer widget, we should not
- // need to explicitly schedule update here, because if
- // an update is necessary, it should have been scheduled
- // implicitly (due to source texture provider's signal
- // `textureChanged()`).
-
- ds1.sourceTextureSize = ds1.tpObserver.nativeTextureSize
- if (root.mode === DualKawaseBlur.Mode.FourPass) {
- ds2.sourceTextureSize = ds2.tpObserver.nativeTextureSize
- us1.sourceTextureSize = us1.tpObserver.nativeTextureSize
- }
- us2.sourceTextureSize = us2.tpObserver.nativeTextureSize
-
- // It is not clear if `ShaderEffect` updates the uniform
- // buffer after `afterAnimating()` signal but before the
- // next frame. This is important because if `ShaderEffect`
- // updates the uniform buffer during item polish, we already
- // missed it here (`afterAnimating()` is signalled afterward).
- // However, we can call `ensurePolished()` slot to ask for
- // re-polish, which in case the `ShaderEffect` should now
- // consider the new values. If it does not exist (Qt 6.2),
- // we will rely on the next frame in worst case, which
- // should be fine as long as the size does not constantly
- // change in each frame.
- if (ds1.ensurePolished)
- {
- // No need to check for if such slot exists for each,
- // this is basically Qt version check in disguise.
- ds1.ensurePolished()
- if (root.mode === DualKawaseBlur.Mode.FourPass) {
- ds2.ensurePolished()
- us1.ensurePolished()
- }
- us2.ensurePolished()
- }
- }
- }
-
component DefaultShaderEffect : ShaderEffect {
id: shaderEffect
required property Item source
readonly property int radius: root.radius
+ // 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:
property size sourceTextureSize
+
+ Binding on sourceTextureSize {
+ when: root.live
+ value: textureProviderObserver.nativeTextureSize
+ restoreMode: Binding.RestoreNone // No need to restore
+ }
+
property rect normalRect // may not be necessary, but still needed to prevent warning
property alias tpObserver: textureProviderObserver
@@ -295,6 +268,7 @@ Item {
TextureProviderObserver {
id: textureProviderObserver
source: shaderEffect.source
+ notifyAllChanges: (root.visible && root.live)
}
}
@@ -379,6 +353,11 @@ Item {
width: ds1.width / 2
height: ds1.height / 2
+ Binding on tpObserver.notifyAllChanges {
+ when: (root.mode !== DualKawaseBlur.Mode.FourPass)
+ value: false
+ }
+
// Qt uses reference counting, otherwise ds1layer may not be released, even if it has no parent (see `QQuickItemPrivate::derefWindow()`):
source: ((root.mode === DualKawaseBlur.Mode.TwoPass) || !ds1layer.parent) ? null : ds1layer
}
@@ -419,6 +398,11 @@ Item {
width: ds2.width * 2
height: ds2.height * 2
+ Binding on tpObserver.notifyAllChanges {
+ when: (root.mode !== DualKawaseBlur.Mode.FourPass)
+ value: false
+ }
+
// Qt uses reference counting, otherwise ds2layer may not be released, even if it has no parent (see `QQuickItemPrivate::derefWindow()`):
source: ((root.mode === DualKawaseBlur.Mode.TwoPass) || !ds2layer.parent) ? null : ds2layer
}
=====================================
modules/gui/qt/widgets/qml/EnhancedImageExt.qml
=====================================
@@ -61,6 +61,19 @@ ImageExt {
return true
}
+ /// <debug>
+ readonly property QtObject _sourceWindow: (targetTextureProvider?.Window.window ?? null)
+ function _onWindowChanged() {
+ console.assert((_sourceWindow && root.Window.window) ? (_sourceWindow === root.Window.window) : true)
+ }
+ on_SourceWindowChanged: {
+ _onWindowChanged()
+ }
+ Window.onWindowChanged: {
+ _onWindowChanged()
+ }
+ /// </debug>
+
TextureProviderIndirection {
id: textureProviderIndirection
@@ -82,20 +95,12 @@ ImageExt {
textureSubRect: sourceNeedsTiling ? Qt.rect(0, 0, root.paintedWidth, root.paintedHeight) : undefined
- property size textureSize
-
- Connections {
- target: root.Window.window
- enabled: root.visible && textureProviderIndirection.source && !(textureProviderIndirection.source instanceof Image)
-
- function onAfterAnimating() {
- textureProviderIndirection.textureSize = observer.textureSize
- }
- }
+ readonly property size textureSize: observer.textureSize
TextureProviderObserver {
id: observer
source: textureProviderIndirection
+ notifyAllChanges: (root.visible && textureProviderIndirection.source)
}
}
}
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/b91572e7424a472bbf80d3ad5025fc20ca3fbd1d...32d35ba1caf94055b98465555d591abe6bfb4390
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/b91572e7424a472bbf80d3ad5025fc20ca3fbd1d...32d35ba1caf94055b98465555d591abe6bfb4390
You're receiving this email because of your account on code.videolan.org.
More information about the vlc-commits
mailing list