diff --git a/.agents/skills/logging-guidelines/SKILL.md b/.agents/skills/logging-guidelines/SKILL.md index df2210642e..1f2225e2fa 100644 --- a/.agents/skills/logging-guidelines/SKILL.md +++ b/.agents/skills/logging-guidelines/SKILL.md @@ -59,6 +59,7 @@ waylib │ ├── waylib.qtquick.surface lcWlSurface │ ├── waylib.qtquick.texture lcWlQtQuickTexture │ ├── waylib.renderer lcWlRenderer +│ ├── waylib.renderer.damage lcWlDamage │ └── waylib.qtquick.bufferitem lcWlBufferItem ├── Utils │ ├── waylib.utils.imagecapture lcWlImageCapture diff --git a/.clang-format b/.clang-format index 205d4ee172..ef021efde8 100644 --- a/.clang-format +++ b/.clang-format @@ -23,7 +23,9 @@ ColumnLimit: 100 # Disable reflow of some specific comments # qdoc comments: indentation rules are different. # Translation comments and SPDX license identifiers are also excluded. +# REUSE-IgnoreStart CommentPragmas: "^!|^:|^ SPDX-License-Identifier:" +# REUSE-IgnoreEnd PointerAlignment: Right diff --git a/.gitignore b/.gitignore index 8166c0730a..72c33c850d 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ Testing/Temporary/ # codegraph index .codegraph/ + +# generated damage playground dumps +waylib/tests/manual/damage/reports/ diff --git a/AGENTS.md b/AGENTS.md index 6af8756e94..d3f1d08879 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,10 @@ ctest --preset ci - Avoid complex lambdas; if a lambda body grows beyond a few lines, extract it into a named member function. - Keep diffs minimal: touch only task-related files and symbols. - Avoid broad refactors, unrelated renames, and unrelated reformatting. -- New source files must include an SPDX line consistent with this repo, typically `SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only`. +- New source files must include an SPDX line consistent with this repo, typically: + + `SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only`. + - Logging changes must use `.agents/skills/logging-guidelines/SKILL.md`. - If real task work shows that a repo-local skill is wrong, outdated, or incomplete, update the skill proactively instead of preserving known-bad guidance. diff --git a/debian/control b/debian/control index 6b0a3f5671..cfc796563a 100644 --- a/debian/control +++ b/debian/control @@ -50,6 +50,7 @@ Build-Depends: cmake (>= 3.25.0), libxcb-xfixes0-dev, libxcb-xinput-dev, pkg-config, + qml6-module-qtquick-controls, qml6-module-qtquick-templates, qml6-module-qtquick-effects, qt6-base-dev (>= 6.8.0), diff --git a/src/core/qml/Effects/Blur.qml b/src/core/qml/Effects/Blur.qml index 0b7f369563..f9732dd769 100644 --- a/src/core/qml/Effects/Blur.qml +++ b/src/core/qml/Effects/Blur.qml @@ -26,9 +26,25 @@ RenderBufferBlitter { property Item effectContent: contentLoader property bool contentOffscreen: false property bool effectEnabled: true + // Geometric slope cap fed to the glass shader (refractionMaxTan) and to + // damageExpansion below. Single source so the two can never desync. + property real refractionMaxTan: 3.3 z: parent.z ? parent.z - 1 : -1 anchors.fill: parent + // Worst-case backdrop margin: the full MultiEffect pyramid radius at + // blur 1.0 (blurMax scaled by blurMultiplier) plus the Liquid Glass + // refraction reach min(bezel * 0.85, thickness * 0.75), capped at 48px + // (maxDisp in liquidglass.frag). blurAmount animates, so the margin must + // not shrink with it; glass refracts even at blurAmount 0. + damageExpansion: (blurMax > 0 + ? Math.ceil(blurMax * (1 + Math.max(multiplier, 0))) + : 0) + + (glassEnabled + ? Math.max(2, Math.ceil(Math.min(Math.min(Helper.config.glassBezel * 0.85, + Helper.config.glassThickness * 0.75), + 48) * Math.max(refractionMaxTan / 2.75, 1))) + : 0) // Dispatch between Liquid Glass and traditional blur via a Loader so only // the active branch is instantiated. Toggling the DConfig key unloads one @@ -60,7 +76,7 @@ RenderBufferBlitter { saturation: blitter.saturation contentEdgePull: 0.0 contentRampEnd: 0.0 - refractionMaxTan: 3.3 + refractionMaxTan: blitter.refractionMaxTan profilePower: Helper.config.glassProfilePower innerShadow: 0.0 } diff --git a/src/core/qml/OutputMenuBar.qml b/src/core/qml/OutputMenuBar.qml index bcbb2a99df..30e59247ef 100644 --- a/src/core/qml/OutputMenuBar.qml +++ b/src/core/qml/OutputMenuBar.qml @@ -130,6 +130,24 @@ Item { color: "red" } + ToolButton { + text: "Damage" + onClicked: damageMenu.popup() + + Menu { + id: damageMenu + + MenuItem { + text: "Off" + onClicked: Helper.setDamageVisual("off") + } + MenuItem { + text: "Highlight" + onClicked: Helper.setDamageVisual("highlight") + } + } + } + ToolButton { text: "Output" diff --git a/src/plugins/lockscreen/qml/ControlAction.qml b/src/plugins/lockscreen/qml/ControlAction.qml index 10a65d34ac..859f562e26 100644 --- a/src/plugins/lockscreen/qml/ControlAction.qml +++ b/src/plugins/lockscreen/qml/ControlAction.qml @@ -40,6 +40,7 @@ RowLayout { } background: RoundBlur { + objectName: "sessionItemBlur" radius: parent.width / 2 color: Qt.rgba(1.0, 1.0, 1.0, 0.3) } @@ -143,6 +144,7 @@ RowLayout { anchors.centerIn: parent background: RoundBlur { + objectName: actionItem.iconName + "Blur" radius: parent.width / 2 color: Qt.rgba(1.0, 1.0, 1.0, 0.3) } diff --git a/src/plugins/lockscreen/qml/ShutdownButton.qml b/src/plugins/lockscreen/qml/ShutdownButton.qml index 88f961ec46..469dcc287b 100644 --- a/src/plugins/lockscreen/qml/ShutdownButton.qml +++ b/src/plugins/lockscreen/qml/ShutdownButton.qml @@ -45,6 +45,7 @@ Button { background: Item { RoundBlur { + objectName: "shutdownBtnBlur" anchors.fill: parent radius: btn.width / 2 color: root.D.ColorSelector.backgroundColor @@ -83,6 +84,7 @@ Button { background: Item { visible: root.pressed || root.hovered || root.activeFocus RoundBlur { + objectName: "shutdownLabelBlur" anchors.fill: parent radius: 6 color: root.D.ColorSelector.backgroundColor diff --git a/src/plugins/lockscreen/qml/UserInput.qml b/src/plugins/lockscreen/qml/UserInput.qml index 2285cc9829..0a9d449611 100644 --- a/src/plugins/lockscreen/qml/UserInput.qml +++ b/src/plugins/lockscreen/qml/UserInput.qml @@ -227,6 +227,7 @@ Item { } background: RoundBlur { + objectName: "passwordBlur" color: Qt.rgba(1, 1, 1, 0.4) radius: 6 } @@ -270,6 +271,7 @@ Item { enabled: passwordField.length != 0 font: D.DTK.fontManager.t8 background: RoundBlur { + objectName: "loginBtnBlur" anchors.fill: parent color: Qt.rgba(1.0, 1.0, 1.0, 0.4) radius: parent.height / 2 diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index b67020dd45..5ff2986b25 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -3366,6 +3366,35 @@ bool Helper::toggleDebugMenuBar() return ok; } +QString Helper::damageVisual() const +{ + if (!m_renderWindow) + return QStringLiteral("off"); + switch (m_renderWindow->damageVisual()) { + case WOutputRenderWindow::DamageVisual::Highlight: + return QStringLiteral("highlight"); + case WOutputRenderWindow::DamageVisual::Off: + break; + } + return QStringLiteral("off"); +} + +bool Helper::setDamageVisual(const QString &mode) +{ + if (!m_renderWindow) + return false; + const QString v = mode.trimmed().toLower(); + WOutputRenderWindow::DamageVisual visual = WOutputRenderWindow::DamageVisual::Off; + if (v == QLatin1String("highlight") || v == QLatin1String("1") + || v == QLatin1String("true") || v == QLatin1String("on")) + visual = WOutputRenderWindow::DamageVisual::Highlight; + else if (v != QLatin1String("off") && v != QLatin1String("none") + && v != QLatin1String("0") && v != QLatin1String("false")) + return false; + m_renderWindow->setDamageVisual(visual); + return true; +} + ShowDesktopInterfaceV1::State Helper::showDesktopState() const { return m_showDesktop; diff --git a/src/seat/helper.h b/src/seat/helper.h index e2c677a64d..0463c44f64 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -215,6 +215,8 @@ class Helper : public WSeatEventFilter, public WAYLIB_SERVER_NAMESPACE::WObject void setCursorPosition(const QPointF &position); bool toggleDebugMenuBar(); + Q_INVOKABLE QString damageVisual() const; + Q_INVOKABLE bool setDamageVisual(const QString &mode); ShowDesktopInterfaceV1::State showDesktopState() const; diff --git a/tests/protocols/drm/setup.cpp b/tests/protocols/drm/setup.cpp index 8a6d2ac537..eb60b50b67 100644 --- a/tests/protocols/drm/setup.cpp +++ b/tests/protocols/drm/setup.cpp @@ -8,6 +8,7 @@ #include "seat/helper.h" #include "surface/surfacewrapper.h" #include "workspace/workspace.h" +#include "output/output.h" #include #include @@ -17,6 +18,7 @@ #include #include #include +#include namespace { SurfaceWrapper *g_wrapper = nullptr; @@ -58,7 +60,10 @@ extern "C" void drm_read_render_state(void *data) return; } - renderWindow->render(); + for (auto *output : Helper::instance()->rootSurfaceContainer()->outputs()) { + if (auto *vp = output->screenViewport()) + vp->render(true); + } WTextureCapturer capturer(content); QFutureWatcher watcher; QEventLoop loop; diff --git a/tests/protocols/treeland-capture-desktop-v1/setup.cpp b/tests/protocols/treeland-capture-desktop-v1/setup.cpp index 7bf8b4e5f9..53fa10600e 100644 --- a/tests/protocols/treeland-capture-desktop-v1/setup.cpp +++ b/tests/protocols/treeland-capture-desktop-v1/setup.cpp @@ -8,10 +8,12 @@ #include "surface/surfacewrapper.h" #include "treeland-capture-desktop-v1.h" #include "workspace/workspace.h" +#include "output/output.h" #include #include #include +#include namespace { SurfaceWrapper *g_wrapper = nullptr; @@ -72,7 +74,10 @@ extern "C" void capture_desktop_select_mapped_surface(void *data) // Select the verified mapped production surface through the selector's // production selection entry point. - helper->window()->render(); + for (auto *output : helper->rootSurfaceContainer()->outputs()) { + if (auto *vp = output->screenViewport()) + vp->render(true); + } selector->selectSurface(content); g_state.hovered_mapped_content = 1; @@ -91,7 +96,10 @@ extern "C" void capture_desktop_select_mapped_surface(void *data) extern "C" void capture_desktop_render_selected_source(void *data) { auto *helper = Helper::instance(); - helper->window()->render(); + for (auto *output : helper->rootSurfaceContainer()->outputs()) { + if (auto *vp = output->screenViewport()) + vp->render(true); + } g_state.render_requested = 1; copyState(data); } diff --git a/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp b/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp index 37a4c29e9c..d8d9434f05 100644 --- a/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp +++ b/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp @@ -8,9 +8,11 @@ #include "surface/surfacewrapper.h" #include "treeland-foreign-toplevel-manager-v2.h" #include "workspace/workspace.h" +#include "output/output.h" #include #include +#include #include #include @@ -107,7 +109,10 @@ extern "C" void ftm_render_and_settle(void *) // The protocol request can be queued until the next scene-graph frame. // Render first, then inspect the wrapper so we do not miss an animation // created while processing that frame. - helper->window()->render(); + for (auto *output : helper->rootSurfaceContainer()->outputs()) { + if (auto *vp = output->screenViewport()) + vp->render(true); + } if (!g_wrapper->isAnimationRunning()) return; diff --git a/tests/protocols/wlr-foreign-toplevel-management-unstable-v1/setup.cpp b/tests/protocols/wlr-foreign-toplevel-management-unstable-v1/setup.cpp index 05efe5c938..9a6ff444c5 100644 --- a/tests/protocols/wlr-foreign-toplevel-management-unstable-v1/setup.cpp +++ b/tests/protocols/wlr-foreign-toplevel-management-unstable-v1/setup.cpp @@ -8,8 +8,10 @@ #include "surface/surfacewrapper.h" #include "wlr-foreign-toplevel-management-unstable-v1.h" #include "workspace/workspace.h" +#include "output/output.h" #include +#include #include #include @@ -76,7 +78,10 @@ extern "C" void foreign_toplevel_render(void *) if (!g_wrapper) return; auto *helper = Helper::instance(); - helper->window()->render(); + for (auto *output : helper->rootSurfaceContainer()->outputs()) { + if (auto *vp = output->screenViewport()) + vp->render(true); + } if (!g_wrapper->isAnimationRunning()) return; diff --git a/waylib/debian/control b/waylib/debian/control index 4a3e819108..a8b7464166 100644 --- a/waylib/debian/control +++ b/waylib/debian/control @@ -8,6 +8,8 @@ Build-Depends: cmake, libpixman-1-dev (>= 0.43.0), libxcb-ewmh-dev, pkg-config, + qml6-module-qtquick-controls, + qml6-module-qtquick-effects, qml6-module-qtquick-templates, qt6-base-dev-tools (>= 6.6.0), qt6-base-private-dev (>= 6.6.0), diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index 6934ab26b9..f6b25ab1a7 100644 --- a/waylib/src/server/CMakeLists.txt +++ b/waylib/src/server/CMakeLists.txt @@ -162,6 +162,15 @@ set(SOURCES qtquick/private/wqmlhelper.cpp qtquick/private/wbufferrenderer.cpp qtquick/private/wrenderbuffernode.cpp + qtquick/scenegraph/wsgbatchrenderer.cpp + qtquick/scenegraph/wsgrhivisualizer.cpp + qtquick/scenegraph/wsgcontext.cpp + qtquick/scenegraph/wsgimagenode.cpp + qtquick/scenegraph/wsgdamagetracker.cpp + qtquick/scenegraph/wsgviewport.cpp + qtquick/scenegraph/wsgdamagenode.cpp + qtquick/scenegraph/wsgdamagelog.cpp + qtquick/wregionhighlightitem.cpp ${WAYLAND_PROTOCOLS_OUTPUTDIR}/text-input-unstable-v1-protocol.c ${WAYLAND_PROTOCOLS_OUTPUTDIR}/text-input-unstable-v2-protocol.c @@ -170,6 +179,7 @@ set(SOURCES ${WAYLAND_PROTOCOLS_OUTPUTDIR}/xdg-shell-protocol.c utils/wtools.cpp + utils/wpixmanregion.cpp utils/wthreadutils.cpp utils/wimagebuffer.cpp utils/wcursorimage.cpp @@ -277,6 +287,7 @@ set(HEADERS qtquick/wsgtextureprovider.h qtquick/wtextureproviderprovider.h qtquick/wbufferitem.h + utils/wpixmanregion.h utils/wtools.h utils/wthreadutils.h @@ -356,6 +367,14 @@ set(PRIVATE_HEADERS qtquick/private/wbufferrenderer_p.h qtquick/private/wrenderbuffernode_p.h qtquick/private/wsurfaceitem_p.h + qtquick/scenegraph/wsgbatchrenderer_p.h + qtquick/scenegraph/wsgrhivisualizer_p.h + qtquick/scenegraph/wsgcontext_p.h + qtquick/scenegraph/wsgimagenode_p.h + qtquick/scenegraph/wsgdamagelog_p.h + qtquick/scenegraph/wsgdamagenode_p.h + qtquick/scenegraph/wsgdamagetracker_p.h + qtquick/scenegraph/wsgviewport_p.h ${WAYLAND_PROTOCOLS_OUTPUTDIR}/text-input-unstable-v1-protocol.h ${WAYLAND_PROTOCOLS_OUTPUTDIR}/text-input-unstable-v2-protocol.h @@ -458,6 +477,7 @@ target_include_directories(${TARGET} $ $ $ + $ $ $ $ @@ -465,6 +485,7 @@ target_include_directories(${TARGET} $ PRIVATE $ + $ ${Qt6Gui_PRIVATE_INCLUDE_DIRS} ${Qt6Quick_PRIVATE_INCLUDE_DIRS} ${Qt6EglSupport_PRIVATE_INCLUDE_DIRS} diff --git a/waylib/src/server/qtquick/private/wbufferrenderer.cpp b/waylib/src/server/qtquick/private/wbufferrenderer.cpp index 224e6cf531..3f6b3d7af6 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer.cpp +++ b/waylib/src/server/qtquick/private/wbufferrenderer.cpp @@ -10,11 +10,17 @@ #include "wtools.h" #include "wsgtextureprovider.h" #include "private/wprivateaccessor_p.h" +#include "wsgbatchrenderer_p.h" +#include "wsgdamagetracker_p.h" +#include "wsgdamagenode_p.h" +#include "woutput.h" +#include "woutputrenderwindow.h" #include #include #include +#include #include #include @@ -28,7 +34,6 @@ #include #include #endif -#include #include #include @@ -84,6 +89,7 @@ WBufferRenderer::WBufferRenderer(QQuickItem *parent) : QQuickItem(parent) , m_cacheBuffer(true) , m_hideSource(false) + , m_selfManagedRenderer(false) { // ensure graphical resources are released before scene graph is invalidated // since WBufferRenderer's ItemHasContent bit is unset @@ -101,7 +107,7 @@ WBufferRenderer::WBufferRenderer(QQuickItem *parent) WBufferRenderer::~WBufferRenderer() { cleanTextureProvider(); - resetSources(); + resetViewport(); delete m_renderHelper; } @@ -119,59 +125,76 @@ void WBufferRenderer::setOutput(WOutput *output) Q_EMIT sceneGraphChanged(); } -int WBufferRenderer::sourceCount() const +std::weak_ptr WBufferRenderer::viewport() const { - return m_sourceList.size(); + return m_viewport; } -QList WBufferRenderer::sourceList() const +QQuickItem *WBufferRenderer::source() const { - QList list; - list.reserve(m_sourceList.size()); - - for (const Data &i : std::as_const(m_sourceList)) - list.append(i.source); - - return list; + return m_source; } -void WBufferRenderer::setSourceList(QList sources, bool hideSource) +void WBufferRenderer::setViewport(std::weak_ptr viewport, QQuickItem *source, bool hideSource) { - bool changed = sources.size() != m_sourceList.size() || m_hideSource != hideSource; - if (!changed) { - for (int i = 0; i < sources.size(); ++i) { - if (sources.at(i) != m_sourceList.at(i).source) { - changed = true; - break; - } - } + const auto sameOwner = [](const auto &a, const auto &b) { + return !a.owner_before(b) && !b.owner_before(a); + }; + + if (viewport.expired()) { + resetViewport(); + return; } - if (!changed) + if (sameOwner(m_viewport, viewport) && m_source == source && m_hideSource == hideSource) return; - resetSources(); + resetViewport(); + + m_viewport = viewport; + m_source = source; m_hideSource = hideSource; - for (auto s : std::as_const(sources)) { - m_sourceList.append({s, nullptr}); + if (m_source) { + m_sourceDestroyed = connect(m_source, &QObject::destroyed, this, [this] { + m_source = nullptr; + resetViewport(); + Q_EMIT sceneGraphChanged(); + }); + QQuickItemPrivate::get(m_source)->refFromEffectItem(m_hideSource); + } + + markFullDamage(); + Q_EMIT sceneGraphChanged(); +} + +void WBufferRenderer::resetViewport() +{ + markFullDamage(); - if (isRootItem(s)) - continue; + if (m_batchRenderer) { + if (auto *tracker = m_batchRenderer->damageTracker()) { + if (auto viewport = m_viewport.lock()) + tracker->removeViewport(viewport.get()); + } + m_batchRenderer = nullptr; + } - connect(s, &QQuickItem::destroyed, this, [this] { - const int index = indexOfSource(static_cast(sender())); - Q_ASSERT(index >= 0); - // destroySource accesses m_sourceList[index], so must be called before removeAt - destroySource(index); - m_sourceList.removeAt(index); - }); + QObject::disconnect(m_sourceDestroyed); - auto d = QQuickItemPrivate::get(s); - d->refFromEffectItem(m_hideSource); + if (m_selfManagedRenderer) { + delete m_renderer; } + m_renderer = nullptr; - Q_EMIT sceneGraphChanged(); + if (m_source) { + auto priv = QQuickItemPrivate::get(m_source); + if (!priv->inDestructor) + priv->derefFromEffectItem(m_hideSource); + m_source = nullptr; + } + m_viewport.reset(); + m_hideSource = false; } bool WBufferRenderer::cacheBuffer() const @@ -219,20 +242,19 @@ void WBufferRenderer::setClearColor(const QColor &clearColor) m_clearColor = clearColor; } -QSGRenderer *WBufferRenderer::currentRenderer() const +QSGRenderer *WBufferRenderer::renderer() const { - return state.renderer; + return m_renderer; } -QSGBatchRenderer::Renderer *WBufferRenderer::currentBatchRenderer() const +WSGBatchRenderer::Renderer *WBufferRenderer::batchRenderer() const { - Q_ASSERT(state.renderer == state.batchRenderer); - return state.batchRenderer; + return m_batchRenderer; } -qreal WBufferRenderer::currentDevicePixelRatio() const +qreal WBufferRenderer::devicePixelRatio() const { - return state.devicePixelRatio; + return m_devicePixelRatio; } const QMatrix4x4 &WBufferRenderer::currentWorldTransform() const @@ -250,6 +272,16 @@ wlr_buffer *WBufferRenderer::lastBuffer() const return m_lastBuffer; } +const WDamageRegion &WBufferRenderer::lastFlushRegion() const +{ + return m_lastFlushRegion; +} + +const WDamageRegion &WBufferRenderer::lastRenderRegion() const +{ + return m_lastRenderRegion; +} + QRhiTexture *WBufferRenderer::currentRenderTarget() const { if (!state.sgRenderTarget.rt) @@ -329,16 +361,35 @@ QTransform WBufferRenderer::inputMapToOutput(const QRectF &sourceRect, const QRe return t; } -wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio, - uint32_t format, RenderFlags flags, - WGlobal::ColorContentsMode mode) +inline static QRect scaleToRect(const QRectF &s, qreal scale) { + return QRect((s.topLeft() * scale).toPoint(), + (s.size() * scale).toSize()); +} + +wlr_buffer *WBufferRenderer::render(uint32_t format, RenderFlags flags, + WGlobal::ColorContentsMode mode) { Q_ASSERT(!state.buffer); Q_ASSERT(m_output); - if (pixelSize.isEmpty()) + if (m_pixelSize.isEmpty()) return nullptr; + auto viewport = m_viewport.lock(); + if (!viewport) + return nullptr; + + auto wd = QQuickWindowPrivate::get(window()); + ensureRenderer(wd->context); + + const uint32_t previousFormat = m_swapchain ? m_swapchain->format.format : DRM_FORMAT_INVALID; + const auto *previousSwapchain = m_swapchain.get(); + // New render cycle: reset the per-cycle flush and render accumulators. + m_lastFlushRegion.clear(); + m_lastRenderRegion.clear(); + state.flags = flags; + state.colorContentsMode = mode; + Q_EMIT beforeRendering(); // configure swapchain @@ -349,9 +400,9 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix return nullptr; } - if (!m_swapchain || QSize(m_swapchain->width, m_swapchain->height) != pixelSize + if (!m_swapchain || QSize(m_swapchain->width, m_swapchain->height) != m_pixelSize || m_swapchain->format.format != renderFormat->format) { - m_swapchain.reset(wlr_swapchain_create(m_output->allocator(), pixelSize.width(), pixelSize.height(), renderFormat)); + m_swapchain.reset(wlr_swapchain_create(m_output->allocator(), m_pixelSize.width(), m_pixelSize.height(), renderFormat)); if (!m_swapchain) return nullptr; } @@ -359,7 +410,7 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix // The configure helpers take a wlr_swapchain**; hand ownership out // temporarily and take it back (possibly a new swapchain) afterwards. wlr_swapchain *sc = m_swapchain.release(); - bool ok = m_output->configureCursorSwapchain(pixelSize, format, &sc); + bool ok = m_output->configureCursorSwapchain(m_pixelSize, format, &sc); if (!ok) { m_swapchain.reset(sc); return nullptr; @@ -367,7 +418,7 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix m_swapchain.reset(sc); } else { wlr_swapchain *sc = m_swapchain.release(); - bool ok = m_output->configurePrimarySwapchain(pixelSize, format, &sc, + bool ok = m_output->configurePrimarySwapchain(m_pixelSize, format, &sc, !flags.testFlag(DontTestSwapchain)); if (!ok) { m_swapchain.reset(sc); @@ -375,6 +426,8 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix } m_swapchain.reset(sc); } + if (m_swapchain.get() != previousSwapchain || m_swapchain->format.format != previousFormat) + markFullDamage(); // TODO: Support scanout buffer of wlr_surface(from WSurfaceItem) auto buffer = wlr_swapchain_acquire(m_swapchain.get()); @@ -383,9 +436,8 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix if (!m_renderHelper) m_renderHelper = new WRenderHelper(m_output->renderer()); - m_renderHelper->setSize(pixelSize); + m_renderHelper->setSize(m_pixelSize); - auto wd = QQuickWindowPrivate::get(window()); Q_ASSERT(wd->renderControl); auto rt = m_renderHelper->acquireRenderTarget(wd->renderControl, buffer, mode); if (rt.isNull()) { @@ -396,7 +448,7 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix // For software renderer, update the dirty parts relative to the last paint device. WPixmanRegion damage; wlr_damage_ring_rotate_buffer(m_damageRing.get(), buffer, damage); - state.dirty = WTools::fromPixmanRegion(damage); + state.dirty = damage; auto rtValue = rt.rt(); auto rtd = QQuickRenderTargetPrivate::get(&rtValue); @@ -405,13 +457,11 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix if (rtd->type == QQuickRenderTargetPrivate::Type::PaintDevice) { sgRT.paintDevice = rtd->u.paintDevice; - if (devicePixelRatio != 1.0) { - state.dirty = QTransform::fromScale(1.0 / devicePixelRatio, - 1.0 / devicePixelRatio).map(state.dirty); + if (m_devicePixelRatio != 1.0) { + state.dirty = state.dirty.mappedOuter( + QTransform::fromScale(1.0 / m_devicePixelRatio, 1.0 / m_devicePixelRatio)); } } else { - state.dirty = QRegion(); - Q_ASSERT(rtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget); sgRT.rt = rtd->u.rhiRt; sgRT.cb = wd->redirect.commandBuffer; @@ -431,32 +481,17 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix state.flags = flags; state.colorContentsMode = mode; state.context = wd->context; - state.pixelSize = pixelSize; - state.devicePixelRatio = devicePixelRatio; state.buffer.reset(buffer); state.renderTarget = rt; state.sgRenderTarget = sgRT; - return buffer; -} + auto *damageTracker = m_batchRenderer ? m_batchRenderer->damageTracker() : nullptr; + const bool tracksDamage = damageTracker != nullptr; -inline static QRect scaleToRect(const QRectF &s, qreal scale) { - return QRect((s.topLeft() * scale).toPoint(), - (s.size() * scale).toSize()); -} - -void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect, const QRectF &targetRect) -{ - Q_ASSERT(state.buffer); + const QMatrix4x4 &renderMatrix = viewport->renderMatrix(); + const QRectF sourceRect = viewport->sourceRect(); + const QRectF targetRect = viewport->targetRect(); - const auto &source = m_sourceList.at(sourceIndex); - QSGRenderer *renderer = ensureRenderer(sourceIndex, state.context); - auto wd = QQuickWindowPrivate::get(window()); - - const qreal devicePixelRatio = state.devicePixelRatio; - state.renderer = renderer; - state.batchRenderer = dynamic_cast(renderer); state.worldTransform = renderMatrix; // The renderer should always receive the window's DPR (Device Pixel Ratio) // because, regardless of the DPR used for rendering, all resources within @@ -478,12 +513,13 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, // how Shape fills the QSGTexture provided by fillItem. Therefore, we need to ensure // that QSGRenderer::devicePixelRatio and QQuickWindow::effectiveDevicePixelRatio // are always consistent. Otherwise, some Items might render incorrectly. - renderer->setDevicePixelRatio(window()->effectiveDevicePixelRatio()); - renderer->setDeviceRect(QRect(QPoint(0, 0), state.pixelSize)); - renderer->setRenderTarget(state.sgRenderTarget); - const auto viewportRect = scaleToRect(targetRect, devicePixelRatio); + const QRect bufferRect(QPoint(), m_pixelSize); + m_renderer->setDevicePixelRatio(window()->effectiveDevicePixelRatio()); + m_renderer->setDeviceRect(bufferRect); + m_renderer->setRenderTarget(state.sgRenderTarget); + const auto viewportRect = scaleToRect(targetRect, m_devicePixelRatio); - auto softwareRenderer = dynamic_cast(renderer); + auto softwareRenderer = dynamic_cast(m_renderer); const bool isVulkanRhi = wd->rhi && wd->rhi->backend() == QRhi::Vulkan; { // before render if (softwareRenderer) { @@ -499,30 +535,28 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, } #endif if (!state.dirty.isEmpty()) { - W_PRIVATE_MEMBER(*softwareRenderer, QSGAbsSoftRenderer_m_dirtyRegion_tag{}) += state.dirty; - state.dirty = QRegion(); + W_PRIVATE_MEMBER(*softwareRenderer, QSGAbsSoftRenderer_m_dirtyRegion_tag{}) += state.dirty.toQRegion(); + state.dirty.clear(); } - // because software renderer don't supports viewportRect, - // so use transform to simulation. - const auto mapTransform = inputMapToOutput(sourceRect, targetRect, - state.pixelSize, state.devicePixelRatio); - if (!mapTransform.isIdentity()) - state.worldTransform = mapTransform * state.worldTransform; + // Because QSGSoftwareRenderer does not support hardware viewportRect, simulate + // it by pre-baking the logical scene-to-output mapping into worldTransform (dp). + state.worldTransform = QMatrix4x4(inputMapToOutput()) * state.worldTransform; state.worldTransform.optimize(); auto image = getImageFrom(state.renderTarget.rt()); - image->setDevicePixelRatio(devicePixelRatio); + image->setDevicePixelRatio(m_devicePixelRatio); // TODO: Should set to QSGSoftwareRenderer, but it's not support specify matrix. // If transform is changed, it will full repaint. - if (isRootItem(source.source)) { + if (isRootItem(m_source)) { auto rootTransform = QQuickItemPrivate::get(wd->contentItem)->itemNode(); if (rootTransform->matrix() != state.worldTransform) rootTransform->setMatrix(state.worldTransform); } else { auto t = state.worldTransform.toTransform(); if (t.type() > QTransform::TxTranslate) { - (image->operator QImage &()).fill(renderer->clearColor()); + if (clearColor) + (image->operator QImage &()).fill(m_renderer->clearColor()); softwareRenderer->markDirty(); } @@ -538,15 +572,15 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, if (viewportRect.isValid()) { QRect vr = viewportRect; if (flipY) - vr.moveTop(-vr.y() + state.pixelSize.height() - vr.height()); - renderer->setViewportRect(vr); + vr.moveTop(-vr.y() + m_pixelSize.height() - vr.height()); + m_renderer->setViewportRect(vr); } else { - renderer->setViewportRect(QRect(QPoint(0, 0), state.pixelSize)); + m_renderer->setViewportRect(bufferRect); } QRectF rect = sourceRect; if (!rect.isValid()) - rect = QRectF(QPointF(0, 0), QSizeF(state.pixelSize) / devicePixelRatio); + rect = QRectF(QPointF(0, 0), QSizeF(m_pixelSize) / m_devicePixelRatio); const float left = rect.x(); const float right = rect.x() + rect.width(); @@ -570,9 +604,8 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, } projectionMatrixWithNativeNDC = matrix * state.worldTransform; - renderer->setProjectionMatrix(projectionMatrix); - renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC); - + m_renderer->setProjectionMatrix(projectionMatrix); + m_renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC); } } @@ -580,7 +613,46 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, if (m_renderHelper) m_renderHelper->prepareVulkanRenderTarget(state.sgRenderTarget.cb, state.renderTarget); #endif - state.context->renderNextFrame(renderer); + + QMatrix4x4 toBuffer; + if (tracksDamage) { + toBuffer = sceneToBufferTransform(); + bool invertible = false; + const QMatrix4x4 fromBuffer = toBuffer.inverted(&invertible); + const QRect sceneOutputRect = invertible ? mapOuter(fromBuffer, bufferRect) : QRect(); + m_batchRenderer->setDamageScissorTarget(state.sgRenderTarget.rt); + + // Shared scenes were committed for all outputs by the render window. + // Independently owned sources commit above. isFull after that is a + // whole-buffer redraw, not a WPixmanRegion. + const WDamageRegion &committed = viewport->damageRegion(); + if (!invertible || committed.isFull || flags.testFlag(RenderFlag::ForceRender)) { + m_batchRenderer->setDamage(WDamageRegion(true), WDamageRegion(true), sceneOutputRect, viewport.get()); + } else { + WPixmanRegion scene = sceneOutputRect.isEmpty() + ? committed.region + : committed.region & sceneOutputRect; + + WPixmanRegion bufferAgeInScene; + if (!softwareRenderer && state.renderTarget.colorPreserved() && !state.dirty.isEmpty()) + bufferAgeInScene = state.dirty.mappedOuter(fromBuffer); + WPixmanRegion gpu = scene + bufferAgeInScene; + if (!sceneOutputRect.isEmpty()) + gpu &= sceneOutputRect; + + const bool coversOutput = + !sceneOutputRect.isEmpty() && (WPixmanRegion(sceneOutputRect) - gpu).isEmpty(); + const bool sceneCoversOutput = + !sceneOutputRect.isEmpty() && (WPixmanRegion(sceneOutputRect) - scene).isEmpty(); + + const WDamageRegion renderDamage = + coversOutput ? WDamageRegion(true) : WDamageRegion(gpu, false); + const WDamageRegion flushDamage = + sceneCoversOutput ? WDamageRegion(true) : WDamageRegion(scene, false); + m_batchRenderer->setDamage(flushDamage, renderDamage, sceneOutputRect, viewport.get()); + } + } + state.context->renderNextFrame(m_renderer); #ifdef ENABLE_VULKAN_RENDER if (m_renderHelper) m_renderHelper->finishVulkanRenderTarget(state.sgRenderTarget.cb, state.renderTarget); @@ -588,8 +660,29 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, { // after render if (!softwareRenderer) { - // TODO: get damage area from QRhi renderer - wlr_damage_ring_add_whole(m_damageRing.get()); + if (tracksDamage) { + m_lastRenderRegion = m_batchRenderer->lastRenderRegion(); + m_lastFlushRegion = m_batchRenderer->lastFlushRegion(); + + const auto bufferDamageFor = [&](const WDamageRegion &damage) { + WPixmanRegion bufferDamage = damage.isFull + ? WPixmanRegion(bufferRect) : damage.region.mappedOuter(toBuffer); + if (viewportRect.isValid()) + bufferDamage &= viewportRect; + bufferDamage &= bufferRect; + return bufferDamage; + }; + const WPixmanRegion flushRegion = bufferDamageFor(m_lastFlushRegion); + const WPixmanRegion renderRegion = bufferDamageFor(m_lastRenderRegion); + if (!flushRegion.isEmpty()) + wlr_damage_ring_add(m_damageRing.get(), flushRegion); + qCDebug(lcWlBufferRenderer) << "RHI buffer render damage" << renderRegion + << "buffer content damage" << flushRegion; + } else { + m_lastRenderRegion.isFull = true; + m_lastFlushRegion.isFull = true; + wlr_damage_ring_add_whole(m_damageRing.get()); + } // ###: maybe Qt bug? Before executing QRhi::endOffscreenFrame, we may // use the same QSGRenderer for multiple drawings. This can lead to // rendering the same content for different QSGRhiRenderTarget instances @@ -601,34 +694,46 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, if (!isVulkanRhi) wd->rhi->finish(); } else { - state.dirty = softwareRenderer->flushRegion(); - + m_lastFlushRegion.region = WPixmanRegion::fromQRegion(softwareRenderer->flushRegion()); + m_lastRenderRegion.region = WPixmanRegion::fromQRegion(softwareRenderer->flushRegion()); + const QSize logicalSize( + qMax(1, qRound(m_pixelSize.width() / qMax(qreal(1), m_devicePixelRatio))), + qMax(1, qRound(m_pixelSize.height() / qMax(qreal(1), m_devicePixelRatio)))); + const bool isFull = (!m_lastFlushRegion.region.isEmpty() + && (WPixmanRegion(QRect(QPoint(0, 0), logicalSize)) + - m_lastFlushRegion.region).isEmpty()); + if (isFull) { + m_lastFlushRegion.isFull = true; + m_lastRenderRegion.isFull = true; + } + state.dirty = WPixmanRegion::fromQRegion(softwareRenderer->flushRegion()); auto currentImage = getImageFrom(state.renderTarget.rt()); Q_ASSERT(currentImage && currentImage == softwareRenderer->renderTarget().paintDevice); - currentImage->setDevicePixelRatio(1.0); - const auto scaleTF = QTransform::fromScale(devicePixelRatio, devicePixelRatio); - const auto scaledFlushRegion = scaleTF.map(softwareRenderer->flushRegion()); - WPixmanRegion scaledFlushDamage; - bool ok = WTools::toPixmanRegion(scaledFlushRegion, scaledFlushDamage); - Q_ASSERT(ok); + const auto scaleTF = QTransform::fromScale(m_devicePixelRatio, m_devicePixelRatio); + const WPixmanRegion scaledFlushDamage = state.dirty.mappedOuter(scaleTF); { if (viewportRect.isValid()) { QRect imageRect = (currentImage->operator const QImage &()).rect(); - QRegion invalidRegion(imageRect); + WPixmanRegion invalidRegion(imageRect); invalidRegion -= viewportRect; - if (!scaledFlushRegion.isEmpty()) - invalidRegion &= scaledFlushRegion; + if (!scaledFlushDamage.isEmpty()) + invalidRegion &= scaledFlushDamage; if (!invalidRegion.isEmpty()) { QPainter pa(currentImage); - for (const auto r : std::as_const(invalidRegion)) - pa.fillRect(r, softwareRenderer->clearColor()); + int count = 0; + const pixman_box32_t *rects = invalidRegion.rectangles(&count); + for (int i = 0; i < count && rects; ++i) { + pa.fillRect(QRect(rects[i].x1, rects[i].y1, + rects[i].x2 - rects[i].x1, rects[i].y2 - rects[i].y1), + softwareRenderer->clearColor()); + } } } } - if (!isRootItem(source.source)) + if (!isRootItem(m_source)) applyTransform(softwareRenderer, state.worldTransform.inverted().toTransform()); wlr_damage_ring_add(m_damageRing.get(), scaledFlushDamage); } @@ -641,16 +746,14 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, if (shouldCacheBuffer()) wTextureProvider()->setBuffer(state.buffer.get()); + return buffer; } void WBufferRenderer::endRender() { - Q_ASSERT(state.buffer.get()); - { + if (state.buffer) { WBufferUnlockPtr buffer; buffer.swap(state.buffer); - state.renderer = nullptr; - state.batchRenderer = nullptr; m_lastBuffer = buffer.get(); } @@ -658,16 +761,20 @@ void WBufferRenderer::endRender() #ifndef QT_NO_OPENGL auto wd = QQuickWindowPrivate::get(window()); if (state.flags.testFlag(RedirectOpenGLContextDefaultFrameBufferObject) - && wd->rhi && wd->rhi->backend() == QRhi::OpenGLES2) { + && wd && wd->rhi && wd->rhi->backend() == QRhi::OpenGLES2) { auto glContext = QOpenGLContext::currentContext(); Q_ASSERT(glContext); QOpenGLContextPrivate::get(glContext)->defaultFboRedirect = GL_NONE; } #endif + if (auto viewport = m_viewport.lock()) + viewport->finishFrame(); + + state.buffer.reset(); + Q_EMIT afterRendering(); } - void WBufferRenderer::componentComplete() { QQuickItem::componentComplete(); @@ -714,13 +821,13 @@ void WBufferRenderer::invalidateSceneGraph() { if (m_textureProvider) m_textureProvider.reset(); - resetSources(); + resetViewport(); } void WBufferRenderer::releaseResources() { cleanTextureProvider(); - resetSources(); + resetViewport(); } void WBufferRenderer::cleanTextureProvider() @@ -749,69 +856,113 @@ void WBufferRenderer::cleanTextureProvider() } } -void WBufferRenderer::resetSources() -{ - for (int i = 0; i < m_sourceList.size(); ++i) { - destroySource(i); - } - m_sourceList.clear(); -} - -void WBufferRenderer::destroySource(int index) +void WBufferRenderer::ensureRenderer(QSGRenderContext *rc) { - auto &s = m_sourceList[index]; - if (isRootItem(s.source)) + if (m_renderer) return; - // Renderer of source is delay initialized in ensureRenderer. It might be null here. - if (s.renderer) { - delete s.renderer; - s.renderer = nullptr; + if (isRootItem(m_source)) { + m_renderer = QQuickWindowPrivate::get(window())->renderer; + m_selfManagedRenderer = false; + } else { + auto rootNode = WQmlHelper::getRootNode(m_source); + Q_ASSERT(rootNode); + + auto dr = qobject_cast(rc); + const bool useDepth = dr ? dr->useDepthBufferFor2D() : false; + const auto renderMode = useDepth ? QSGRendererInterface::RenderMode2D + : QSGRendererInterface::RenderMode2DNoDepthBuffer; + m_renderer = rc->createRenderer(renderMode); + m_selfManagedRenderer = true; + // refFromEffectItem provides this Qt-owned root and its dirty notifications. + // Borrow it without reparenting nodes; only the renderer and its separate + // damage tree belong to this source. + m_renderer->setRootNode(rootNode); + QObject::connect(m_renderer, &QSGRenderer::sceneGraphChanged, + this, &WBufferRenderer::sceneGraphChanged); + + m_renderer->setClearColor(m_clearColor); } - s.source->disconnect(this); - - auto d = QQuickItemPrivate::get(s.source); - if (d->inDestructor) - return; - d->derefFromEffectItem(m_hideSource); + if (m_renderer) { + m_batchRenderer = dynamic_cast(m_renderer); + if (m_batchRenderer) { + if (auto *tracker = m_batchRenderer->damageTracker()) { + if (auto viewport = m_viewport.lock()) + tracker->addViewport(viewport); + } + } + } } -int WBufferRenderer::indexOfSource(QQuickItem *s) +bool WBufferRenderer::settleDamage() { - for (int i = 0; i < m_sourceList.size(); ++i) { - if (m_sourceList.at(i).source == s) { - return i; + auto viewport = m_viewport.lock(); + if (!viewport || m_pixelSize.isEmpty() || !window()) + return true; + auto *wd = QQuickWindowPrivate::get(window()); + if (!wd || !wd->context) + return true; + + ensureRenderer(wd->context); + if (auto *batch = m_batchRenderer) { + if (auto *tracker = batch->damageTracker()) { + tracker->commit(); + return viewport->isDirty(); } } - - return -1; + return true; } -QSGRenderer *WBufferRenderer::ensureRenderer(int sourceIndex, QSGRenderContext *rc) +void WBufferRenderer::setPixelSize(const QSize &size) { - Data &d = m_sourceList[sourceIndex]; - if (isRootItem(d.source)) - return QQuickWindowPrivate::get(window())->renderer; + if (m_pixelSize == size) + return; + m_pixelSize = size; + markFullDamage(); +} - if (Q_LIKELY(d.renderer)) - return d.renderer; +void WBufferRenderer::setDevicePixelRatio(qreal devicePixelRatio) +{ + if (qFuzzyCompare(m_devicePixelRatio, devicePixelRatio)) + return; + m_devicePixelRatio = devicePixelRatio; + markFullDamage(); + Q_EMIT devicePixelRatioChanged(); +} - auto rootNode = WQmlHelper::getRootNode(d.source); - Q_ASSERT(rootNode); +QTransform WBufferRenderer::inputMapToOutput() const +{ + auto viewport = m_viewport.lock(); + Q_ASSERT(viewport); + return inputMapToOutput(viewport->sourceRect(), viewport->targetRect(), + m_pixelSize, m_devicePixelRatio); +} - auto dr = qobject_cast(rc); - const bool useDepth = dr ? dr->useDepthBufferFor2D() : false; - const auto renderMode = useDepth ? QSGRendererInterface::RenderMode2D - : QSGRendererInterface::RenderMode2DNoDepthBuffer; - d.renderer = rc->createRenderer(renderMode); - d.renderer->setRootNode(rootNode); - QObject::connect(d.renderer, &QSGRenderer::sceneGraphChanged, - this, &WBufferRenderer::sceneGraphChanged); +QMatrix4x4 WBufferRenderer::sceneToBufferTransform(const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, + const QRectF &targetRect, + const QSize &pixelSize, + qreal devicePixelRatio) +{ + QMatrix4x4 scale; + scale.scale(devicePixelRatio, devicePixelRatio); + const QMatrix4x4 inputMap(inputMapToOutput(sourceRect, targetRect, pixelSize, devicePixelRatio)); + return scale * (inputMap * renderMatrix); +} - d.renderer->setClearColor(m_clearColor); +QMatrix4x4 WBufferRenderer::sceneToBufferTransform() const +{ + auto viewport = m_viewport.lock(); + Q_ASSERT(viewport); + return sceneToBufferTransform(viewport->renderMatrix(), viewport->sourceRect(), + viewport->targetRect(), m_pixelSize, m_devicePixelRatio); +} - return d.renderer; +void WBufferRenderer::markFullDamage() +{ + if (auto viewport = m_viewport.lock()) + viewport->markFull(); } WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/private/wbufferrenderer_p.h b/waylib/src/server/qtquick/private/wbufferrenderer_p.h index 0b10e75154..d75339dc2e 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer_p.h +++ b/waylib/src/server/qtquick/private/wbufferrenderer_p.h @@ -9,11 +9,16 @@ #include #include #include +#include +#include + +#include #include #include #include +#include #include Q_MOC_INCLUDE() @@ -21,14 +26,15 @@ Q_MOC_INCLUDE() QT_BEGIN_NAMESPACE class QSGPlainTexture; class QSGRenderContext; -namespace QSGBatchRenderer { -class Renderer; -} QT_END_NAMESPACE struct pixman_region32; WAYLIB_SERVER_BEGIN_NAMESPACE +namespace WSGBatchRenderer { +class Renderer; +} + class WSGTextureProvider; class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem { @@ -43,6 +49,7 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem DontTestSwapchain = 2, RedirectOpenGLContextDefaultFrameBufferObject = 4, UseCursorFormats = 8, + ForceRender = 16, }; Q_DECLARE_FLAGS(RenderFlags, RenderFlag) @@ -52,9 +59,17 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem WOutput *output() const; void setOutput(WOutput *output); - int sourceCount() const; - QList sourceList() const; - void setSourceList(QList sources, bool hideSource); + std::weak_ptr viewport() const; + QQuickItem *source() const; + void setViewport(std::weak_ptr viewport, QQuickItem *source = nullptr, bool hideSource = false); + + void setPixelSize(const QSize &pixelSize); + QSize pixelSize() const { return m_pixelSize; } + + void setDevicePixelRatio(qreal devicePixelRatio); + + QTransform inputMapToOutput() const; + QMatrix4x4 sceneToBufferTransform() const; bool cacheBuffer() const; void setCacheBuffer(bool newCacheBuffer); @@ -65,12 +80,18 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem QColor clearColor() const; void setClearColor(const QColor &clearColor); - QSGRenderer *currentRenderer() const; - QSGBatchRenderer::Renderer *currentBatchRenderer() const; - qreal currentDevicePixelRatio() const; + QSGRenderer *renderer() const; + WSGBatchRenderer::Renderer *batchRenderer() const; + qreal devicePixelRatio() const; const QMatrix4x4 ¤tWorldTransform() const; wlr_buffer *currentBuffer() const; wlr_buffer *lastBuffer() const; + + // Scene-logical diagnostics retained for highlight consumers. + // Reset by render(). + const WDamageRegion &lastFlushRegion() const; + const WDamageRegion &lastRenderRegion() const; + QRhiTexture *currentRenderTarget() const; bool isColorPreserved() const; const wlr_damage_ring *damageRing() const; @@ -81,7 +102,12 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem WSGTextureProvider *wTextureProvider() const; static QTransform inputMapToOutput(const QRectF &sourceRect, const QRectF &targetRect, - const QSize &pixelSize, const qreal devicePixelRatio); + const QSize &pixelSize, qreal devicePixelRatio); + static QMatrix4x4 sceneToBufferTransform(const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, + const QRectF &targetRect, + const QSize &pixelSize, + qreal devicePixelRatio); Q_SIGNALS: void sceneGraphChanged(); @@ -91,11 +117,8 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem void afterRendering(); protected: - wlr_buffer *beginRender(const QSize &pixelSize, qreal devicePixelRatio, - uint32_t format, RenderFlags flags = {}, - WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); - void render(int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect = {}, const QRectF &targetRect = {}); + wlr_buffer *render(uint32_t format, RenderFlags flags = {}, + WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); void endRender(); void componentComplete() override; @@ -119,10 +142,10 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem return nullptr == source; } - void resetSources(); - void destroySource(int index); - int indexOfSource(QQuickItem *item); - QSGRenderer *ensureRenderer(int sourceIndex, QSGRenderContext *rc); + void markFullDamage(); + void resetViewport(); + void ensureRenderer(QSGRenderContext *rc); + bool settleDamage(); WUniquePointer m_swapchain; WRenderHelper *m_renderHelper = nullptr; @@ -132,32 +155,31 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem RenderFlags flags; WGlobal::ColorContentsMode colorContentsMode = WGlobal::ColorContentsMode::DontCare; QSGRenderContext *context; - QSGRenderer *renderer; - QSGBatchRenderer::Renderer *batchRenderer; QMatrix4x4 worldTransform; - QSize pixelSize; - qreal devicePixelRatio; WBufferUnlockPtr buffer; WRenderHelper::RenderTarget renderTarget; QSGRenderTarget sgRenderTarget; - QRegion dirty; + WPixmanRegion dirty; } state; QPointer m_output; - - struct Data { - QQuickItem *source = nullptr; // Don't using QPointer, See isRootItem - QSGRenderer *renderer = nullptr; - }; - - QList m_sourceList; + QSize m_pixelSize; + qreal m_devicePixelRatio = 1.0; + std::weak_ptr m_viewport; + QPointer m_source; + QSGRenderer *m_renderer = nullptr; + WSGBatchRenderer::Renderer *m_batchRenderer = nullptr; + QMetaObject::Connection m_sourceDestroyed; WDamageRing m_damageRing; + WDamageRegion m_lastFlushRegion; + WDamageRegion m_lastRenderRegion; mutable std::unique_ptr m_textureProvider; QColor m_clearColor = Qt::transparent; QList m_cacheBufferLocker; uint m_cacheBuffer:1; uint m_hideSource:1; + uint m_selfManagedRenderer:1; }; WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/private/woutputviewport_p.h b/waylib/src/server/qtquick/private/woutputviewport_p.h index 22c3504d5f..a5f2a3207d 100644 --- a/waylib/src/server/qtquick/private/woutputviewport_p.h +++ b/waylib/src/server/qtquick/private/woutputviewport_p.h @@ -10,6 +10,7 @@ #include #include +#include #include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -65,7 +66,6 @@ class Q_DECL_HIDDEN WOutputViewportPrivate : public QQuickItemPrivate void updateImplicitSize(); void updateRenderBufferSource(); - void setExtraRenderSource(QQuickItem *source); W_DECLARE_PUBLIC(WOutputViewport) QList depends; @@ -76,7 +76,7 @@ class Q_DECL_HIDDEN WOutputViewportPrivate : public QQuickItemPrivate qreal devicePixelRatio = 1.0; WBufferRenderer *bufferRenderer = nullptr; - QPointer extraRenderSource; + std::shared_ptr viewport = std::make_shared(); QRectF sourceRect; QRectF targetRect; diff --git a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp index 34d858744f..ceb2461a35 100644 --- a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp +++ b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp @@ -12,24 +12,31 @@ #include "wqmlhelper_p.h" #include "platformplugin/types.h" #include "private/wprivateaccessor_p.h" +#include "wsgbatchrenderer_p.h" +#include "wsgdamagelog_p.h" #include #include #include #include +#include +#include +#include +#include + #include #include #include #include #include -#include #include #include #include #include #include +#include // QSGRenderer: access protected methods preprocess()/render() and private // bit-field members m_changed_emitted/m_is_rendering. @@ -69,20 +76,12 @@ static_assert(sizeof(QSGRenderer) == 432, "QSGRenderer size changed — review qsgrenderer_p.h and update the bit-field accessor"); #endif -// QSGBatchRenderer::Renderer::m_shaderManager is a private data member. -// QSGBatchRenderer::Renderer::useDepthBuffer() is a private const method. -// Access them via the explicit-instantiation template trick. -W_DECLARE_PRIVATE_MEMBER(QSGBatchRenderer_m_shaderManager_tag, - QSGBatchRenderer::Renderer, m_shaderManager, - QSGBatchRenderer::ShaderManager*); -W_DECLARE_PRIVATE_CONST_METHOD(QSGBatchRenderer_useDepthBuffer_tag, - QSGBatchRenderer::Renderer, useDepthBuffer, bool); - -static inline QSGBatchRenderer::ShaderManager *&rendererShaderManager(QSGBatchRenderer::Renderer *r) { - return W_PRIVATE_MEMBER(*r, QSGBatchRenderer_m_shaderManager_tag{}); +static inline WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::ShaderManager * +rendererShaderManager(WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::Renderer *r) { + return r ? r->shaderManager() : nullptr; } -static inline bool rendererUseDepthBuffer(const QSGBatchRenderer::Renderer *r) { - return W_PRIVATE_CALL(*r, QSGBatchRenderer_useDepthBuffer_tag{}); +static inline bool rendererUseDepthBuffer(const WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::Renderer *r) { + return r && r->usesDepthBuffer(); } static inline void rendererPreprocess(QSGRenderer *r) { W_PRIVATE_CALL(*r, QSGRenderer_preprocess_tag{}); @@ -248,7 +247,10 @@ class Q_DECL_HIDDEN DataManager : public DataManagerBase } } + // Recycle only idle entries so an in-use resource is not aliased. for (auto data : std::as_const(dataList)) { + if (data->released == 0) + continue; if (get()->check(data->data, std::forward(keys)...)) { data->released = 0; return data; @@ -359,6 +361,24 @@ struct WlrAndRhiTexture { class Q_DECL_HIDDEN RhiTextureManager : public DataManager { Q_OBJECT +public: + WlrAndRhiTexture *ensurePersistent(WlrAndRhiTexture *current, QRhiTexture::Format format, + uint32_t drmFormat, uint64_t drmModifier, const QSize &size) + { + if (current && check(current, format, drmFormat, drmModifier, size)) + return current; + if (current) + destroy(current); + return create(format, drmFormat, drmModifier, size); + } + + static void destroyPersistent(WlrAndRhiTexture *texture) + { + if (texture) + destroy(texture); + } + +private: friend class DataManager; @@ -475,7 +495,7 @@ class Q_DECL_HIDDEN RhiManager : public DataManager do { if (forceDepthTest && Q_LIKELY(isBatchRenderer)) { - auto batchRenderer = static_cast(renderer); + auto batchRenderer = static_cast(renderer); if (auto sm = rendererShaderManager(batchRenderer)) { if (Q_LIKELY(!sm->pipelineCache.isEmpty())) { QVector> tmp; @@ -539,7 +559,12 @@ class Q_DECL_HIDDEN RhiManager : public DataManager // renderer is using depth test, and the other QSGRenderer is not finished render. // For an example: RhiNode to render its content nodes on an exists renderTarget. renderer = context->createRenderer(QSGRendererInterface::RenderMode2DNoDepthBuffer); - isBatchRenderer = dynamic_cast(renderer); + auto *batch = dynamic_cast(renderer); + isBatchRenderer = batch; + // This renderer only draws a small content/rotate tree into a known + // target. Compositor damage belongs on the output pass. + if (batch) + batch->setDamageMode(WSGBatchRenderer::Renderer::DamageMode::Off); } ~RhiManager() override { @@ -610,6 +635,18 @@ static QSizeF mapSize(const QRectF &source, const QMatrix4x4 &matrix) return size; } +// updateClip() in the batch renderer only turns a rectangular clip into a +// scissor for axis-aligned or 90-degree transforms; anything else falls back +// to stencil, which a color-preserving capture target cannot provide. + static bool isScissorSafeTransform(const QMatrix4x4 &m) + { + if (!qFuzzyIsNull(m(3, 0)) || !qFuzzyIsNull(m(3, 1))) + return false; + const bool aligned = qFuzzyIsNull(m(0, 1)) && qFuzzyIsNull(m(1, 0)); + const bool quarterTurn = qFuzzyIsNull(m(0, 0)) && qFuzzyIsNull(m(1, 1)); + return aligned || quarterTurn; + } + inline static void restoreChildNodesTo(QSGNode *node, QSGNode *realParent) { Q_ASSERT(realParent->firstChild() == node->firstChild()); Q_ASSERT(realParent->lastChild() == node->lastChild()); @@ -641,6 +678,12 @@ inline static void overrideChildNodesTo(QSGNode *node, QSGNode *newParent) { } class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { + struct ViewportCapture { + QPointer renderer; + WlrAndRhiTexture *texture = nullptr; + bool copiedFromCompositor = false; + }; + public: RhiNode(QQuickItem *item) : WRenderBufferNode(item, new Texture) @@ -677,6 +720,14 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { return DepthAwareRendering; } + bool hasCompositorCaptureImpl() const override { + const ViewportCapture *cap = captureFor(currentOutputRenderer()); + if (!cap || !cap->copiedFromCompositor) + return false; + const auto tex = cap->texture; + return tex && tex->rhiTexture; + } + void releaseResources() override { destroy(); } @@ -694,9 +745,9 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { inline WBufferRenderer *maybeBufferRenderer() const { const auto currentRenderer = renderWindow()->currentRenderer(); - if (!currentRenderer || !currentRenderer->currentRenderer()) + if (!currentRenderer || !currentRenderer->renderer()) return nullptr; - return renderTarget() == currentRenderer->currentRenderer()->renderTarget().rt + return renderTarget() == currentRenderer->renderer()->renderTarget().rt ? currentRenderer : nullptr; } @@ -712,49 +763,66 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { auto window = renderWindow(); auto ct = currentRenderTexture(); if (!ct) { - reset(); + // Nested QSGLayer / window FBO has no compositor texture. + // Drop the active capture so render() does not assert on a + // leftover texture pointer. + texture = nullptr; + if (!hasAnyCopiedCapture()) + reset(); return; } auto buffer = WRenderHelper::lookupBuffer(ct); if (!buffer) { - reset(); + if (!hasAnyCopiedCapture()) + reset(); return; } wlr_dmabuf_attributes attribs; if (!wlr_buffer_get_dmabuf(buffer, &attribs)) { - reset(); + if (!hasAnyCopiedCapture()) + reset(); return; } const auto currentRenderer = maybeBufferRenderer(); + WBufferRenderer *output = currentOutputRenderer(); // TODO: Apple viewport to matrix, needs get QSGRenderer renderMatrix = currentRenderer - ? currentRenderer->currentWorldTransform() * (*this->matrix()) - : *this->matrix(); + ? currentRenderer->currentWorldTransform() * m_sceneMatrix + : m_sceneMatrix; devicePixelRatio = effectiveDevicePixelRatio(); const auto oldManager = manager; manager = RhiTextureManager::resolveByOwner(manager, window); if (oldManager != manager) { sgTexture()->setTexture(nullptr); - if (oldManager) - oldManager->release(texture); - texture.reset(); + for (auto &c : captures) { + if (c.texture && oldManager) + oldManager->destroyPersistent(c.texture); + c.texture = nullptr; + c.copiedFromCompositor = false; + } + texture = nullptr; } Q_ASSERT(ct->rhi() == window->rhi()); rhi = rhi->resolveByOwner(rhi, window); Q_ASSERT(rhi); + ViewportCapture *cap = ensureCapture(output); + if (!cap) + return; + texture = cap->texture; + const bool hasRotation = renderMatrix.flags().testAnyFlags(QMatrix4x4::Rotation2D | QMatrix4x4::Rotation); QSize pixelSize; if (hasRotation) { const QSizeF size = mapSize(m_rect, renderMatrix) * devicePixelRatio; if (size.isEmpty()) { - reset(); + dropCurrentCapture(); return; } @@ -763,19 +831,23 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { if (!renderData) { renderData.reset(new RenderData); + renderData->clipNode = new QSGClipNode(); + renderData->clipNode->setFlag(QSGNode::OwnedByParent); + renderData->clipNode->setIsRectangular(true); renderData->imageNode = window->createImageNode(); renderData->imageNode->setFlag(QSGNode::OwnedByParent); renderData->imageNode->setOwnsTexture(false); renderData->texture.setOwnsTexture(false); renderData->imageNode->setTexture(&renderData->texture); - renderData->rootNode.appendChildNode(renderData->imageNode); + renderData->clipNode->appendChildNode(renderData->imageNode); + renderData->rootNode.appendChildNode(renderData->clipNode); } } else { renderData.reset(); QSizeF size = renderMatrix.mapRect(m_rect).size() * devicePixelRatio; if (!size.isValid()) { - reset(); + dropCurrentCapture(); return; } @@ -785,18 +857,35 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { { auto format = attribs.format; auto modifier = attribs.modifier; - texture = manager->resolve(texture, ct->format(), std::move(format), std::move(modifier), pixelSize); + WlrAndRhiTexture *previous = texture; + texture = manager->ensurePersistent(texture, ct->format(), std::move(format), + std::move(modifier), pixelSize); + if (cap) { + cap->texture = texture; + if (previous != texture) + cap->copiedFromCompositor = false; + } } - if (Q_UNLIKELY(texture.expired())) { - reset(); + if (Q_UNLIKELY(!texture)) { + dropCurrentCapture(); return; } - auto texture = this->texture.lock(); - Q_ASSERT(texture->data); + + if (sgTexture()->rhiTexture() != texture->rhiTexture) { + sgTexture()->setTexture(texture->rhiTexture); + doNotifyTextureChanged(); + } if (renderData) { - if (!renderData->rt || sgTexture()->rhiTexture() != texture->data->rhiTexture) { - QRhiTextureRenderTargetDescription rtDesc(texture->data->rhiTexture); + QRhiTexture *need = texture->rhiTexture; + QRhiTexture *have = nullptr; + if (renderData->rt) { + const auto desc = renderData->rt->description(); + if (desc.colorAttachmentCount() > 0) + have = desc.colorAttachmentAt(0)->texture(); + } + if (!renderData->rt || have != need) { + QRhiTextureRenderTargetDescription rtDesc(need); const auto flags = QRhiTextureRenderTarget::PreserveColorContents | QRhiTextureRenderTarget::PreserveDepthStencilContents; auto newRT = rhi->rhi()->newTextureRenderTarget(rtDesc, flags); @@ -819,54 +908,113 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } void render([[maybe_unused]] const RenderState *state) override { - auto texture = this->texture.lock(); + auto *texture = this->texture; if (Q_UNLIKELY(!texture)) return; - auto rhiTexture = texture->data->rhiTexture; + auto rhiTexture = texture->rhiTexture; auto ct = currentRenderTexture(); Q_ASSERT(ct); - if (renderData) { - renderData->texture.setTexture(ct); - renderData->texture.setTextureSize(ct->pixelSize()); - - const QPointF sourcePos = renderMatrix.map(m_rect.topLeft()); - renderData->imageNode->setRect(QRectF(-(devicePixelRatio - 1) * sourcePos, ct->pixelSize())); - - rhi->sync(rhiTexture->pixelSize(), &renderData->rootNode, renderMatrix.inverted(), {}, nullptr, - {rhiTexture->pixelSize().width() / float(m_rect.width() * devicePixelRatio), - rhiTexture->pixelSize().height() / float(m_rect.height() * devicePixelRatio)}); - rhi->render(renderData->rt.get()); - } else { - const QPoint sourcePos = (renderMatrix.map(m_rect.topLeft()) * devicePixelRatio).toPoint(); - const QRect sourceTextureRect(sourcePos, rhiTexture->pixelSize()); - const QRect renderTargetRect(QPoint(0, 0), ct->pixelSize()); - const QRect copySourceRect = sourceTextureRect.intersected(renderTargetRect); - if (copySourceRect.isEmpty()) - return; + // Recapture only when behind-tree damage hit this quad, or we still + // have no compositor texture. Pending overlapping the blitter is a + // punch: composite the last capture, do not copy a hole. + const bool copySource = needsSourceCopy() || !hasCompositorCapture(); + if (Q_UNLIKELY(lcWlDamage().isDebugEnabled())) { + const QString how = copySource + ? QStringLiteral("draw: recapture compositor pixels under this blitter into the offscreen texture") + : QStringLiteral("draw: reuse last capture; pending punches this blitter, do not copy a hole"); + qCDebug(lcWlDamage) << WSGDamageLog::frameTag() << "blitter" << debugLabel() + << "on-screen" << WSGDamageLog::describe(QRegion(mappedQuad()), false) + << how + << "recapture-rect" << WSGDamageLog::describe(m_damageRegion, false) + << (hasCompositorCapture() + ? "already has compositor capture" + : "no compositor capture yet"); + } + if (copySource) { + // Refreshing: drop validity BEFORE copying so every failure path + // below retries next frame instead of reusing a stale cache whose + // recopy debt was already consumed. Success marks it current. + invalidateCurrentCopy(); + qCDebug(lcWlRenderBuffer) << "extra-QRhi" + << (renderData ? "rotate-blit" : "copyTexture") + << rhiTexture->pixelSize() + << "blitter" << debugLabel() + << "on-screen" << mappedQuad(); + if (renderData) { + const QPointF sourcePos = renderMatrix.map(m_rect.topLeft()); + renderData->imageNode->setRect(QRectF(-(devicePixelRatio - 1) * sourcePos, ct->pixelSize())); + + // Bound the rotate-blit to the damaged compositor area; the + // capture target preserves color so clipped pixels keep the + // last capture. Full render when unmappable or not scissor-safe. + QRect clipRect(QPoint(0, 0), ct->pixelSize()); + if (isScissorSafeTransform(renderMatrix)) { + const QRect dirty = damageSourceRect().intersected(clipRect); + if (!dirty.isEmpty()) + clipRect = dirty; + } + renderData->clipNode->setClipRect(QRectF(clipRect)); + renderData->clipNode->markDirty(QSGNode::DirtyGeometry); + + rhi->sync(rhiTexture->pixelSize(), &renderData->rootNode, renderMatrix.inverted(), {}, nullptr, + {rhiTexture->pixelSize().width() / float(m_rect.width() * devicePixelRatio), + rhiTexture->pixelSize().height() / float(m_rect.height() * devicePixelRatio)}); + if (!rhi->render(renderData->rt.get())) { + qCDebug(lcWlRenderBuffer) << "extra-QRhi rotate-blit failed" + << rhiTexture->pixelSize(); + return; + } + markCurrentCopied(); + } else { + const QPoint sourcePos = (renderMatrix.map(m_rect.topLeft()) * devicePixelRatio).toPoint(); + const QRect sourceTextureRect(sourcePos, rhiTexture->pixelSize()); + const QRect renderTargetRect(QPoint(0, 0), ct->pixelSize()); + QRect copySourceRect = sourceTextureRect.intersected(renderTargetRect); + // A tiny backdrop change must not re-upload the whole quad: + // bound the copy to the damaged scene area (buffer pixels). + // Unmappable or inconsistent damage keeps the full rect. + const QRect dirtySource = damageSourceRect().intersected(copySourceRect); + if (!dirtySource.isEmpty()) + copySourceRect = dirtySource; + if (copySourceRect.isEmpty()) { + qCDebug(lcWlRenderBuffer) << "extra-QRhi copy skipped, empty source" + << sourceTextureRect << "rt" << renderTargetRect; + return; + } - auto rhi = this->rhi->rhi(); - auto rub = rhi->nextResourceUpdateBatch(); - QRhiTextureCopyDescription desc; - desc.setPixelSize(copySourceRect.size()); - desc.setSourceTopLeft(copySourceRect.topLeft()); - desc.setDestinationTopLeft(copySourceRect.topLeft() - sourcePos); - rub->copyTexture(rhiTexture, ct, desc); + auto rhi = this->rhi->rhi(); + auto rub = rhi->nextResourceUpdateBatch(); + QRhiTextureCopyDescription desc; + desc.setPixelSize(copySourceRect.size()); + desc.setSourceTopLeft(copySourceRect.topLeft()); + desc.setDestinationTopLeft(copySourceRect.topLeft() - sourcePos); + rub->copyTexture(rhiTexture, ct, desc); + + QRhiCommandBuffer *cb = nullptr; + if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess) { + qCDebug(lcWlRenderBuffer) << "extra-QRhi beginOffscreenFrame failed" + << rhiTexture->pixelSize(); + return; + } + Q_ASSERT(cb); - QRhiCommandBuffer *cb = nullptr; - if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess) - return; - Q_ASSERT(cb); + // TODO: needs vkCmdPipelineBarrier? + cb->resourceUpdate(rub); + if (rhi->endOffscreenFrame() != QRhi::FrameOpSuccess) { + qCDebug(lcWlRenderBuffer) << "extra-QRhi endOffscreenFrame failed" + << rhiTexture->pixelSize(); + return; + } + markCurrentCopied(); + } - // TODO: needs vkCmdPipelineBarrier? - cb->resourceUpdate(rub); - rhi->endOffscreenFrame(); + if (sgTexture()->rhiTexture() != rhiTexture) + sgTexture()->setTexture(rhiTexture); + doNotifyTextureChanged(); } - if (sgTexture()->rhiTexture() != rhiTexture) - sgTexture()->setTexture(rhiTexture); - doNotifyTextureChanged(); if (contentNode) { Q_ASSERT(renderTarget()->resourceType() == QRhiResource::TextureRenderTarget); @@ -877,8 +1025,8 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { auto currentRenderer = maybeBufferRenderer(); // If the source renderer enable depth test, we should enable depth test also, // to ensure the contentNode's z order on RenderMode2DNoDepthBuffer render mode. - const bool forceDepthTest = currentRenderer && currentRenderer->currentBatchRenderer() - && rendererUseDepthBuffer(currentRenderer->currentBatchRenderer()); + const bool forceDepthTest = currentRenderer && currentRenderer->batchRenderer() + && rendererUseDepthBuffer(currentRenderer->batchRenderer()); if (clipList() || inheritedOpacity() < 1.0) { if (!node) @@ -909,7 +1057,7 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { overrideChildNodesTo(contentNode, childContainer); rhi->sync(ct->pixelSize(), &node->rootNode, {}, *projectionMatrix(), - currentRenderer ? currentRenderer->currentRenderer() : nullptr); + currentRenderer ? currentRenderer->renderer() : nullptr); qreal oldDPR; QRhiCommandBuffer *oldCB; if (rhi->preprocess(textureRT, oldDPR, oldCB)) { @@ -932,7 +1080,7 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } else { node.reset(); rhi->sync(ct->pixelSize(), contentNode, *this->matrix(), *projectionMatrix(), - currentRenderer ? currentRenderer->currentRenderer() : nullptr); + currentRenderer ? currentRenderer->renderer() : nullptr); rhi->render(textureRT, forceDepthTest); } @@ -941,16 +1089,111 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } private: + WBufferRenderer *currentOutputRenderer() const { + auto *window = renderWindow(); + return window ? window->currentRenderer() : nullptr; + } + + const ViewportCapture *captureFor(const WBufferRenderer *renderer) const { + if (!renderer) + return nullptr; + for (const auto &c : std::as_const(captures)) { + if (c.renderer == renderer) + return &c; + } + return nullptr; + } + + ViewportCapture *ensureCapture(WBufferRenderer *renderer) { + pruneCaptures(); + if (!renderer) + return nullptr; + for (auto &c : captures) { + if (c.renderer == renderer) + return &c; + } + ViewportCapture c; + c.renderer = renderer; + captures.append(c); + return &captures.last(); + } + + void pruneCaptures() { + for (int i = captures.size() - 1; i >= 0; --i) { + if (captures.at(i).renderer) + continue; + releaseCapture(captures[i]); + captures.removeAt(i); + } + } + + void releaseCapture(ViewportCapture &cap) { + cap.copiedFromCompositor = false; + if (cap.texture) { + RhiTextureManager::destroyPersistent(cap.texture); + if (texture == cap.texture) + texture = nullptr; + } + cap.texture = nullptr; + } + + void dropCurrentCapture() { + if (auto *cap = ensureCapture(currentOutputRenderer())) + releaseCapture(*cap); + texture = nullptr; + sgTexture()->setTexture(nullptr); + } + + void markCurrentCopied() { + if (auto *cap = ensureCapture(currentOutputRenderer())) + cap->copiedFromCompositor = true; + } + + void invalidateCurrentCopy() { + if (auto *cap = ensureCapture(currentOutputRenderer())) + cap->copiedFromCompositor = false; + } + + // Scene-space m_damageRegion in compositor-buffer pixels (outer cover). + // Empty when there is no damage or it cannot be mapped: the caller then + // keeps the full rect (conservative). + QRect damageSourceRect() const { + if (m_damageRegion.isEmpty()) + return { }; + bool invertible = false; + // renderMatrix maps blitter-local to output-logical, m_sceneMatrix + // maps blitter-local to scene; both are fresh (prepare() runs after + // applyFrame() in the same pass), so this maps scene to output. + const QMatrix4x4 toOutput = renderMatrix * m_sceneMatrix.inverted(&invertible); + if (!invertible) + return { }; + const WPixmanRegion dirtyOutput = m_damageRegion.mappedOuter(toOutput); + if (dirtyOutput.isEmpty()) + return { }; + const WPixmanRegion dirtyBuffer = dirtyOutput.mappedOuter( + QTransform::fromScale(devicePixelRatio, devicePixelRatio)); + return dirtyBuffer.boundingRect(); + } + + bool hasAnyCopiedCapture() const { + for (const auto &c : std::as_const(captures)) { + if (c.copiedFromCompositor && c.renderer) + return true; + } + return false; + } + void reset(bool notifyTexture = true) { if (renderData) renderData->rt.reset(); + for (auto &c : captures) + releaseCapture(c); + captures.clear(); if (!sgTexture()->rhiTexture() && notifyTexture) doNotifyTextureChanged(); sgTexture()->setTexture(nullptr); - if (!texture.expired() && manager) - manager->release(texture.lock()); - texture.reset(); + texture = nullptr; } void destroy() { @@ -958,11 +1201,12 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { renderData.reset(); node.reset(); manager = nullptr; - texture.reset(); + texture = nullptr; } DataManagerPointer manager; - std::weak_ptr texture; + WlrAndRhiTexture *texture = nullptr; + QList captures; DataManagerPointer rhi; QMatrix4x4 renderMatrix; qreal devicePixelRatio; @@ -998,7 +1242,8 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { std::unique_ptr rt; QSGRootNode rootNode; - QSGImageNode *imageNode; + QSGClipNode *clipNode = nullptr; + QSGImageNode *imageNode = nullptr; QSGPlainTexture texture; }; @@ -1058,30 +1303,6 @@ WRenderBufferNode *WRenderBufferNode::createRhiNode(QQuickItem *item) return node; } -class Q_DECL_HIDDEN QImageManager : public DataManager -{ - Q_OBJECT - - friend class DataManager; - - QImageManager(QQuickWindow *owner) - : DataManager(owner) { - Q_ASSERT(owner->findChildren(Qt::FindDirectChildrenOnly).size() == 1); - } - - static bool check(QImage *image, QImage::Format format, const QSize &size) { - return image->format() == format && image->size() == size; - } - - QImage *create(QImage::Format format, const QSize &size) { - return new QImage(size, format); - } - - static void destroy(QImage *image) { - delete image; - } -}; - class Q_DECL_HIDDEN SoftwareNode : public WRenderBufferNode { public: SoftwareNode(QQuickItem *item) @@ -1102,7 +1323,7 @@ class Q_DECL_HIDDEN SoftwareNode : public WRenderBufferNode { QImage toImage() const override { - return image.expired() ? QImage() : *image.lock()->data; + return image ? *image : QImage(); } void render([[maybe_unused]] const RenderState *state) override { @@ -1114,19 +1335,12 @@ class Q_DECL_HIDDEN SoftwareNode : public WRenderBufferNode { QSGRendererInterface::PainterResource)); Q_ASSERT(p); - // const auto currentRenderer = window->currentRenderer(); - // const auto sgRenderer = currentRenderer ? currentRenderer->currentRenderer() : nullptr; - const auto matrix = /*(sgRenderer && sgRenderer->renderTarget().paintDevice == p->device()) - ? currentRenderer->currentWorldTransform() * (*this->matrix()) :*/ *this->matrix(); - const auto oldManager = manager; - manager = QImageManager::resolveByOwner(manager, window); - - if (oldManager != manager) { - texture()->setTexture(nullptr); - if (oldManager) - oldManager->release(image); - image.reset(); - } + // Software never runs the batch-renderer applyFrame() path that + // writes m_sceneMatrix. QSGRenderNode::matrix() is the live + // item-to-root transform for this render(). + if (const QMatrix4x4 *nodeMatrix = matrix()) + m_sceneMatrix = *nodeMatrix; + const auto matrix = m_sceneMatrix; const bool hasRotation = matrix.flags().testAnyFlags(QMatrix4x4::Rotation2D | QMatrix4x4::Rotation); QSizeF size; @@ -1161,14 +1375,13 @@ class Q_DECL_HIDDEN SoftwareNode : public WRenderBufferNode { return; } - if (Q_UNLIKELY(sourceImage.isNull())) { - image = manager->resolve(image, QImage::Format_RGB30, pixelSize); - } else { - image = manager->resolve(image, sourceImage.format(), pixelSize); - } + const QImage::Format format = sourceImage.isNull() + ? QImage::Format_RGB30 + : sourceImage.format(); + if (!image || image->size() != pixelSize || image->format() != format) + image = std::make_unique(pixelSize, format); - auto image = this->image.lock(); - painter.begin(image->data); + painter.begin(image.get()); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.setCompositionMode(QPainter::CompositionMode_Source); auto transform = matrix.toTransform().inverted(); @@ -1186,7 +1399,7 @@ class Q_DECL_HIDDEN SoftwareNode : public WRenderBufferNode { painter.end(); - texture()->setImage(*image->data); + texture()->setImage(*image); // Ensuse always render on software renderer texture()->setHasAlphaChannel(true); doNotifyTextureChanged(); @@ -1201,19 +1414,15 @@ class Q_DECL_HIDDEN SoftwareNode : public WRenderBufferNode { if (!texture()->image().isNull() && notifyTexture) doNotifyTextureChanged(); texture()->setTexture(nullptr); - if (manager) - manager->release(image); image.reset(); } void destroy() { reset(false); - manager = nullptr; } friend class WRenderBufferNode; - DataManagerPointer manager; - std::weak_ptr image; + std::unique_ptr image; QPainter painter; }; @@ -1241,6 +1450,29 @@ void WRenderBufferNode::resize(const QSizeF &size) m_rect = QRectF(QPointF(0, 0), m_size); } +void WRenderBufferNode::setDamageExpansion(const QMargins &margins) +{ + const QMargins clamped(qMax(0, margins.left()), qMax(0, margins.top()), + qMax(0, margins.right()), qMax(0, margins.bottom())); + if (m_damageExpansion == clamped) + return; + m_damageExpansion = clamped; + markDirty(DirtyGeometry); +} + +void WRenderBufferNode::setDamageExpansion(int px) +{ + setDamageExpansion(QMargins(px, px, px, px)); +} + +void WRenderBufferNode::setClipDamageExpansion(bool clip) +{ + if (m_clipDamageExpansion == clip) + return; + m_clipDamageExpansion = clip; + markDirty(DirtyGeometry); +} + void WRenderBufferNode::setContentItem(QQuickItem *item) { if (m_content == item) @@ -1269,11 +1501,98 @@ qreal WRenderBufferNode::effectiveDevicePixelRatio() const auto renderer = window->currentRenderer(); if (!renderer) return window->effectiveDevicePixelRatio(); - auto sgRenderer = renderer->currentRenderer(); + auto sgRenderer = renderer->renderer(); if (!sgRenderer || sgRenderer->renderTarget().rt != renderTarget()) return window->effectiveDevicePixelRatio(); - return renderer->currentDevicePixelRatio(); + return renderer->devicePixelRatio(); +} + +QString WRenderBufferNode::debugLabel() const +{ + if (!m_item) + return QStringLiteral("(no item)"); + + static const QSet kGeneric{ + QStringLiteral("blitter"), + QStringLiteral("root"), + QStringLiteral("content"), + QStringLiteral("contentLoader"), + QStringLiteral("background"), + QStringLiteral("parent"), + }; + + QStringList names; + for (QQuickItem *it = m_item; it; it = it->parentItem()) { + if (QQmlContext *ctx = qmlContext(it)) { + const QString id = ctx->nameForObject(it); + if (!id.isEmpty() && !kGeneric.contains(id) && !names.contains(id)) + names.append(id); + } + const QString objName = it->objectName(); + if (!objName.isEmpty() && !kGeneric.contains(objName) && !names.contains(objName)) + names.append(objName); + if (names.size() >= 4) + break; + } + + QString type = QString::fromUtf8(m_item->metaObject()->className()); + const int qmlType = type.indexOf(QLatin1String("_QMLTYPE_")); + if (qmlType > 0) + type.truncate(qmlType); + const int qmlId = type.indexOf(QLatin1String("_QML_")); + if (qmlId > 0) + type.truncate(qmlId); + + const QString size = QStringLiteral("%1x%2") + .arg(qRound(m_item->width())) + .arg(qRound(m_item->height())); + if (names.isEmpty()) + return type + QLatin1Char(' ') + size; + return names.join(QLatin1Char('/')) + QLatin1Char(' ') + type + QLatin1Char(' ') + size; +} + +void WRenderBufferNode::applyFrame(const WPixmanRegion &recapture, const QRect &mapped, + const QMatrix4x4 &sceneMatrix) +{ + m_sceneMatrix = sceneMatrix; + m_mappedRect = mapped; + m_damageRegion = mapped.isValid() ? recapture : WPixmanRegion(); +} + +void WRenderBufferNode::destroyWindowDataManagers(QQuickWindow *window) +{ + if (!window) + return; + + // DataManagerBase has no Q_OBJECT. Copy the list: delete mutates children(). + const QObjectList children = window->children(); + for (QObject *child : children) { + if (dynamic_cast(child)) + delete child; + } +} + + +bool WRenderBufferNode::needsRender(const WPixmanRegion &pending, bool full) const +{ + if (!hasCompositorCapture()) + return true; + if (!m_damageRegion.isEmpty()) + return true; + if (!m_mappedRect.isValid()) + return false; + QRect sample = m_mappedRect; + if (!m_clipDamageExpansion && !m_damageExpansion.isNull()) { + const qreal sx = m_rect.width() > 0 ? m_mappedRect.width() / m_rect.width() : 1; + const qreal sy = m_rect.height() > 0 ? m_mappedRect.height() / m_rect.height() : 1; + const QMargins mapped(qCeil(m_damageExpansion.left() * sx), + qCeil(m_damageExpansion.top() * sy), + qCeil(m_damageExpansion.right() * sx), + qCeil(m_damageExpansion.bottom() * sy)); + sample = sample.marginsAdded(mapped); + } + return full || pending.intersects(sample); } WRenderBufferNode::WRenderBufferNode(QQuickItem *item, QSGTexture *texture) diff --git a/waylib/src/server/qtquick/private/wrenderbuffernode_p.h b/waylib/src/server/qtquick/private/wrenderbuffernode_p.h index 3ab513494e..2a3ca83868 100644 --- a/waylib/src/server/qtquick/private/wrenderbuffernode_p.h +++ b/waylib/src/server/qtquick/private/wrenderbuffernode_p.h @@ -8,9 +8,16 @@ #include #include #include +#include +#include +#include + +#include "wpixmanregion.h" +#include QT_BEGIN_NAMESPACE class QQuickItem; +class QQuickWindow; class QSGTexture; QT_END_NAMESPACE @@ -26,6 +33,9 @@ class WAYLIB_SERVER_EXPORT WRenderBufferNode : public QSGRenderNode { static WRenderBufferNode *createRhiNode(QQuickItem *item); static WRenderBufferNode *createSoftwareNode(QQuickItem *item); + // Drop window-parented blit managers while the imported GL context is still + // alive. Call after QQuickRenderControl::invalidate() and before deleting it. + static void destroyWindowDataManagers(QQuickWindow *window); QRectF rect() const override; RenderingFlags flags() const override; @@ -45,13 +55,45 @@ class WAYLIB_SERVER_EXPORT WRenderBufferNode : public QSGRenderNode { WOutputRenderWindow *renderWindow() const; qreal effectiveDevicePixelRatio() const; + // QML id / objectName chain plus type, for damage logs (passwordField, …). + QString debugLabel() const; + + // Scene transform for this frame, after WSGDamageTracker::commit(). + // `recapture` is this output's copy source (empty = reuse that + // viewport's last capture). Each output has its own cache texture. + void applyFrame(const WPixmanRegion &recapture, const QRect &mapped, + const QMatrix4x4 &sceneMatrix); + QRect mappedQuad() const { return m_mappedRect; } + const QMatrix4x4 &sceneMatrix() const { return m_sceneMatrix; } + bool needsSourceCopy() const { return !m_damageRegion.isEmpty(); } + bool hasCompositorCapture() const { return hasCompositorCaptureImpl(); } + // Recapture is needsSourceCopy(). Draw only when that recapture is + // pending, or the frame flush overlaps this quad (or is full). + bool needsRender(const WPixmanRegion &flush, bool full) const; + + // Blur kernel spread, in item pixels. Copied onto the damage graph + // GeometryNode (needsBackdrop) so flush accumulate can dilate. + void setDamageExpansion(const QMargins &margins); + void setDamageExpansion(int px); + QMargins damageExpansion() const { return m_damageExpansion; } + void setClipDamageExpansion(bool clip); + bool clipDamageExpansion() const { return m_clipDamageExpansion; } + protected: WRenderBufferNode(QQuickItem *item, QSGTexture *texture); + // RHI: false until the current output's pass has copied into that + // viewport's texture. Attaching an empty rhiTexture in prepare() must not count. + virtual bool hasCompositorCaptureImpl() const { return true; } QPointer m_item; QPointer m_content; QSizeF m_size; QRectF m_rect; + QMatrix4x4 m_sceneMatrix; + QRect m_mappedRect; + WPixmanRegion m_damageRegion; + QMargins m_damageExpansion; + bool m_clipDamageExpansion = true; QScopedPointer m_texture; TextureChangedNotifer m_renderCallback = nullptr; void *m_callbackData = nullptr; diff --git a/waylib/src/server/qtquick/scenegraph/README.md b/waylib/src/server/qtquick/scenegraph/README.md new file mode 100644 index 0000000000..c32000fb4d --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/README.md @@ -0,0 +1,230 @@ +# WSGBatchRenderer + +Vendored fork of Qt Quick's `QSGBatchRenderer`. + +## Source + +- Tree: `/home/zccrs/projects/qtdeclarative` +- Tag: `v6.11.1` (base; version-gated to also build against 6.8.0 and 6.12.x) +- Commit: `a02bed441965ee1f18f856352c7d5ee5ba35d795` +- Upstream files: + - `src/quick/scenegraph/coreapi/qsgbatchrenderer.cpp` + - `src/quick/scenegraph/coreapi/qsgbatchrenderer_p.h` + - `src/quick/scenegraph/coreapi/qsgrhivisualizer.cpp` + - `src/quick/scenegraph/coreapi/qsgrhivisualizer_p.h` + +## Local changes vs upstream + +- Namespace `QSGBatchRenderer` moved to `WSGBatchRenderer` under + `WAYLIB_SERVER_BEGIN_NAMESPACE` (a native waylib type, not a Qt-namespace + fork). +- File and include-guard names renamed to `wsg*`. +- `qt_sg_envInt` inlined (not exported from Qt). +- `Q_QUICK_EXPORT` dropped from `Renderer` (same DSO as waylibserver). +- `QSGNode::m_subtreeRenderableCount` read via waylib's private accessor (Qt friends `QSGBatchRenderer::Renderer` only). +- Public helpers on `Renderer`: `create()`, `usesDepthBuffer()`, `shaderManager()`. +- `QSGSamplerDescription` compare/hash/`fromTexture` copied locally (Qt does not export them). +- Version gates so one copy builds against Qt 6.8.0, 6.11.x and 6.12.x: + - 6.8.0: one-arg `fullDynamicBufferUpdateForCurrentFrame` + - 6.9.2+: `invertFrontFace()` for layer culling + - 6.12+: `QSGNodePrivate::mutabilityGroup` for batch compatibility +- `WSGContext` covers `QQuickRenderControlPrivate::sg` before the first + `QQuickRenderControl` is constructed (`WOutputRenderWindow` and + `WRenderHelper::getGraphicsApi()`). Its render context overrides + `createRenderer()` so the window renderer is created as `WSGBatchRenderer` + in `syncSceneGraph()`, instead of swapping Qt's renderer after `sync()`. + If `QSGContext::createDefaultContext()` is not a `QSGDefaultContext` + (software or another adaptation), that context is kept as `sg`. +- `WSGBatchRenderer` maps the output QSG tree onto `WSGDamageNode`s + (`Geometry` / `Transform` / `Clip`; glass is Geometry + `needsBackdrop`). + The compositor output pass runs `WSGDamageTracker::commit()`, which + computes scene-content damage and accumulates it into registered + viewports, populating each backdrop's per-output recopy debt. Each viewport + keeps its own WDamageRegion until it presents, so outputs sharing one + scene at different refresh rates never lose damage. + scissors (`sceneFlush ∪ bufferAge`). `sceneFlushRegion()` is the ring + write-back (scene-graph damage only). Glass recapture uses the backdrop's + per-output recopy debt and kernel dilation. + Nested QSGLayer / MultiEffect passes that reuse this Renderer do not + run the three-phase tracker or prepare/render blitters. + `WSGDamageClipNode` clips descendant world bounds, opaque, and damage + in world space so a ListView scroll reports the viewport. Geometry + keeps unclipped local bounds; a non-invertible transform does not + need a clip inverse. Local bounds prefer a typed rect + (`QSGImageNode` / `QSGRectangleNode` / `QSGSimpleRectNode` / + `QSGSimpleTextureNode` / QML internal rectangle and image / + nine-patch / glyph / painted-item / `QSGClipNode::clipRect`) and + fall back to a 2D vertex AABB. Clip nodes with an empty `clipRect` + use that vertex AABB. `QQuickDefaultClipNode` copies `rect()` and + `radius()`: damage still uses the AABB, opaque is the AABB minus + `ceil(radius)` corner squares. Unknown stencil clips + (`!isRectangular()` and radius 0) contribute no descendant opaque. + QML `Rectangle` fill opaque uses the same inner region when radius > 0. + `DirtyMaterial` without + `WSGImageNode` explicit damage is ignored (image-node polish must not + recopy every blitter). Detached nested `RootNode`s are ignored via + the pinned tracked root. + With `QT_LOGGING_RULES=waylib.renderer.damage.debug=true` (no extra quotes), + each compositor output frame is wrapped by `======== [fN] BEGIN ========` + and `======== [fN] END ========`. Blitter lines start with `blitter `. + `will recapture` vs `keep last capture` is the prepare decision; + `draw: recapture` copies compositor pixels under the blitter, + `draw: reuse last capture` composites the last capture because pending + punches the blitter; idle blitters are listed once at prepare end. + The extra-QRhi renderer used to draw content / rotate-blit has damage + tracking off. +- Recycled swapchain buffers `PreserveColorContents`. `DontCare` resolves + to Preserve (wlroots `LOAD`), so a buffer's RT is not flipped + Clear↔Preserve. GLES and Vulkan both follow wlr_scene: keep the + recycled pixels and redraw `flushRegion` under GPU scissors (one scissor + + draw per rect, capped like `WLR_DAMAGE_RING_MAX_RECTS`). Swapchain + buffer damage from `wlr_damage_ring_rotate_buffer` is mapped into scene + space by `WBufferRenderer` and unioned into GPU scissors only. + GPU scissors are `flush ∪ bufferAge`. It is + **not** written back to the damage ring — + `sceneFlushRegion()` is scene-graph flush only, or slots ping-pong the + same rects. Nested `QSGLayer` / `MultiEffect` offscreen RTs reuse the + same `Renderer`; damage scissors apply only while the output RT that + `WBufferRenderer` just bound is the current target. There is no blit from + the previous buffer. A fully dirty + buffer is redrawn without scissor. Damage bounds always use the current + TransformNode stack, not the last-render `QSGGeometryNode::matrix()` + (that pointer is stale during `nodeChanged` and would miss the new cursor + position). +- `WSGImageNode` (from `WSGContext::createImageNode`) carries optional + content damage on the image node itself (e.g. `WSurfaceItemContent` + + `wlr_surface_get_effective_damage`). `DirtyMaterial` on that node reports + only the mapped explicit region (empty = no content damage). Add/move/ + resize/remove still use the geometry AABB so a window moving dirties the + old and new full bounds. The same node carries `wl_surface` opaque_region + (inner-mapped into item space). Empty explicit opaque means the client + claimed no opaque pixels; do not fall back to material blending. + `QRegion::contains(QRect)` means overlap, not + coverage; a caret-sized dirty region must not collapse to a full output + flush. Cover the output with `QRegion(output).subtracted(damage).isEmpty()`. +- `WAYLIB_DAMAGE` is read by `WSGBatchRenderer::Renderer` (default `on`): + - `off` / `0` / `none`: no tracker. Draw and present like the pre-damage + batch renderer (`e848551`). `wlr_damage_ring_add_whole`. + - `commit` / `output`: track and report `sceneFlushRegion` to the + output (`wlr_damage_ring`). Draw is still fullscreen (no scissor, no + cull, blitters recopy). `flushRegion` is GPU scissors only + (`sceneFlush ∪ bufferAge`) and is not written to the ring. + - `on` / `full`: track, GPU scissor, cull, and recopy blitters by damage. + `Renderer::setDamageMode(DamageMode::Off)` forces `off` on one renderer + (nested MultiEffect / extra-QRhi). + Visual overlay is `WOutputRenderWindow::damageVisual` + (`WAYLIB_DEBUG_DAMAGE=highlight`; default off). + Per-step tracker / blitter logs are independent: + `QT_LOGGING_RULES=waylib.renderer.damage.debug=true`. + Highlight is a `WOutputLayer` created per viewport by + `WOutputRenderWindow`. Quads are pooled `QQuickRectangle`s; the layer + is sized to the overlay AABB. `OutputHelper` records `lastFlushRegion` + after each output render. Batch renderer and `WBufferRenderer` do not + own overlay state. + Nested `QSGLayer` / `grabToImage` / `MultiEffect` / blitter passes are + unaffected. + - `off` (default): layer disabled + - `highlight` (or `1` / `true`): overlay of each frame's `flushRegion` + on the dedicated layer. Current frame is red; older live frames shift + toward green. Alpha fades over 250ms. + The debug menu **Damage** entry (Ctrl+Shift+Meta+F11) switches highlight + on or off at runtime. + +## Testing damage + +`waylib/tests/unit_tests/test_wsgdamage` has two layers: + +1. Overlay / blitter tests in `main.cpp` (`WRenderBufferNode::applyFrame` + / `needsRender`, highlight fade). Graph math lives in `test_wdamagegraph`. +2. QML scene tests in `scenetest.cpp`. They install `WSGContext`, drive an + offscreen `QQuickRenderControl` window, `polishItems()`+`sync()`, then + clip the tracker's scene-wide `commit()` to the output rect, call + `Renderer::setDamage()`, and assert `lastRenderRegion`: + - add / remove / move / rotate / scale / `Translate` transform + - caret opacity blink must not cover distant buttons + - idle frames stay empty + +Failures dump the actual region (`WxH+X+Y`). To add a scenario: put the +widgets in `kSceneQml`, mutate one item, `take()`, and assert covers/avoids +the mapped scene rects. + +`test_wsgdamage_visual` covers DamageDebug overlay pixels. It starts a +headless compositor, loads `TestWindow.qml` / `DamageScene.qml` (the same +photo as `examples/test_glass`, rounded card + shadow, MultiEffect blur of +the photo, and an optional `RenderBufferBlitter` frosted-glass panel), then +for each frame: + +1. On `afterRendering`, snapshot `flushRegion` and highlight entries. + Dump the `wlr_buffer` to `QImage` only after `render()` returns + (`QQuickRenderControl::endFrame` has submitted the Vulkan commands). +2. Assert `flushRegion` covers the expected item bounds (including shadow + / blur padding) and misses a distant sentinel. +3. `grabToImage` the QML scene on a follow-up frame with highlight off + (the grab itself refs the item as an effect source and would otherwise + dirty this frame's region). +4. Replay those entries with `WSGDamageOverlay::paint` onto the grab. +5. Compare the committed buffer with the painted grab, and also compare + dump vs grab *outside* the overlay (leftover 2px border on wallpaper + fails this). + +Also covers fade frames while highlight stays on, idle compositor frames +(`OutputRenderWindow::render()` without `forceRender` — this is the path +that used to skip overlay fade when the QML scene was not dirty), and +software-cursor moves driven by `WCursor::setPosition`. `frosted_behindMoveMatchesRerender` +turns on a +`RenderBufferBlitter` over the photo (same setup as `waylib/examples/blur`) +and compares the blitter pixels after a behind-item move against a full +redraw of the same scene — not `grabToImage`, which cannot see the +compositor copy. + +`LockScene.qml` is a replica of `treeland --lockscreen` (UserInput + +session chip + SessionList). It uses Qt Quick Controls and waylib +`RenderBufferBlitter` (`RoundBlur.qml`), not DTK, so waylib-only CI +can build it. It does not import compositor plugins. Visual tests +drive it through `showLockscreen()`: + +- popup open: damage covers the popup, not fullscreen, not a + full-output-height stripe +- list scroll: damage height is the list viewport (clip), and blitter + pixels match a full redraw (no whiteboard; blur stays) +- caret blink: covers the caret, not the whole password field +- idle password blur: two frames match and do not recopy the blitter + +The session `Popup` uses `popupType: Popup.Item` and is parented to the +lock scene (`modal: false`) so it sits in the output tree rather than +`QQuickOverlay`. + +Run with: + +```bash +cmake --build build -j$(nproc) --target test_wsgdamage test_wsgdamage_visual +ctest --test-dir build --output-on-failure -R test_wsgdamage +``` + +With no extra env vars the binaries spawn child runs themselves: +`test_wsgdamage` uses `QSG_RHI_BACKEND=null`, `opengl`, then `vulkan` +(offscreen Vulkan skips the QML scene tests — no `QVulkanInstance`). +`test_wsgdamage_visual` uses `WLR_RENDERER=gles2` then `vulkan`. +Set those variables yourself to run a single backend. +Vulkan still runs region checks, but skips dump-vs-`grabToImage` +comparisons that include live `MultiEffect` (blur / drop shadow). + + +`waylib/tests/manual/damage` is a compositor playground for this damage +work. It turns on `WAYLIB_DEBUG_DAMAGE=highlight` unless the env var is +already set. Rotate, move and scale keep running; drag the opaque / alpha +/ blitter covers over them to see whether `flushRegion` follows the actor +AABB. `waylib/tests/manual/damagegraph-demo` is the same +`WSGDamageTracker` visualizer without a compositor (Debug builds only). +Its C++ (`demoscene`, overlay, visual model) is shared with +`waylib/tests/manual/damage`; only the QML window chrome differs. + +```bash +cmake --build build -j$(nproc) --target damage damagegraph-demo +./build/waylib/tests/manual/damage/damage +./build/waylib/tests/manual/damagegraph-demo/damagegraph-demo +``` + +## Upgrade + +When bumping the base Qt, three-way merge these four files against the recorded commit, then re-apply the rename, accessors, and `QT_VERSION_CHECK` gates. diff --git a/waylib/src/server/qtquick/scenegraph/wsgbatchrenderer.cpp b/waylib/src/server/qtquick/scenegraph/wsgbatchrenderer.cpp new file mode 100644 index 0000000000..870b1758e5 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgbatchrenderer.cpp @@ -0,0 +1,6114 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// Copyright (C) 2016 Jolla Ltd, author: +// Copyright (C) 2016 Robin Burchell +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgbatchrenderer_p.h" +#include "wsgrhivisualizer_p.h" +#include "wayliblogging.h" +#include "wpixmanregion.h" +#include "wrenderbuffernode_p.h" +#include "wrenderhelper.h" +#include "wsgdamagelog_p.h" +#include "wsgdamagenode_p.h" +#include "wsgdamagetracker_p.h" +#include "wsgimagenode_p.h" +#include "woutputrenderwindow.h" +#include "private/wprivateaccessor_p.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// QSGNode friends QSGBatchRenderer::Renderer, not this fork. +W_DECLARE_PRIVATE_MEMBER(WSG_QSGNode_m_subtreeRenderableCount, QSGNode, m_subtreeRenderableCount, int); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_rect, + QSGBasicInternalRectangleNode, + m_rect, + QRectF); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_color, + QSGBasicInternalRectangleNode, + m_color, + QColor); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_radius, + QSGBasicInternalRectangleNode, + m_radius, + float); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_topLeftRadius, + QSGBasicInternalRectangleNode, + m_topLeftRadius, + float); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_topRightRadius, + QSGBasicInternalRectangleNode, + m_topRightRadius, + float); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_bottomLeftRadius, + QSGBasicInternalRectangleNode, + m_bottomLeftRadius, + float); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_bottomRightRadius, + QSGBasicInternalRectangleNode, + m_bottomRightRadius, + float); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalRect_m_gradient_stops, + QSGBasicInternalRectangleNode, + m_gradient_stops, + QGradientStops); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGBasicInternalImage_m_targetRect, + QSGBasicInternalImageNode, + m_targetRect, + QRectF); +W_DECLARE_PRIVATE_MEMBER(WSG_QSGDefaultNinePatch_m_bounds, + QSGDefaultNinePatchNode, + m_bounds, + QRectF); + +QT_BEGIN_NAMESPACE + +// QSGSamplerDescription helpers live in Qt's qsgtexture.cpp without +// Q_QUICK_EXPORT, so they are not visible to this DSO. +bool operator==(const QSGSamplerDescription &a, const QSGSamplerDescription &b) noexcept +{ + return a.filtering == b.filtering + && a.mipmapFiltering == b.mipmapFiltering + && a.horizontalWrap == b.horizontalWrap + && a.verticalWrap == b.verticalWrap + && a.anisotropylevel == b.anisotropylevel; +} + +bool operator!=(const QSGSamplerDescription &a, const QSGSamplerDescription &b) noexcept +{ + return !(a == b); +} + +size_t qHash(const QSGSamplerDescription &s, size_t seed) noexcept +{ + const int f = s.filtering; + const int m = s.mipmapFiltering; + const int w = s.horizontalWrap; + const int a = s.anisotropylevel; + return (((f & 7) << 24) | ((m & 7) << 16) | ((w & 7) << 8) | (a & 7)) ^ seed; +} + +QSGSamplerDescription QSGSamplerDescription::fromTexture(QSGTexture *t) +{ + QSGSamplerDescription s; + s.filtering = t->filtering(); + s.mipmapFiltering = t->mipmapFiltering(); + s.horizontalWrap = t->horizontalWrapMode(); + s.verticalWrap = t->verticalWrapMode(); + s.anisotropylevel = t->anisotropyLevel(); + return s; +} + +QT_END_NAMESPACE + +static int qt_sg_envInt(const char *name, int defaultValue) +{ + if (Q_LIKELY(!qEnvironmentVariableIsSet(name))) + return defaultValue; + bool ok = false; + const int value = qEnvironmentVariableIntValue(name, &ok); + return ok ? value : defaultValue; +} + +WAYLIB_SERVER_BEGIN_NAMESPACE + +namespace WSGBatchRenderer { + +#define DECLARE_DEBUG_VAR(variable) \ + static bool debug_ ## variable() \ + { static bool value = qgetenv("QSG_RENDERER_DEBUG").contains(QT_STRINGIFY(variable)); return value; } +DECLARE_DEBUG_VAR(render) +DECLARE_DEBUG_VAR(build) +DECLARE_DEBUG_VAR(change) +DECLARE_DEBUG_VAR(upload) +DECLARE_DEBUG_VAR(roots) +DECLARE_DEBUG_VAR(dump) +DECLARE_DEBUG_VAR(pools) +DECLARE_DEBUG_VAR(noalpha) +DECLARE_DEBUG_VAR(noopaque) +DECLARE_DEBUG_VAR(noclip) +#undef DECLARE_DEBUG_VAR + +#define QSGNODE_TRAVERSE(NODE) for (QSGNode *child = NODE->firstChild(); child; child = child->nextSibling()) +#define SHADOWNODE_TRAVERSE(NODE) for (Node *child = NODE->firstChild(); child; child = child->sibling()) + +static inline int size_of_type(int type) +{ + static int sizes[] = { + sizeof(char), + sizeof(unsigned char), + sizeof(short), + sizeof(unsigned short), + sizeof(int), + sizeof(unsigned int), + sizeof(float), + 2, + 3, + 4, + sizeof(double) + }; + Q_ASSERT(type >= QSGGeometry::ByteType && type <= QSGGeometry::DoubleType); + return sizes[type - QSGGeometry::ByteType]; +} + +bool qsg_sort_element_increasing_order(Element *a, Element *b) { return a->order < b->order; } +bool qsg_sort_element_decreasing_order(Element *a, Element *b) { return a->order > b->order; } +bool qsg_sort_batch_increasing_order(Batch *a, Batch *b) { return a->first->order < b->first->order; } +bool qsg_sort_batch_decreasing_order(Batch *a, Batch *b) { return a->first->order > b->first->order; } + +QSGMaterial::Flag QSGMaterial_FullMatrix = (QSGMaterial::Flag) (QSGMaterial::RequiresFullMatrix & ~QSGMaterial::RequiresFullMatrixExceptTranslate); + +static bool isTranslate(const QMatrix4x4 &m) { return m.flags() <= QMatrix4x4::Translation; } +static bool isScale(const QMatrix4x4 &m) { return m.flags() <= QMatrix4x4::Scale; } +static bool is2DSafe(const QMatrix4x4 &m) { return m.flags() < QMatrix4x4::Rotation; } + +const float OPAQUE_LIMIT = 0.999f; + +const uint DYNAMIC_VERTEX_INDEX_BUFFER_THRESHOLD = 4; +const int VERTEX_BUFFER_BINDING = 0; +const int ZORDER_BUFFER_BINDING = VERTEX_BUFFER_BINDING + 1; + +const float VIEWPORT_MIN_DEPTH = 0.0f; +const float VIEWPORT_MAX_DEPTH = 1.0f; + +const quint32 DEFAULT_BUFFER_POOL_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB for m_vboPool and m_iboPool each + +template +inline Int aligned(Int v, Int byteAlign) +{ + return (v + byteAlign - 1) & ~(byteAlign - 1); +} + +QRhiVertexInputAttribute::Format qsg_vertexInputFormat(const QSGGeometry::Attribute &a) +{ + switch (a.type) { + case QSGGeometry::FloatType: + if (a.tupleSize == 4) + return QRhiVertexInputAttribute::Float4; + if (a.tupleSize == 3) + return QRhiVertexInputAttribute::Float3; + if (a.tupleSize == 2) + return QRhiVertexInputAttribute::Float2; + if (a.tupleSize == 1) + return QRhiVertexInputAttribute::Float; + break; + case QSGGeometry::UnsignedByteType: + if (a.tupleSize == 4) + return QRhiVertexInputAttribute::UNormByte4; + if (a.tupleSize == 2) + return QRhiVertexInputAttribute::UNormByte2; + if (a.tupleSize == 1) + return QRhiVertexInputAttribute::UNormByte; + break; + default: + break; + } + qWarning("Unsupported attribute type 0x%x with %d components", a.type, a.tupleSize); + Q_UNREACHABLE_RETURN(QRhiVertexInputAttribute::Float); +} + +static QRhiVertexInputLayout calculateVertexInputLayout(const QSGMaterialShader *s, const QSGGeometry *geometry, bool batchable) +{ + Q_ASSERT(geometry); + const QSGMaterialShaderPrivate *sd = QSGMaterialShaderPrivate::get(s); + if (!sd->vertexShader) { + qWarning("No vertex shader in QSGMaterialShader %p", s); + return QRhiVertexInputLayout(); + } + + const int attrCount = geometry->attributeCount(); + QVarLengthArray inputAttributes; + inputAttributes.reserve(attrCount + 1); + quint32 offset = 0; + for (int i = 0; i < attrCount; ++i) { + const QSGGeometry::Attribute &a = geometry->attributes()[i]; + if (!sd->vertexShader->vertexInputLocations.contains(a.position)) { + qWarning("Vertex input %d is present in material but not in shader. This is wrong.", + a.position); + } + inputAttributes.append(QRhiVertexInputAttribute(VERTEX_BUFFER_BINDING, a.position, qsg_vertexInputFormat(a), offset)); + offset += a.tupleSize * size_of_type(a.type); + } + if (batchable) { + inputAttributes.append(QRhiVertexInputAttribute(ZORDER_BUFFER_BINDING, sd->vertexShader->qt_order_attrib_location, + QRhiVertexInputAttribute::Float, 0)); + } + + Q_ASSERT(VERTEX_BUFFER_BINDING == 0 && ZORDER_BUFFER_BINDING == 1); // not very flexible + QVarLengthArray inputBindings; + inputBindings.append(QRhiVertexInputBinding(geometry->sizeOfVertex())); + if (batchable) + inputBindings.append(QRhiVertexInputBinding(sizeof(float))); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings(inputBindings.cbegin(), inputBindings.cend()); + inputLayout.setAttributes(inputAttributes.cbegin(), inputAttributes.cend()); + + return inputLayout; +} + +QRhiCommandBuffer::IndexFormat qsg_indexFormat(const QSGGeometry *geometry) +{ + switch (geometry->indexType()) { + case QSGGeometry::UnsignedShortType: + return QRhiCommandBuffer::IndexUInt16; + break; + case QSGGeometry::UnsignedIntType: + return QRhiCommandBuffer::IndexUInt32; + break; + default: + Q_UNREACHABLE_RETURN(QRhiCommandBuffer::IndexUInt16); + } +} + +QRhiGraphicsPipeline::Topology qsg_topology(int geomDrawMode, QRhi *rhi) +{ + QRhiGraphicsPipeline::Topology topology = QRhiGraphicsPipeline::Triangles; + switch (geomDrawMode) { + case QSGGeometry::DrawPoints: + topology = QRhiGraphicsPipeline::Points; + break; + case QSGGeometry::DrawLines: + topology = QRhiGraphicsPipeline::Lines; + break; + case QSGGeometry::DrawLineStrip: + topology = QRhiGraphicsPipeline::LineStrip; + break; + case QSGGeometry::DrawTriangles: + topology = QRhiGraphicsPipeline::Triangles; + break; + case QSGGeometry::DrawTriangleStrip: + topology = QRhiGraphicsPipeline::TriangleStrip; + break; + case QSGGeometry::DrawTriangleFan: + { + static bool triangleFanSupported = false; + static bool triangleFanSupportChecked = false; + if (!triangleFanSupportChecked) { + triangleFanSupportChecked = true; + triangleFanSupported = rhi->isFeatureSupported(QRhi::TriangleFanTopology); + } + if (triangleFanSupported) { + topology = QRhiGraphicsPipeline::TriangleFan; + break; + } + Q_FALLTHROUGH(); + } + default: + qWarning("Primitive topology 0x%x not supported", geomDrawMode); + break; + } + return topology; +} + +void qsg_setMultiViewFlagsOnMaterial(QSGMaterial *material, int multiViewCount) +{ + material->setFlag(QSGMaterial::MultiView2, multiViewCount == 2); + material->setFlag(QSGMaterial::MultiView3, multiViewCount == 3); + material->setFlag(QSGMaterial::MultiView4, multiViewCount == 4); +} + +ShaderManager::Shader *ShaderManager::prepareMaterial(QSGMaterial *material, + const QSGGeometry *geometry, + QSGRendererInterface::RenderMode renderMode, + int multiViewCount) +{ + qsg_setMultiViewFlagsOnMaterial(material, multiViewCount); + + QSGMaterialType *type = material->type(); + ShaderKey key = { type, renderMode, multiViewCount }; + Shader *shader = rewrittenShaders.value(key, nullptr); + if (shader) + return shader; + + shader = new Shader; + QSGMaterialShader *s = static_cast(material->createShader(renderMode)); + context->initializeRhiShader(s, QShader::BatchableVertexShader); + shader->materialShader = s; + shader->inputLayout = calculateVertexInputLayout(s, geometry, true); + QSGMaterialShaderPrivate *sD = QSGMaterialShaderPrivate::get(s); + shader->stages = { + { QRhiShaderStage::Vertex, sD->shader(QShader::VertexStage), QShader::BatchableVertexShader }, + { QRhiShaderStage::Fragment, sD->shader(QShader::FragmentStage) } + }; + + shader->lastOpacity = 0; + + rewrittenShaders[key] = shader; + return shader; +} + +ShaderManager::Shader *ShaderManager::prepareMaterialNoRewrite(QSGMaterial *material, + const QSGGeometry *geometry, + QSGRendererInterface::RenderMode renderMode, + int multiViewCount) +{ + qsg_setMultiViewFlagsOnMaterial(material, multiViewCount); + + QSGMaterialType *type = material->type(); + ShaderKey key = { type, renderMode, multiViewCount }; + Shader *shader = stockShaders.value(key, nullptr); + if (shader) + return shader; + + shader = new Shader; + QSGMaterialShader *s = static_cast(material->createShader(renderMode)); + context->initializeRhiShader(s, QShader::StandardShader); + shader->materialShader = s; + shader->inputLayout = calculateVertexInputLayout(s, geometry, false); + QSGMaterialShaderPrivate *sD = QSGMaterialShaderPrivate::get(s); + shader->stages = { + { QRhiShaderStage::Vertex, sD->shader(QShader::VertexStage) }, + { QRhiShaderStage::Fragment, sD->shader(QShader::FragmentStage) } + }; + + shader->lastOpacity = 0; + + stockShaders[key] = shader; + + return shader; +} + +void ShaderManager::invalidated() +{ + qDeleteAll(stockShaders); + stockShaders.clear(); + qDeleteAll(rewrittenShaders); + rewrittenShaders.clear(); + + qDeleteAll(pipelineCache); + pipelineCache.clear(); + + qDeleteAll(srbPool); + srbPool.clear(); +} + +void ShaderManager::clearCachedRendererData() +{ + for (ShaderManager::Shader *sms : std::as_const(stockShaders)) { + QSGMaterialShader *s = sms->materialShader; + if (s) { + QSGMaterialShaderPrivate *sd = QSGMaterialShaderPrivate::get(s); + sd->clearCachedRendererData(); + } + } + for (ShaderManager::Shader *sms : std::as_const(rewrittenShaders)) { + QSGMaterialShader *s = sms->materialShader; + if (s) { + QSGMaterialShaderPrivate *sd = QSGMaterialShaderPrivate::get(s); + sd->clearCachedRendererData(); + } + } +} + +void qsg_dumpShadowRoots(BatchRootInfo *i, int indent) +{ + static int extraIndent = 0; + ++extraIndent; + + QByteArray ind(indent + extraIndent + 10, ' '); + + if (!i) { + qDebug("%s - no info", ind.constData()); + } else { + qDebug() << ind.constData() << "- parent:" << i->parentRoot << "orders" << i->firstOrder << "->" << i->lastOrder << ", avail:" << i->availableOrders; + for (QSet::const_iterator it = i->subRoots.constBegin(); + it != i->subRoots.constEnd(); ++it) { + qDebug() << ind.constData() << "-" << *it; + qsg_dumpShadowRoots((*it)->rootInfo(), indent); + } + } + + --extraIndent; +} + +void qsg_dumpShadowRoots(Node *n) +{ +#ifndef QT_NO_DEBUG_OUTPUT + static int indent = 0; + ++indent; + + QByteArray ind(indent, ' '); + + if (n->type() == QSGNode::ClipNodeType || n->isBatchRoot) { + qDebug() << ind.constData() << "[X]" << n->sgNode << Qt::hex << uint(n->sgNode->flags()); + qsg_dumpShadowRoots(n->rootInfo(), indent); + } else { + QDebug d = qDebug(); + d << ind.constData() << "[ ]" << n->sgNode << Qt::hex << uint(n->sgNode->flags()); + if (n->type() == QSGNode::GeometryNodeType) + d << "order" << Qt::dec << n->element()->order; + } + + SHADOWNODE_TRAVERSE(n) + qsg_dumpShadowRoots(child); + + --indent; +#else + Q_UNUSED(n); +#endif +} + +Updater::Updater(Renderer *r) + : renderer(r) + , m_roots(32) + , m_rootMatrices(8) +{ + m_roots.add(0); + m_combined_matrix_stack.add(&m_identityMatrix); + m_rootMatrices.add(m_identityMatrix); +} + +void Updater::updateStates(QSGNode *n) +{ + m_current_clip = nullptr; + + m_added = 0; + m_transformChange = 0; + m_opacityChange = 0; + + Node *sn = renderer->m_nodes.value(n, 0); + Q_ASSERT(sn); + + if (Q_UNLIKELY(debug_roots())) + qsg_dumpShadowRoots(sn); + + if (Q_UNLIKELY(debug_build())) { + qDebug("Updater::updateStates()"); + if (sn->dirtyState & (QSGNode::DirtyNodeAdded << 16)) + qDebug(" - nodes have been added"); + if (sn->dirtyState & (QSGNode::DirtyMatrix << 16)) + qDebug(" - transforms have changed"); + if (sn->dirtyState & (QSGNode::DirtyOpacity << 16)) + qDebug(" - opacity has changed"); + if (uint(sn->dirtyState) & uint(QSGNode::DirtyForceUpdate << 16)) + qDebug(" - forceupdate"); + } + + if (Q_UNLIKELY(renderer->m_visualizer->mode() == Visualizer::VisualizeChanges)) + renderer->m_visualizer->visualizeChangesPrepare(sn); + + visitNode(sn); +} + +void Updater::visitNode(Node *n) +{ + if (m_added == 0 && n->dirtyState == 0 && m_force_update == 0 && m_transformChange == 0 && m_opacityChange == 0) + return; + + int count = m_added; + if (n->dirtyState & QSGNode::DirtyNodeAdded) + ++m_added; + + int force = m_force_update; + if (n->dirtyState & QSGNode::DirtyForceUpdate) + ++m_force_update; + + switch (n->type()) { + case QSGNode::OpacityNodeType: + visitOpacityNode(n); + break; + case QSGNode::TransformNodeType: + visitTransformNode(n); + break; + case QSGNode::GeometryNodeType: + visitGeometryNode(n); + break; + case QSGNode::ClipNodeType: + visitClipNode(n); + break; + case QSGNode::RenderNodeType: + if (m_added) + n->renderNodeElement()->root = m_roots.last(); + Q_FALLTHROUGH(); // to visit children + default: + SHADOWNODE_TRAVERSE(n) visitNode(child); + break; + } + + m_added = count; + m_force_update = force; + n->dirtyState = {}; +} + +void Updater::visitClipNode(Node *n) +{ + ClipBatchRootInfo *extra = n->clipInfo(); + + QSGClipNode *cn = static_cast(n->sgNode); + + if (m_roots.last() && m_added > 0) + renderer->registerBatchRoot(n, m_roots.last()); + + cn->setRendererClipList(m_current_clip); + m_current_clip = cn; + m_roots << n; + m_rootMatrices.add(m_rootMatrices.last() * *m_combined_matrix_stack.last()); + extra->matrix = m_rootMatrices.last(); + cn->setRendererMatrix(&extra->matrix); + m_combined_matrix_stack << &m_identityMatrix; + + SHADOWNODE_TRAVERSE(n) visitNode(child); + + m_current_clip = cn->clipList(); + m_rootMatrices.pop_back(); + m_combined_matrix_stack.pop_back(); + m_roots.pop_back(); +} + +void Updater::visitOpacityNode(Node *n) +{ + QSGOpacityNode *on = static_cast(n->sgNode); + + qreal combined = m_opacity_stack.last() * on->opacity(); + on->setCombinedOpacity(combined); + m_opacity_stack.add(combined); + + if (m_added == 0 && n->dirtyState & QSGNode::DirtyOpacity) { + bool was = n->isOpaque; + bool is = on->opacity() > OPAQUE_LIMIT; + if (was != is) { + renderer->m_rebuild = Renderer::FullRebuild; + n->isOpaque = is; + } + ++m_opacityChange; + SHADOWNODE_TRAVERSE(n) visitNode(child); + --m_opacityChange; + } else { + if (m_added > 0) + n->isOpaque = on->opacity() > OPAQUE_LIMIT; + SHADOWNODE_TRAVERSE(n) visitNode(child); + } + + m_opacity_stack.pop_back(); +} + +void Updater::visitTransformNode(Node *n) +{ + bool popMatrixStack = false; + bool popRootStack = false; + bool dirty = n->dirtyState & QSGNode::DirtyMatrix; + + QSGTransformNode *tn = static_cast(n->sgNode); + + if (n->isBatchRoot) { + if (m_added > 0 && m_roots.last()) + renderer->registerBatchRoot(n, m_roots.last()); + tn->setCombinedMatrix(m_rootMatrices.last() * *m_combined_matrix_stack.last() * tn->matrix()); + + // The only change in this subtree is ourselves and we are a batch root, so + // only update subroots and return, saving tons of child-processing (flickable-panning) + + if (!n->becameBatchRoot && m_added == 0 && m_force_update == 0 && m_opacityChange == 0 && dirty && (n->dirtyState & ~QSGNode::DirtyMatrix) == 0) { + BatchRootInfo *info = renderer->batchRootInfo(n); + for (QSet::const_iterator it = info->subRoots.constBegin(); + it != info->subRoots.constEnd(); ++it) { + updateRootTransforms(*it, n, tn->combinedMatrix()); + } + return; + } + + n->becameBatchRoot = false; + + m_combined_matrix_stack.add(&m_identityMatrix); + m_roots.add(n); + m_rootMatrices.add(tn->combinedMatrix()); + + popMatrixStack = true; + popRootStack = true; + } else if (!tn->matrix().isIdentity()) { + tn->setCombinedMatrix(*m_combined_matrix_stack.last() * tn->matrix()); + m_combined_matrix_stack.add(&tn->combinedMatrix()); + popMatrixStack = true; + } else { + tn->setCombinedMatrix(*m_combined_matrix_stack.last()); + } + + if (dirty) + ++m_transformChange; + + SHADOWNODE_TRAVERSE(n) visitNode(child); + + if (dirty) + --m_transformChange; + if (popMatrixStack) + m_combined_matrix_stack.pop_back(); + if (popRootStack) { + m_roots.pop_back(); + m_rootMatrices.pop_back(); + } +} + +void Updater::visitGeometryNode(Node *n) +{ + QSGGeometryNode *gn = static_cast(n->sgNode); + + gn->setRendererMatrix(m_combined_matrix_stack.last()); + gn->setRendererClipList(m_current_clip); + gn->setInheritedOpacity(m_opacity_stack.last()); + + if (m_added) { + Element *e = n->element(); + e->root = m_roots.last(); + e->translateOnlyToRoot = isTranslate(*gn->matrix()); + + if (e->root) { + BatchRootInfo *info = renderer->batchRootInfo(e->root); + while (info != nullptr) { + info->availableOrders--; + if (info->availableOrders < 0) { + renderer->m_rebuild |= Renderer::BuildRenderLists; + } else { + renderer->m_rebuild |= Renderer::BuildRenderListsForTaggedRoots; + renderer->m_taggedRoots << e->root; + } + if (info->parentRoot != nullptr) + info = renderer->batchRootInfo(info->parentRoot); + else + info = nullptr; + } + } else { + renderer->m_rebuild |= Renderer::FullRebuild; + } + } else { + if (m_transformChange) { + Element *e = n->element(); + e->translateOnlyToRoot = isTranslate(*gn->matrix()); + } + if (m_opacityChange) { + Element *e = n->element(); + if (e->batch) + renderer->invalidateBatchAndOverlappingRenderOrders(e->batch); + } + } + + SHADOWNODE_TRAVERSE(n) visitNode(child); +} + +void Updater::updateRootTransforms(Node *node, Node *root, const QMatrix4x4 &combined) +{ + BatchRootInfo *info = renderer->batchRootInfo(node); + QMatrix4x4 m; + Node *n = node; + + while (n != root) { + if (n->type() == QSGNode::TransformNodeType) + m = static_cast(n->sgNode)->matrix() * m; + n = n->parent(); + } + + m = combined * m; + + if (node->type() == QSGNode::ClipNodeType) { + static_cast(info)->matrix = m; + } else { + Q_ASSERT(node->type() == QSGNode::TransformNodeType); + static_cast(node->sgNode)->setCombinedMatrix(m); + } + + for (QSet::const_iterator it = info->subRoots.constBegin(); + it != info->subRoots.constEnd(); ++it) { + updateRootTransforms(*it, node, m); + } +} + +int qsg_positionAttribute(QSGGeometry *g) +{ + int vaOffset = 0; + for (int a=0; aattributeCount(); ++a) { + const QSGGeometry::Attribute &attr = g->attributes()[a]; + if (attr.isVertexCoordinate && attr.tupleSize == 2 && attr.type == QSGGeometry::FloatType) { + return vaOffset; + } + vaOffset += attr.tupleSize * size_of_type(attr.type); + } + return -1; +} + + +void Rect::map(const QMatrix4x4 &matrix) +{ + const float *m = matrix.constData(); + if (isScale(matrix)) { + tl.x = tl.x * m[0] + m[12]; + tl.y = tl.y * m[5] + m[13]; + br.x = br.x * m[0] + m[12]; + br.y = br.y * m[5] + m[13]; + if (tl.x > br.x) + qSwap(tl.x, br.x); + if (tl.y > br.y) + qSwap(tl.y, br.y); + } else { + Pt mtl = tl; + Pt mtr = { br.x, tl.y }; + Pt mbl = { tl.x, br.y }; + Pt mbr = br; + + mtl.map(matrix); + mtr.map(matrix); + mbl.map(matrix); + mbr.map(matrix); + + set(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + (*this) |= mtl; + (*this) |= mtr; + (*this) |= mbl; + (*this) |= mbr; + } +} + +void Element::computeBounds() +{ + Q_ASSERT(!boundsComputed); + boundsComputed = true; + + QSGGeometry *g = node->geometry(); + int offset = qsg_positionAttribute(g); + if (offset == -1) { + // No position attribute means overlaps with everything.. + bounds.set(-FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX); + return; + } + + bounds.set(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + char *vd = (char *) g->vertexData() + offset; + for (int i=0; ivertexCount(); ++i) { + bounds |= *(Pt *) vd; + vd += g->sizeOfVertex(); + } + bounds.map(*node->matrix()); + + if (!qt_is_finite(bounds.tl.x) || bounds.tl.x == FLT_MAX) + bounds.tl.x = -FLT_MAX; + if (!qt_is_finite(bounds.tl.y) || bounds.tl.y == FLT_MAX) + bounds.tl.y = -FLT_MAX; + if (!qt_is_finite(bounds.br.x) || bounds.br.x == -FLT_MAX) + bounds.br.x = FLT_MAX; + if (!qt_is_finite(bounds.br.y) || bounds.br.y == -FLT_MAX) + bounds.br.y = FLT_MAX; + + Q_ASSERT(bounds.tl.x <= bounds.br.x); + Q_ASSERT(bounds.tl.y <= bounds.br.y); + + boundsOutsideFloatRange = bounds.isOutsideFloatRange(); +} + +BatchCompatibility Batch::isMaterialCompatible(Element *e) const +{ + Element *n = first; + // Skip to the first node other than e which has not been removed + while (n && (n == e || n->removed)) + n = n->nextInBatch; + + // Only 'e' in this batch, so a material change doesn't change anything as long as + // its blending is still in sync with this batch... + if (!n) + return BatchIsCompatible; + + QSGMaterial *m = e->node->activeMaterial(); + QSGMaterial *nm = n->node->activeMaterial(); + return (nm->type() == m->type() && nm->viewCount() == m->viewCount() && nm->compare(m) == 0) + ? BatchIsCompatible + : BatchBreaksOnCompare; +} + +/* + * Marks this batch as dirty or in the case where the geometry node has + * changed to be incompatible with this batch, return false so that + * the caller can mark the entire sg for a full rebuild... + */ +bool Batch::geometryWasChanged(QSGGeometryNode *gn) +{ + Element *e = first; + Q_ASSERT_X(e, "Batch::geometryWasChanged", "Batch is expected to 'valid' at this time"); + // 'gn' is the first node in the batch, compare against the next one. + while (e && (e->node == gn || e->removed)) + e = e->nextInBatch; + if (!e || e->node->geometry()->attributes() == gn->geometry()->attributes()) { + needsUpload = true; + return true; + } else { + return false; + } +} + +void Batch::cleanupRemovedElements() +{ + if (!needsPurge) + return; + + // remove from front of batch.. + while (first && first->removed) { + first = first->nextInBatch; + } + + // Then continue and remove other nodes further out in the batch.. + if (first) { + Element *e = first; + while (e->nextInBatch) { + if (e->nextInBatch->removed) + e->nextInBatch = e->nextInBatch->nextInBatch; + else + e = e->nextInBatch; + + } + } + + needsPurge = false; +} + +/* + * Iterates through all geometry nodes in this batch and unsets their batch, + * thus forcing them to be rebuilt + */ +void Batch::invalidate() +{ + cleanupRemovedElements(); + Element *e = first; + first = nullptr; + root = nullptr; + while (e) { + e->batch = nullptr; + Element *n = e->nextInBatch; + e->nextInBatch = nullptr; + e = n; + } +} + +bool Batch::isTranslateOnlyToRoot() const { + bool only = true; + Element *e = first; + while (e && only) { + only &= e->translateOnlyToRoot; + e = e->nextInBatch; + } + return only; +} + +/* + * Iterates through all the nodes in the batch and returns true if the + * nodes are all safe to batch. There are two separate criteria: + * + * - The matrix is such that the z component of the result is of no + * consequence. + * + * - The bounds are inside the stable floating point range. This applies + * to desktop only where we in this case can trigger a fallback to + * unmerged in which case we pass the geometry straight through and + * just apply the matrix. + * + * NOTE: This also means a slight performance impact for geometries which + * are defined to be outside the stable floating point range and still + * use single precision float, but given that this implicitly fixes + * huge lists and tables, it is worth it. + */ +bool Batch::isSafeToBatch() const { + Element *e = first; + while (e) { + if (e->boundsOutsideFloatRange) + return false; + if (!is2DSafe(*e->node->matrix())) + return false; + e = e->nextInBatch; + } + return true; +} + +static int qsg_countNodesInBatch(const Batch *batch) +{ + int sum = 0; + Element *e = batch->first; + while (e) { + ++sum; + e = e->nextInBatch; + } + return sum; +} + +static int qsg_countNodesInBatches(const QDataBuffer &batches) +{ + int sum = 0; + for (int i=0; irect(); + return { }; +} + +static QRectF localRectFromQSGImageNode(QSGNode *sg) +{ + return static_cast(sg)->rect(); +} + +static QRectF localRectFromRectangleNode(QSGNode *sg) +{ + return static_cast(sg)->rect(); +} + +static QRectF localRectFromSimpleRectNode(QSGNode *sg) +{ + return static_cast(sg)->rect(); +} + +static QRectF localRectFromSimpleTextureNode(QSGNode *sg) +{ + return static_cast(sg)->rect(); +} + +static QRectF localRectFromInternalRectangleNode(QSGNode *sg) +{ + return W_PRIVATE_MEMBER(*static_cast(sg), + WSG_QSGBasicInternalRect_m_rect{ }); +} + +static QRectF localRectFromInternalImageNode(QSGNode *sg) +{ + return W_PRIVATE_MEMBER(*static_cast(sg), + WSG_QSGBasicInternalImage_m_targetRect{ }); +} + +static QRectF localRectFromNinePatchNode(QSGNode *sg) +{ + return W_PRIVATE_MEMBER(*static_cast(sg), + WSG_QSGDefaultNinePatch_m_bounds{ }); +} + +static QRectF localRectFromGlyphNode(QSGNode *sg) +{ + return static_cast(sg)->boundingRect(); +} + +static QRectF localRectFromPainterNode(QSGNode *sg) +{ + return QRectF(QPointF(), QSizeF(static_cast(sg)->size())); +} + +static QRectF localRectFromClipNode(QSGNode *sg) +{ + return static_cast(sg)->clipRect(); +} + +static QRectF localRectFromRenderNode(QSGNode *sg) +{ + return static_cast(sg)->rect(); +} + +static Node::LocalRectFn localRectFnFor(QSGNode *sg) +{ + if (!sg) + return nullptr; + if (sg->type() == QSGNode::GeometryNodeType && WSGImageNode::enclosingNode(sg)) + return localRectFromImageNode; + if (dynamic_cast(sg)) + return localRectFromQSGImageNode; + if (dynamic_cast(sg)) + return localRectFromRectangleNode; + if (dynamic_cast(sg)) + return localRectFromSimpleRectNode; + if (dynamic_cast(sg)) + return localRectFromSimpleTextureNode; + if (dynamic_cast(sg)) + return localRectFromInternalRectangleNode; + if (dynamic_cast(sg)) + return localRectFromInternalImageNode; + if (dynamic_cast(sg)) + return localRectFromNinePatchNode; + if (dynamic_cast(sg)) + return localRectFromGlyphNode; + if (dynamic_cast(sg)) + return localRectFromPainterNode; + if (sg->type() == QSGNode::ClipNodeType) + return localRectFromClipNode; + if (sg->type() == QSGNode::RenderNodeType) + return localRectFromRenderNode; + return nullptr; +} + +struct Renderer::DamageTree +{ + QSGNode *trackedRoot = nullptr; + std::unique_ptr root; + WSGDamageNode *sceneRoot = nullptr; + WSGDamageTracker tracker; + QHash *nodes = nullptr; + + struct AggregatedSubtree + { + Node *shadowRoot = nullptr; + WSGDamageGeometryNode *proxy = nullptr; + }; + + QHash aggregatedSubtrees; + QHash aggregateOwners; + // QSG nodes whose raster extent cannot be derived (untyped geometry + // without a Float2 position attribute, points with shader-driven size, + // unbounded render nodes with an empty rect()). The damage graph stores + // them as contentless, so every cull/damage decision must treat them as + // conservatively intersecting instead. Stale entries are dropped in + // nodeWasRemoved; the QSG node outlives its shadow entry there. + mutable QSet unknownBounds; + + void setBoundsKnown(QSGNode *sg, bool known) const + { + if (!sg) + return; + if (known) + unknownBounds.remove(sg); + else + unknownBounds.insert(sg); + } + + bool hasUnknownBounds(const QSGNode *sg) const + { + return sg && unknownBounds.contains(const_cast(sg)); + } + + WDamageRegion render; // GPU redraw region + WDamageRegion flush; // Scene content dirty region + QRect outputRect; + + // Per-tracker state. Keeping it here makes DamageMode::Off release all + // damage regions, scissor storage, and diagnostic lists. + bool modeSwitchFull = false; + bool scissorEnabled = false; + QRhiRenderTarget *scissorTarget = nullptr; + WSGViewport *viewport = nullptr; + WPixmanRegion extraScissor; + WPixmanRegion blitterPending; + bool blitterPendingFull = true; + QVector nativeScissors; + WPixmanRegion sceneScissors; + QStringList idleBlitters; + QStringList nestedSkipBlitters; + + + static QRect contentLocalFull(const QRectF &br) + { + return QRect(0, + 0, + qMax(0, int(std::ceil(br.width()))), + qMax(0, int(std::ceil(br.height())))); + } + + static WPixmanRegion toContentLocal(const QRectF &br, const WPixmanRegion &local) + { + return local.translated(-int(std::floor(br.x())), -int(std::floor(br.y()))); + } + + static int vertexPositionOffset(const QSGGeometry *g) + { + if (!g || g->vertexCount() <= 0) + return -1; + int offset = 0; + for (int a = 0; a < g->attributeCount(); ++a) { + const QSGGeometry::Attribute &attr = g->attributes()[a]; + if (attr.isVertexCoordinate && attr.tupleSize == 2 + && attr.type == QSGGeometry::FloatType) { + return offset; + } + const int typeSize = attr.type == QSGGeometry::FloatType ? 4 + : attr.type == QSGGeometry::DoubleType ? 8 + : attr.type == QSGGeometry::UnsignedByteType || attr.type == QSGGeometry::ByteType + ? 1 + : attr.type == QSGGeometry::UnsignedShortType || attr.type == QSGGeometry::ShortType + || attr.type == QSGGeometry::Bytes2Type + ? 2 + : attr.type == QSGGeometry::Bytes3Type ? 3 + : attr.type == QSGGeometry::Bytes4Type ? 4 + : 4; + offset += attr.tupleSize * typeSize; + } + return -1; + } + + static QRectF vertexLocalRect(const QSGBasicGeometryNode *gn) + { + if (!gn) + return { }; + const QSGGeometry *g = gn->geometry(); + const int posOffset = vertexPositionOffset(g); + if (posOffset < 0) + return { }; + qreal minX = FLT_MAX, minY = FLT_MAX, maxX = -FLT_MAX, maxY = -FLT_MAX; + const char *vd = static_cast(g->vertexData()) + posOffset; + for (int i = 0; i < g->vertexCount(); ++i) { + const float *pt = reinterpret_cast(vd); + minX = qMin(minX, qreal(pt[0])); + minY = qMin(minY, qreal(pt[1])); + maxX = qMax(maxX, qreal(pt[0])); + maxY = qMax(maxY, qreal(pt[1])); + vd += g->sizeOfVertex(); + } + QRectF local = QRectF(QPointF(minX, minY), QPointF(maxX, maxY)).normalized(); + // Wide lines rasterize beyond the vertex extents. The batcher's own + // overlap check gives up on them entirely; for damage an outward + // half-width margin covers butt/square caps and miter joins. + const int mode = g->drawingMode(); + if ((mode == QSGGeometry::DrawLines || mode == QSGGeometry::DrawLineStrip + || mode == QSGGeometry::DrawLineLoop) + && g->lineWidth() > 1.0f) { + const qreal half = qreal(g->lineWidth()) / 2.0; + local.adjust(-half, -half, half, half); + } + return local; + } + + static QRectF typedLocalRect(QSGNode *sg, Node *shadow) + { + if (shadow && shadow->localRectFn) + return shadow->localRectFn(sg); + return { }; + } + + QRectF localGeometryRect(QSGNode *sg) const + { + Node *shadow = nodes ? nodes->value(sg) : nullptr; + const QRectF typed = typedLocalRect(sg, shadow); + if (!typed.isEmpty()) + return typed; + if (auto *basic = dynamic_cast(sg)) + return vertexLocalRect(basic); + return { }; + } + + // Whether the node's raster extent is derivable. An empty rect here + // means "known empty" (zero-size item: culled, contentless) while false + // means "unknown" (custom vertex layout, unbounded render node): + // conservatively intersecting, never culled, never contentless damage. + bool localBoundsKnown(QSGNode *sg) const + { + if (!sg) + return false; + if (sg->type() == QSGNode::RenderNodeType) { + // rect() is virtual; covers WRenderBufferNode too. An empty rect + // promises nothing about the raster extent (e.g. the surface + // footprint node), so it is unbounded, not empty. + return !static_cast(sg)->rect().isEmpty(); + } + if (sg->type() != QSGNode::GeometryNodeType) + return true; + Node *shadow = nodes ? nodes->value(sg) : nullptr; + if (shadow && shadow->localRectFn) + return true; + auto *basic = dynamic_cast(sg); + const QSGGeometry *g = basic ? basic->geometry() : nullptr; + if (!g) + return false; + if (g->drawingMode() == QSGGeometry::DrawPoints && g->lineWidth() != 1.0f) + return false; // gl_PointSize is shader-driven; extent underivable. + return vertexPositionOffset(g) >= 0; + } + + bool isInTrackedTree(const QSGNode *node) const + { + if (!trackedRoot) + return true; + for (const QSGNode *n = node; n; n = n->parent()) { + if (n == trackedRoot) + return true; + } + return false; + } + + // Damage sync (nodeChanged) runs before Updater::visitOpacityNode refreshes + // QSG combined/inherited opacity, so any opaque decision taken here must + // use the freshly walked product, never the stale inheritedOpacity(). + static qreal qsgFreshInheritedOpacity(const QSGNode *sg) + { + qreal opacity = 1.0; + for (const QSGNode *n = sg ? sg->parent() : nullptr; n; n = n->parent()) { + if (n->type() == QSGNode::OpacityNodeType) + opacity *= static_cast(n)->opacity(); + } + return opacity; + } + + static bool qsgGeometryIsOpaque(const QSGNode *sg, qreal inheritedOpacity) + { + if (!sg || sg->type() != QSGNode::GeometryNodeType) + return false; + auto *gn = static_cast(sg); + if (!gn->activeMaterial()) + return false; + return inheritedOpacity > OPAQUE_LIMIT + && !(gn->activeMaterial()->flags() & QSGMaterial::Blending); + } + + WSGDamageNode *damageOf(const QSGNode *sg) const + { + if (!sg || !nodes) + return nullptr; + Node *n = nodes->value(const_cast(sg)); + return n ? n->damage : nullptr; + } + + static void bindElementDamage(Node *n) + { + if (!n || !n->data) + return; + WSGDamageNode *damage = n->aggregateDamage ? n->aggregateDamage : n->damage; + if (n->type() == QSGNode::GeometryNodeType) + n->element()->damage = damage; + else if (n->type() == QSGNode::RenderNodeType) + n->renderNodeElement()->damage = damage; + } + + void clearDamagePointers(WSGDamageNode *g) + { + if (!g || !nodes) + return; + for (auto it = nodes->cbegin(); it != nodes->cend(); ++it) { + Node *n = it.value(); + if (n->damage != g) + continue; + n->damage = nullptr; + bindElementDamage(n); + } + for (WSGDamageNode *c = g->firstChild(); c; c = c->nextSibling()) + clearDamagePointers(c); + } + + static void markAggregateDirty(WSGDamageGeometryNode *proxy) + { + if (!proxy) + return; + const QRectF bounds = proxy->boundingRect(); + if (!bounds.isEmpty()) + proxy->markContentDirty(contentLocalFull(bounds)); + } + + static bool subtreeContainsBackdrop(const QSGNode *node) + { + if (!node) + return false; + if (dynamic_cast(node)) + return true; + for (const QSGNode *child = node->firstChild(); child; child = child->nextSibling()) { + if (subtreeContainsBackdrop(child)) + return true; + } + return false; + } + + static bool subtreeHasAggregate(const Node *node) + { + if (!node) + return false; + if (node->aggregateDamage) + return true; + for (const Node *child = node->firstChild(); child; child = child->sibling()) { + if (subtreeHasAggregate(child)) + return true; + } + return false; + } + + static void bindAggregateRecursive(Node *node, + WSGDamageGeometryNode *proxy, + const Node *structuralRoot) + { + if (!node) + return; + node->aggregateDamage = proxy; + if (node != structuralRoot) + node->damage = nullptr; + bindElementDamage(node); + for (Node *child = node->firstChild(); child; child = child->sibling()) + bindAggregateRecursive(child, proxy, structuralRoot); + } + + static void clearAggregateRecursive(Node *node) + { + if (!node) + return; + node->aggregateDamage = nullptr; + bindElementDamage(node); + for (Node *child = node->firstChild(); child; child = child->sibling()) + clearAggregateRecursive(child); + } + + static void deleteDamageChildren(WSGDamageNode *node) + { + while (node && node->firstChild()) { + WSGDamageNode *child = node->firstChild(); + node->removeChild(child); + delete child; + } + } + + static qreal internalRectangleRadius(const QSGBasicInternalRectangleNode &n) + { + qreal r = W_PRIVATE_MEMBER(n, WSG_QSGBasicInternalRect_m_radius{ }); + r = std::max(r, qreal(W_PRIVATE_MEMBER(n, WSG_QSGBasicInternalRect_m_topLeftRadius{ }))); + r = std::max(r, qreal(W_PRIVATE_MEMBER(n, WSG_QSGBasicInternalRect_m_topRightRadius{ }))); + r = std::max(r, qreal(W_PRIVATE_MEMBER(n, WSG_QSGBasicInternalRect_m_bottomLeftRadius{ }))); + r = std::max(r, + qreal(W_PRIVATE_MEMBER(n, WSG_QSGBasicInternalRect_m_bottomRightRadius{ }))); + return r; + } + + static bool gradientStopsOpaque(const QGradientStops &stops) + { + for (const QGradientStop &stop : stops) { + if (stop.second.alpha() < 255) + return false; + } + return true; + } + + bool syncTypedOpaque(QSGNode *sg, WSGDamageGeometryNode *geo, qreal inheritedOpacity) const + { + if (inheritedOpacity <= OPAQUE_LIMIT) + return false; + if (auto *image = WSGImageNode::enclosingNode(sg); image && image->hasExplicitOpaque()) { + const QRectF br = geo->boundingRect(); + const WPixmanRegion local = toContentLocal(br, image->opaqueRegion()); + const WPixmanRegion content(contentLocalFull(br)); + if (!content.isEmpty() && (content - local).isEmpty()) + geo->setFullyOpaque(true); + else + geo->setOpaqueRegion(local); + return true; + } + if (auto *rect = dynamic_cast(sg)) { + const QColor color = W_PRIVATE_MEMBER(*rect, WSG_QSGBasicInternalRect_m_color{ }); + if (color.alpha() < 255) + return false; + if (!gradientStopsOpaque( + W_PRIVATE_MEMBER(*rect, WSG_QSGBasicInternalRect_m_gradient_stops{ }))) + return false; + const qreal rad = internalRectangleRadius(*rect); + const QRectF box = geo->boundingRect(); + const QRectF local(0, 0, box.width(), box.height()); + if (rad <= 0) + geo->setFullyOpaque(true); + else + geo->setOpaqueRegion(roundedRectInnerRegion(local, rad)); + return true; + } + if (auto *n = dynamic_cast(sg)) { + if (n->color().alpha() < 255) + return false; + geo->setFullyOpaque(true); + return true; + } + return false; + } + + void syncGeometryOpaque(QSGNode *sg, WSGDamageNode *g, qreal inheritedOpacity) const + { + auto *geo = g ? g->toGeometry() : nullptr; + if (!geo) + return; + if (sg->type() == QSGNode::GeometryNodeType) { + if (!syncTypedOpaque(sg, geo, inheritedOpacity)) + geo->setFullyOpaque(qsgGeometryIsOpaque(sg, inheritedOpacity)); + return; + } + if (sg->type() == QSGNode::RenderNodeType) + geo->setFullyOpaque(false); + } + + void syncClipFromQsg(QSGNode *sg, WSGDamageClipNode *clip) const + { + auto *cn = static_cast(sg); + if (auto *def = dynamic_cast(sg)) { + clip->setRadius(def->radius()); + clip->setIsRectangular(def->isRectangular()); + const QRectF r = def->rect(); + clip->setClipRect(r.isEmpty() ? localGeometryRect(sg) : r); + return; + } + clip->setRadius(0); + clip->setIsRectangular(cn->isRectangular()); + clip->setClipRect(localGeometryRect(sg)); + } + + void syncGeometryBounds(QSGNode *sg, WSGDamageNode *g) const + { + auto *geo = g ? g->toGeometry() : nullptr; + if (!geo) + return; + if (sg->type() == QSGNode::GeometryNodeType) { + const QRectF local = localGeometryRect(sg); + geo->setBoundingRect(local); + geo->setHasContent(!local.isEmpty()); + setBoundsKnown(sg, localBoundsKnown(sg)); + return; + } + if (sg->type() == QSGNode::RenderNodeType) { + QRectF local; + if (auto *blit = dynamic_cast(sg)) + local = blit->rect(); + else + local = localGeometryRect(sg); + geo->setBoundingRect(local); + geo->setHasContent(!local.isEmpty()); + setBoundsKnown(sg, localBoundsKnown(sg)); + } + } + + void syncGeometryContent(QSGNode *sg, WSGDamageNode *g) const + { + syncGeometryBounds(sg, g); + syncGeometryOpaque(sg, g, qsgFreshInheritedOpacity(sg)); + } + void resyncOpaqueDescendants(QSGNode *sg, qreal inherited) + { + Node *shadow = nodes ? nodes->value(sg) : nullptr; + if (shadow && shadow->aggregateDamage) { + markAggregateDirty(shadow->aggregateDamage); + return; + } + qreal local = inherited; + if (sg->type() == QSGNode::OpacityNodeType) + local *= static_cast(sg)->opacity(); + if (WSGDamageNode *g = damageOf(sg)) + syncGeometryOpaque(sg, g, local); + for (QSGNode *child = sg->firstChild(); child; child = child->nextSibling()) + resyncOpaqueDescendants(child, local); + } + + bool isCulled(const WSGDamageNode *g) const + { + if (!g) + return false; + if (g->hasContent() && !pixman_region32_not_empty(g->worldValidRegion())) + return true; + const QRect out = outputRect.isEmpty() ? QRect() : outputRect; + if (!out.isEmpty() && !g->worldBounds().intersects(out)) + return true; + return false; + } + + bool isCulled(const Element *el) const + { + if (!el) + return false; + // Unknown raster extent (custom vertex layout, unbounded render + // node): the damage graph stores no bounds, so culling by them would + // drop content in Full mode. Conservatively intersecting instead. + const QSGNode *sg = el->isRenderNode + ? static_cast(static_cast(el)->renderNode) + : static_cast(el->node); + if (hasUnknownBounds(sg)) + return false; + return isCulled(el->damage); + } + + bool isCulled(const QSGNode *sg) const + { + if (hasUnknownBounds(sg)) + return false; + Node *shadow = nodes ? nodes->value(const_cast(sg)) : nullptr; + return isCulled(shadow && shadow->aggregateDamage ? shadow->aggregateDamage : damageOf(sg)); + } + + WSGDamageNode *createFor(QSGNode *sg) + { + WSGDamageNode *g = nullptr; + switch (sg->type()) { + case QSGNode::TransformNodeType: { + auto *t = new WSGDamageTransformNode; + t->setMatrix(static_cast(sg)->matrix()); + g = t; + break; + } + case QSGNode::GeometryNodeType: { + g = new WSGDamageGeometryNode; + syncGeometryContent(sg, g); + break; + } + case QSGNode::RenderNodeType: { + auto *backdrop = new WSGDamageBackdropNode; + syncGeometryContent(sg, backdrop); + if (auto *blit = dynamic_cast(sg)) { + backdrop->setRecopyExpansion(blit->damageExpansion()); + backdrop->setName(blit->debugLabel()); + } + g = backdrop; + break; + } + case QSGNode::OpacityNodeType: { + g = new WSGDamageNode(); + g->setVisible(static_cast(sg)->opacity() > 0); + g->setName(QStringLiteral("opacity")); + return g; + } + case QSGNode::ClipNodeType: { + auto *clip = new WSGDamageClipNode; + syncClipFromQsg(sg, clip); + clip->setName(QStringLiteral("clip")); + return clip; + } + default: + g = new WSGDamageNode(); + break; + } + if (g && g->name().isEmpty()) + g->setName(QStringLiteral("qsg%1").arg(int(sg->type()))); + return g; + } + + void attachMatchingParent(Node *snode) + { + QSGNode *sg = snode->sgNode; + QSGNode *sgParent = sg->parent(); + if (!sgParent) + return; + WSGDamageNode *gParent = damageOf(sgParent); + if (!gParent) + return; + WSGDamageNode *before = nullptr; + for (QSGNode *sibling = sg->nextSibling(); sibling; sibling = sibling->nextSibling()) { + if (WSGDamageNode *mapped = damageOf(sibling)) { + before = mapped; + break; + } + } + if (before) + gParent->insertChildBefore(snode->damage, before); + else + gParent->appendChild(snode->damage); + } + + void syncNode(QSGNode *sg, WSGDamageNode *g, QSGNode::DirtyState state) + { + if (state & QSGNode::DirtyMatrix) { + if (auto *t = g->toTransform()) { + const QMatrix4x4 next = static_cast(sg)->matrix(); + if (t->matrix() != next) + t->setMatrix(next); + } + } + if (state & QSGNode::DirtyGeometry) { + if (auto *clip = g->toClip()) + syncClipFromQsg(sg, clip); + syncGeometryContent(sg, g); + if (auto *geo = g->toGeometry()) { + const QRectF local = geo->boundingRect(); + if (!local.isEmpty()) { + geo->markContentDirty(contentLocalFull(local)); + if (traceDamage() && local.width() * local.height() > 20000) + qCWarning(lcWlDamage) << "TRACE MARK Geometry" + << "qsgType" << int(sg->type()) << "id" << g->id() + << "name" << g->name() << "local" + << local.toAlignedRect() << "state" << int(state); + } + } + } + // Unknown raster extent stores no bounds, so no content mark can cover + // it. Runs after the geometry sync above so the known-state is current + // for bound transitions; any transform/geometry/material/force change + // is then conservatively a full frame. + if (hasUnknownBounds(sg) + && (state & (QSGNode::DirtyMatrix | QSGNode::DirtyGeometry + | QSGNode::DirtyMaterial | QSGNode::DirtyForceUpdate))) { + markFull(); + } + if (auto *blit = dynamic_cast(sg)) { + if (auto *backdrop = g->toBackdrop()) + backdrop->setRecopyExpansion(blit->damageExpansion()); + } + if (state & QSGNode::DirtyOpacity) { + if (sg->type() == QSGNode::OpacityNodeType) + g->setVisible(static_cast(sg)->opacity() > 0); + // Updater::visitOpacityNode has not run yet: inheritedOpacity() is + // stale, so resync from the freshly walked ancestor product. + resyncOpaqueDescendants(sg, qsgFreshInheritedOpacity(sg)); + revealSubtree(sg); + } + if ((state & QSGNode::DirtyMaterial) && g->toGeometry()) { + syncGeometryOpaque(sg, g, qsgFreshInheritedOpacity(sg)); + if (WSGImageNode *image = WSGImageNode::enclosingNode(sg); + image && image->hasExplicitDamage()) { + const WPixmanRegion ®ion = image->damageRegion(); + if (!region.isEmpty()) { + const QRectF br = g->toGeometry()->boundingRect(); + const WPixmanRegion local = toContentLocal(br, region); + g->toGeometry()->markContentDirty(local.native()); + } + } else { + const QRectF local = g->toGeometry()->boundingRect(); + if (!local.isEmpty()) { + g->toGeometry()->markContentDirty(contentLocalFull(local)); + if (traceDamage() && local.width() * local.height() > 20000) + qCWarning(lcWlDamage) << "TRACE MARK Material" + << "qsgType" << int(sg->type()) << "id" << g->id() + << "name" << g->name() << "local" + << local.toAlignedRect() << "state" << int(state); + } + } + } + if ((state & QSGNode::DirtyForceUpdate) && g->toGeometry()) { + const QRectF local = g->toGeometry()->boundingRect(); + if (!local.isEmpty()) + g->toGeometry()->markContentDirty(contentLocalFull(local)); + } + } + + void syncAggregatedRoot(Node *snode, QSGNode::DirtyState state) + { + QSGNode *sg = snode->sgNode; + WSGDamageNode *damage = snode->damage; + if ((state & QSGNode::DirtyMatrix) && damage->toTransform()) { + const QMatrix4x4 next = static_cast(sg)->matrix(); + if (damage->toTransform()->matrix() != next) + damage->toTransform()->setMatrix(next); + } + if ((state & QSGNode::DirtyGeometry) && damage->toClip()) + syncClipFromQsg(sg, damage->toClip()); + if ((state & QSGNode::DirtyOpacity) && sg->type() == QSGNode::OpacityNodeType) + damage->setVisible(static_cast(sg)->opacity() > 0); + markAggregateDirty(snode->aggregateDamage); + } + + void rebuildExactDescendants(Node *parent) + { + for (Node *child = parent->firstChild(); child; child = child->sibling()) { + child->damage = createFor(child->sgNode); + bindElementDamage(child); + attachMatchingParent(child); + rebuildExactDescendants(child); + } + } + + bool destroySubtreeAggregation(QSGNode *subtreeRoot, bool rebuild) + { + auto it = aggregatedSubtrees.find(subtreeRoot); + if (it == aggregatedSubtrees.end()) + return false; + + const AggregatedSubtree aggregate = it.value(); + WSGDamageNode *structuralRoot = aggregate.shadowRoot->damage; + clearAggregateRecursive(aggregate.shadowRoot); + aggregateOwners.remove(aggregate.proxy); + aggregatedSubtrees.erase(it); + + if (aggregate.proxy->parent()) + aggregate.proxy->parent()->removeChild(aggregate.proxy); + delete aggregate.proxy; + + if (!rebuild) + return true; + + if (structuralRoot->toGeometry()) + syncGeometryContent(subtreeRoot, structuralRoot); + bindElementDamage(aggregate.shadowRoot); + rebuildExactDescendants(aggregate.shadowRoot); + return true; + } + + bool setSubtreeAggregation(QSGNode *subtreeRoot, bool enabled, const QRectF &localBounds) + { + if (!enabled) + return destroySubtreeAggregation(subtreeRoot, true); + if (!subtreeRoot || !nodes) + return false; + + const QRectF bounds = localBounds.width() > 0 && localBounds.height() > 0 + ? localBounds.normalized() + : QRectF(); + if (bounds.isEmpty()) + return false; + + auto existing = aggregatedSubtrees.find(subtreeRoot); + if (existing != aggregatedSubtrees.end()) { + existing->proxy->setBoundingRect(bounds); + markAggregateDirty(existing->proxy); + return true; + } + + Node *shadow = nodes->value(subtreeRoot); + if (!shadow || !shadow->damage || subtreeHasAggregate(shadow) + || subtreeContainsBackdrop(subtreeRoot)) { + return false; + } + + auto *proxy = new WSGDamageGeometryNode; + proxy->setName(QStringLiteral("subtree-aggregate")); + proxy->setBoundingRect(bounds); + + bindAggregateRecursive(shadow, proxy, shadow); + deleteDamageChildren(shadow->damage); + if (shadow->damage->toGeometry()) + shadow->damage->setHasContent(false); + shadow->damage->appendChild(proxy); + + aggregatedSubtrees.insert(subtreeRoot, { shadow, proxy }); + aggregateOwners.insert(proxy, subtreeRoot); + return true; + } + + void nodeWasAdded(Node *snode) + { + if (!snode || !snode->sgNode || snode->damage || !isInTrackedTree(snode->sgNode)) + return; + + if (Node *parent = snode->parent(); parent && parent->aggregateDamage) { + WSGDamageGeometryNode *proxy = parent->aggregateDamage; + if (dynamic_cast(snode->sgNode)) { + destroySubtreeAggregation(aggregateOwners.value(proxy), true); + return; + } + bindAggregateRecursive(snode, proxy, nullptr); + markAggregateDirty(proxy); + return; + } + + QSGNode *node = snode->sgNode; + WSGDamageNode *g = createFor(node); + snode->damage = g; + bindElementDamage(snode); + const bool isRoot = !node->parent() || node == trackedRoot; + if (isRoot && !sceneRoot) { + sceneRoot = g; + root.reset(g); + tracker.setRoot(root.get()); + if (!trackedRoot) + trackedRoot = node; + return; + } + attachMatchingParent(snode); + } + + void nodeWasRemoved(Node *snode) + { + if (!snode) + return; + unknownBounds.remove(snode->sgNode); + + if (aggregatedSubtrees.contains(snode->sgNode)) { + destroySubtreeAggregation(snode->sgNode, false); + } else if (snode->aggregateDamage) { + WSGDamageGeometryNode *proxy = snode->aggregateDamage; + markAggregateDirty(proxy); + clearAggregateRecursive(snode); + return; + } + + WSGDamageNode *g = snode->damage; + if (!g) + return; + + if (g == sceneRoot) { + sceneRoot = nullptr; + if (g->firstChild()) + clearDamagePointers(g); + else { + snode->damage = nullptr; + bindElementDamage(snode); + } + tracker.setRoot(nullptr); + root.release(); + if (g->parent()) + g->parent()->removeChild(g); + delete g; + return; + } + + snode->damage = nullptr; + bindElementDamage(snode); + if (g->parent()) + g->parent()->removeChild(g); + delete g; + } + + void addExistingSubtree(Node *snode) + { + if (!snode) + return; + nodeWasAdded(snode); + for (Node *child = snode->firstChild(); child; child = child->sibling()) + addExistingSubtree(child); + } + + void detachNodes() + { + if (!nodes) + return; + for (Node *node : std::as_const(*nodes)) { + node->damage = nullptr; + node->aggregateDamage = nullptr; + bindElementDamage(node); + } + nodes = nullptr; + } + + void nodeChanged(Node *snode, QSGNode::DirtyState state) + { + if (!snode || !snode->sgNode || !isInTrackedTree(snode->sgNode)) + return; + if (snode->aggregateDamage) { + if (aggregatedSubtrees.contains(snode->sgNode)) + syncAggregatedRoot(snode, state); + else + markAggregateDirty(snode->aggregateDamage); + return; + } + if (snode->damage) + syncNode(snode->sgNode, snode->damage, state); + } + + void revealSubtree(QSGNode *node) + { + if (!node || !isInTrackedTree(node)) + return; + if (hasUnknownBounds(node)) { + markFull(); + return; + } + Node *shadow = nodes ? nodes->value(node) : nullptr; + if (shadow && shadow->aggregateDamage) { + markAggregateDirty(shadow->aggregateDamage); + return; + } + WSGDamageNode *g = damageOf(node); + if (!g) + return; + if (auto *geo = g->toGeometry()) { + const QRectF local = geo->boundingRect(); + if (!local.isEmpty()) + geo->markContentDirty(contentLocalFull(local)); + } + for (QSGNode *child = node->firstChild(); child; child = child->nextSibling()) + revealSubtree(child); + } + + void markFull() + { + render = WDamageRegion(true); + flush = WDamageRegion(true); + } + + // Overlay extra for this frame's draw only. Must not touch flush: + // writing highlight/full back to wlr_damage_ring makes the next + // swapchain slot whole-buffer dirty and coversOutput forever. + void setFlushFull() + { + render = WDamageRegion(true); + } + + static bool traceDamage() + { + static const bool on = qEnvironmentVariableIntValue("WAYLIB_TRACE_DAMAGE") != 0; + return on; + } + + void setTrackedRoot(QSGNode *r) + { + trackedRoot = r; + } + + const WDamageRegion &lastRenderRegion() const + { + return render; + } + + const WDamageRegion &lastFlushRegion() const + { + return flush; + } + + const WPixmanRegion &pendingRegion() const + { + return render.region; + } + + void setDamage(const WDamageRegion &f, const WDamageRegion &r, const QRect &outRect) + { + flush = f; + render = r; + outputRect = outRect; + } +}; + +void Renderer::createDamageTree() +{ + if (m_damageTree) + return; + + auto damageTree = std::make_unique(); + damageTree->nodes = &m_nodes; + + QSGNode *trackedRoot = m_damageTrackedRoot; + if (!trackedRoot || !m_nodes.contains(trackedRoot)) + trackedRoot = rootNode(); + m_damageTrackedRoot = trackedRoot; + damageTree->setTrackedRoot(trackedRoot); + if (Node *shadowRoot = m_nodes.value(trackedRoot)) + damageTree->addExistingSubtree(shadowRoot); + + damageTree->markFull(); + damageTree->modeSwitchFull = true; + m_damageTree = std::move(damageTree); +} + +void Renderer::destroyDamageTree() +{ + if (!m_damageTree) + return; + + m_damageTree->detachNodes(); + m_damageTree.reset(); +} + +Renderer::Renderer(QSGDefaultRenderContext *ctx, QSGRendererInterface::RenderMode renderMode) + : QSGRenderer(ctx) + , m_context(ctx) + , m_renderMode(renderMode) + , m_opaqueRenderList(64) + , m_alphaRenderList(64) + , m_nextRenderOrder(0) + , m_partialRebuild(false) + , m_partialRebuildRoot(nullptr) + , m_forceNoDepthBuffer(false) + , m_opaqueBatches(16) + , m_alphaBatches(16) + , m_batchPool(16) + , m_elementsToDelete(64) + , m_tmpAlphaElements(16) + , m_tmpOpaqueElements(16) + , m_vboPool(16) + , m_iboPool(16) + , m_vboPoolCost(0) + , m_iboPoolCost(0) + , m_rebuild(FullRebuild) + , m_zRange(0) +#if defined(QSGBATCHRENDERER_INVALIDATE_WEDGED_NODES) + , m_renderOrderRebuildLower(-1) + , m_renderOrderRebuildUpper(-1) +#endif + , m_minimumOrderPadding(4) + , m_currentMaterial(nullptr) + , m_currentShader(nullptr) + , m_vertexUploadPool(256) + , m_indexUploadPool(64) +{ + m_rhi = m_context->rhi(); + Q_ASSERT(m_rhi); // no more direct OpenGL code path in Qt 6 + + m_ubufAlignment = m_rhi->ubufAlignment(); + + m_uint32IndexForRhi = !m_rhi->isFeatureSupported(QRhi::NonFourAlignedEffectiveIndexBufferOffset); + if (qEnvironmentVariableIntValue("QSG_RHI_UINT32_INDEX")) + m_uint32IndexForRhi = true; + + bool ok = false; + int padding = qEnvironmentVariableIntValue("QSG_BATCHRENDERER_MINIMUM_ORDER_PADDING", &ok); + if (ok) + m_minimumOrderPadding = padding; + + m_visualizer = new RhiVisualizer(this); + + setNodeUpdater(new Updater(this)); + + // The shader manager is shared between renderers (think for example Item + // layers that create a new Renderer each) with the same rendercontext (and + // so same QRhi). + m_shaderManager = ctx->findChild(QString(), Qt::FindDirectChildrenOnly); + if (!m_shaderManager) { + m_shaderManager = new ShaderManager(ctx); + m_shaderManager->setObjectName(QStringLiteral("__qt_ShaderManager")); + m_shaderManager->setParent(ctx); + QObject::connect(ctx, SIGNAL(invalidated()), m_shaderManager, SLOT(invalidated()), Qt::DirectConnection); + } + + m_batchNodeThreshold = qt_sg_envInt("QSG_RENDERER_BATCH_NODE_THRESHOLD", 64); + m_batchVertexThreshold = qt_sg_envInt("QSG_RENDERER_BATCH_VERTEX_THRESHOLD", 1024); + m_srbPoolThreshold = qt_sg_envInt("QSG_RENDERER_SRB_POOL_THRESHOLD", 1024); + m_bufferPoolSizeLimit = qt_sg_envInt("QSG_RENDERER_BUFFER_POOL_LIMIT", DEFAULT_BUFFER_POOL_SIZE_LIMIT); + + if (Q_UNLIKELY(debug_build() || debug_render() || debug_pools())) { + qDebug("Batch thresholds: nodes: %d vertices: %d srb pool: %d buffer pool: %d", + m_batchNodeThreshold, m_batchVertexThreshold, m_srbPoolThreshold, m_bufferPoolSizeLimit); + } + + m_damageMode = defaultDamageMode(); + if (m_damageMode != DamageMode::Off) + createDamageTree(); +} + +static void qsg_wipeBuffer(Buffer *buffer) +{ + delete buffer->buf; + + // The free here is ok because we're in one of two situations. + // 1. We're using the upload pool in which case unmap will have set the + // data pointer to 0 and calling free on 0 is ok. + // 2. We're using dedicated buffers because of visualization or IBO workaround + // and the data something we malloced and must be freed. + free(buffer->data); +} + +static void qsg_wipeBatch(Batch *batch) +{ + qsg_wipeBuffer(&batch->vbo); + qsg_wipeBuffer(&batch->ibo); + delete batch->ubuf; + batch->stencilClipState.reset(); + delete batch; +} + +Renderer::~Renderer() +{ + destroyDamageTree(); + if (m_rhi) { + // Clean up batches and buffers + for (int i = 0; i < m_opaqueBatches.size(); ++i) + qsg_wipeBatch(m_opaqueBatches.at(i)); + for (int i = 0; i < m_alphaBatches.size(); ++i) + qsg_wipeBatch(m_alphaBatches.at(i)); + for (int i = 0; i < m_batchPool.size(); ++i) + qsg_wipeBatch(m_batchPool.at(i)); + for (int i = 0; i < m_vboPool.size(); ++i) + delete m_vboPool.at(i); + for (int i = 0; i < m_iboPool.size(); ++i) + delete m_iboPool.at(i); + } + + for (Node *n : std::as_const(m_nodes)) { + if (n->type() == QSGNode::GeometryNodeType) { + Element *e = n->element(); + if (!e->removed) + m_elementsToDelete.add(e); + } else if (n->type() == QSGNode::ClipNodeType) { + delete n->clipInfo(); + } else if (n->type() == QSGNode::RenderNodeType) { + RenderNodeElement *e = n->renderNodeElement(); + if (!e->removed) + m_elementsToDelete.add(e); + } + + m_nodeAllocator.release(n); + } + + // Remaining elements... + for (int i=0; i the cached data + // in the rhi shaders have to be purged as it may refer to samplers we + // are going to destroy. + m_shaderManager->clearCachedRendererData(); + + qDeleteAll(m_samplers); + m_stencilClipCommon.reset(); + delete m_dummyTexture; + m_visualizer->releaseResources(); +} + +void Renderer::releaseCachedResources() +{ + m_shaderManager->invalidated(); + + destroyGraphicsResources(); + + m_samplers.clear(); + m_dummyTexture = nullptr; + + m_rhi->releaseCachedResources(); + + m_vertexUploadPool.shrink(0); + m_vertexUploadPool.reset(); + m_indexUploadPool.shrink(0); + m_indexUploadPool.reset(); + + for (int i = 0; i < m_vboPool.size(); ++i) + delete m_vboPool.at(i); + m_vboPool.reset(); + m_vboPoolCost = 0; + + for (int i = 0; i < m_iboPool.size(); ++i) + delete m_iboPool.at(i); + m_iboPool.reset(); + m_iboPoolCost = 0; +} + +void Renderer::invalidateAndRecycleBatch(Batch *b) +{ + if (b->vbo.buf != nullptr && m_vboPoolCost + b->vbo.buf->size() <= quint32(m_bufferPoolSizeLimit)) { + m_vboPool.add(b->vbo.buf); + m_vboPoolCost += b->vbo.buf->size(); + } else { + delete b->vbo.buf; + } + if (b->ibo.buf != nullptr && m_iboPoolCost + b->ibo.buf->size() <= quint32(m_bufferPoolSizeLimit)) { + m_iboPool.add(b->ibo.buf); + m_iboPoolCost += b->ibo.buf->size(); + } else { + delete b->ibo.buf; + } + b->vbo.buf = nullptr; + b->ibo.buf = nullptr; + b->invalidate(); + for (int i=0; imode() == Visualizer::VisualizeNothing) { + // Common case, use a shared memory pool for uploading vertex data to avoid + // excessive reevaluation + QDataBuffer &pool = isIndexBuf ? m_indexUploadPool : m_vertexUploadPool; + if (byteSize > quint32(pool.size())) + pool.resize(byteSize); + buffer->data = pool.data(); + } else if (buffer->size != byteSize) { + free(buffer->data); + buffer->data = (char *) malloc(byteSize); + Q_CHECK_PTR(buffer->data); + } + buffer->size = byteSize; +} + +void Renderer::unmap(Buffer *buffer, bool isIndexBuf) +{ + // Batches are pooled and reused which means the QRhiBuffer will be + // still valid in a recycled Batch. We only hit the newBuffer() path + // when there are no buffers to recycle. + QDataBuffer *bufferPool = isIndexBuf ? &m_iboPool : &m_vboPool; + if (!buffer->buf && bufferPool->isEmpty()) { + buffer->buf = m_rhi->newBuffer(QRhiBuffer::Immutable, + isIndexBuf ? QRhiBuffer::IndexBuffer : QRhiBuffer::VertexBuffer, + buffer->size); + if (!buffer->buf->create()) { + qWarning("Failed to build vertex/index buffer of size %u", buffer->size); + delete buffer->buf; + buffer->buf = nullptr; + } + } else { + if (!buffer->buf) { + const quint32 expectedSize = buffer->size; + qsizetype foundBufferIndex = 0; + for (qsizetype i = 0; i < bufferPool->size(); ++i) { + QRhiBuffer *testBuffer = bufferPool->at(i); + if (!buffer->buf + || (testBuffer->size() >= expectedSize && testBuffer->size() < buffer->buf->size()) + || (testBuffer->size() < expectedSize && testBuffer->size() > buffer->buf->size())) { + foundBufferIndex = i; + buffer->buf = testBuffer; + if (buffer->buf->size() == expectedSize) + break; + } + } + + const qsizetype lastBufferIndex = bufferPool->size() - 1; + if (foundBufferIndex < lastBufferIndex) { + qSwap(bufferPool->data()[foundBufferIndex], + bufferPool->data()[lastBufferIndex]); + } + if (isIndexBuf) + m_iboPoolCost -= bufferPool->data()[lastBufferIndex]->size(); + else + m_vboPoolCost -= bufferPool->data()[lastBufferIndex]->size(); + bufferPool->pop_back(); + } + + bool needsRebuild = false; + if (buffer->buf->size() < buffer->size) { + buffer->buf->setSize(buffer->size); + needsRebuild = true; + } + if (buffer->buf->type() != QRhiBuffer::Dynamic + && buffer->nonDynamicChangeCount > DYNAMIC_VERTEX_INDEX_BUFFER_THRESHOLD) + { + buffer->buf->setType(QRhiBuffer::Dynamic); + buffer->nonDynamicChangeCount = 0; + needsRebuild = true; + } + if (needsRebuild) { + if (!buffer->buf->create()) { + qWarning("Failed to (re)build vertex/index buffer of size %u", buffer->size); + delete buffer->buf; + buffer->buf = nullptr; + } + } + } + if (buffer->buf) { + if (buffer->buf->type() != QRhiBuffer::Dynamic) { + m_resourceUpdates->uploadStaticBuffer(buffer->buf, 0, buffer->size, buffer->data); + buffer->nonDynamicChangeCount += 1; + } else { + if (m_rhi->resourceLimit(QRhi::FramesInFlight) == 1) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 1) + buffer->buf->fullDynamicBufferUpdateForCurrentFrame(buffer->data, buffer->size); +#else + buffer->buf->fullDynamicBufferUpdateForCurrentFrame(buffer->data); +#endif + } else { + m_resourceUpdates->updateDynamicBuffer(buffer->buf, 0, buffer->size, buffer->data); + } + } + } + if (m_visualizer->mode() == Visualizer::VisualizeNothing) + buffer->data = nullptr; +} + +BatchRootInfo *Renderer::batchRootInfo(Node *node) +{ + BatchRootInfo *info = node->rootInfo(); + if (!info) { + if (node->type() == QSGNode::ClipNodeType) + info = new ClipBatchRootInfo; + else { + Q_ASSERT(node->type() == QSGNode::TransformNodeType); + info = new BatchRootInfo; + } + node->data = info; + } + return info; +} + +void Renderer::removeBatchRootFromParent(Node *childRoot) +{ + BatchRootInfo *childInfo = batchRootInfo(childRoot); + if (!childInfo->parentRoot) + return; + BatchRootInfo *parentInfo = batchRootInfo(childInfo->parentRoot); + + Q_ASSERT(parentInfo->subRoots.contains(childRoot)); + parentInfo->subRoots.remove(childRoot); + childInfo->parentRoot = nullptr; +} + +void Renderer::registerBatchRoot(Node *subRoot, Node *parentRoot) +{ + BatchRootInfo *subInfo = batchRootInfo(subRoot); + BatchRootInfo *parentInfo = batchRootInfo(parentRoot); + subInfo->parentRoot = parentRoot; + parentInfo->subRoots << subRoot; +} + +bool Renderer::changeBatchRoot(Node *node, Node *root) +{ + BatchRootInfo *subInfo = batchRootInfo(node); + if (subInfo->parentRoot == root) + return false; + if (subInfo->parentRoot) { + BatchRootInfo *oldRootInfo = batchRootInfo(subInfo->parentRoot); + oldRootInfo->subRoots.remove(node); + } + BatchRootInfo *newRootInfo = batchRootInfo(root); + newRootInfo->subRoots << node; + subInfo->parentRoot = root; + return true; +} + +void Renderer::nodeChangedBatchRoot(Node *node, Node *root) +{ + if (node->type() == QSGNode::ClipNodeType || node->isBatchRoot) { + // When we reach a batchroot, we only need to update it. Its subtree + // is relative to that root, so no need to recurse further. + changeBatchRoot(node, root); + return; + } else if (node->type() == QSGNode::GeometryNodeType) { + // Only need to change the root as nodeChanged anyway flags a full update. + Element *e = node->element(); + if (e) { + e->root = root; + e->boundsComputed = false; + } + } else if (node->type() == QSGNode::RenderNodeType) { + RenderNodeElement *e = node->renderNodeElement(); + if (e) + e->root = root; + } + + SHADOWNODE_TRAVERSE(node) + nodeChangedBatchRoot(child, root); +} + +void Renderer::nodeWasTransformed(Node *node, int *vertexCount) +{ + if (node->type() == QSGNode::GeometryNodeType) { + QSGGeometryNode *gn = static_cast(node->sgNode); + *vertexCount += gn->geometry()->vertexCount(); + Element *e = node->element(); + if (e) { + e->boundsComputed = false; + if (e->batch) { + if (!e->batch->isOpaque) { + invalidateBatchAndOverlappingRenderOrders(e->batch); + } else if (e->batch->merged) { + e->batch->needsUpload = true; + } + } + } + } + + SHADOWNODE_TRAVERSE(node) + nodeWasTransformed(child, vertexCount); +} + +void Renderer::nodeWasAdded(QSGNode *node, Node *shadowParent) +{ + Q_ASSERT(!m_nodes.contains(node)); + if (node->isSubtreeBlocked()) + return; + + Node *snode = m_nodeAllocator.allocate(); + snode->sgNode = node; + snode->localRectFn = localRectFnFor(node); + m_nodes.insert(node, snode); + if (shadowParent) + shadowParent->append(snode); + + if (m_damageTree) + m_damageTree->nodeWasAdded(snode); + + if (node->type() == QSGNode::GeometryNodeType) { + snode->data = m_elementAllocator.allocate(); + snode->element()->setNode(static_cast(node)); + snode->element()->damage = snode->aggregateDamage ? snode->aggregateDamage : snode->damage; + + } else if (node->type() == QSGNode::ClipNodeType) { + snode->data = new ClipBatchRootInfo; + m_rebuild |= FullRebuild; + + } else if (node->type() == QSGNode::RenderNodeType) { + QSGRenderNode *rn = static_cast(node); + RenderNodeElement *e = new RenderNodeElement(rn); + snode->data = e; + e->damage = snode->aggregateDamage ? snode->aggregateDamage : snode->damage; + Q_ASSERT(!m_renderNodeElements.contains(rn)); + m_renderNodeElements.insert(e->renderNode, e); + if (!rn->flags().testFlag(QSGRenderNode::DepthAwareRendering)) + m_forceNoDepthBuffer = true; + m_rebuild |= FullRebuild; + } + + QSGNODE_TRAVERSE(node) + nodeWasAdded(child, snode); +} + +void Renderer::nodeWasRemoved(Node *node) +{ + // Prefix traversal as removeBatchRootFromParent below removes nodes + // in a bottom-up manner. Note that we *cannot* use SHADOWNODE_TRAVERSE + // here, because we delete 'child' (when recursed, down below), so we'd + // have a use-after-free. + { + Node *child = node->firstChild(); + while (child) { + // Remove (and delete) child + node->remove(child); + nodeWasRemoved(child); + child = node->firstChild(); + } + } + + if (m_damageTree) + m_damageTree->nodeWasRemoved(node); + + if (node->type() == QSGNode::GeometryNodeType) { + Element *e = node->element(); + if (e) { + e->removed = true; + m_elementsToDelete.add(e); + e->node = nullptr; + if (e->root) { + BatchRootInfo *info = batchRootInfo(e->root); + info->availableOrders++; + } + if (e->batch) { + e->batch->needsUpload = true; + e->batch->needsPurge = true; + } + + } + + } else if (node->type() == QSGNode::ClipNodeType) { + removeBatchRootFromParent(node); + delete node->clipInfo(); + m_rebuild |= FullRebuild; + m_taggedRoots.remove(node); + + } else if (node->isBatchRoot) { + removeBatchRootFromParent(node); + delete node->rootInfo(); + m_rebuild |= FullRebuild; + m_taggedRoots.remove(node); + + } else if (node->type() == QSGNode::RenderNodeType) { + RenderNodeElement *e = m_renderNodeElements.take(static_cast(node->sgNode)); + if (e) { + e->removed = true; + m_elementsToDelete.add(e); + if (m_renderNodeElements.isEmpty()) { + m_forceNoDepthBuffer = false; + // Must have a full rebuild given useDepthBuffer() now returns + // a different value than before, meaning there can once again + // be an opaque pass. + m_rebuild |= FullRebuild; + } + + if (e->batch != nullptr) + e->batch->needsPurge = true; + } + } + + Q_ASSERT(m_nodes.contains(node->sgNode)); + + m_nodeAllocator.release(m_nodes.take(node->sgNode)); +} + +void Renderer::turnNodeIntoBatchRoot(Node *node) +{ + if (Q_UNLIKELY(debug_change())) qDebug(" - new batch root"); + m_rebuild |= FullRebuild; + node->isBatchRoot = true; + node->becameBatchRoot = true; + + Node *p = node->parent(); + while (p) { + if (p->type() == QSGNode::ClipNodeType || p->isBatchRoot) { + registerBatchRoot(node, p); + break; + } + p = p->parent(); + } + + SHADOWNODE_TRAVERSE(node) + nodeChangedBatchRoot(child, node); +} + +const WDamageRegion &Renderer::lastRenderRegion() const +{ + if (!tracksDamage()) { + static const WDamageRegion s_full(true); + return s_full; + } + return m_damageTree->lastRenderRegion(); +} + +const WDamageRegion &Renderer::lastFlushRegion() const +{ + if (!tracksDamage()) { + static const WDamageRegion s_full(true); + return s_full; + } + return m_damageTree->lastFlushRegion(); +} + +WSGDamageTracker *Renderer::damageTracker() const +{ + return m_damageTree ? &m_damageTree->tracker : nullptr; +} + + +void Renderer::setDamage(const WDamageRegion &flush, + const WDamageRegion &render, + const QRect &sceneOutputRect, + WSGViewport *viewport) +{ + if (!m_damageTree) + return; + m_damageTree->viewport = viewport; + // Consume a pending mode-switch full here (not in the caller): a switch + // between commit() and setDamage() would otherwise be overwritten unseen. + // One-shot: cleared unconditionally so a stored full can never self-latch. + const bool switchFull = m_damageTree->modeSwitchFull; + m_damageTree->modeSwitchFull = false; + const QRect targetRect = sceneOutputRect.isEmpty() ? deviceRect() : sceneOutputRect; + if (switchFull) + m_damageTree->setDamage(WDamageRegion(true), WDamageRegion(true), targetRect); + else + m_damageTree->setDamage(flush, render, targetRect); +} + +bool Renderer::setDamageSubtreeAggregation(QSGNode *subtreeRoot, + bool enabled, + const QRectF &localBounds) +{ + if (!m_damageTree + || !m_damageTree->setSubtreeAggregation(subtreeRoot, enabled, localBounds)) + return false; + return true; +} + +void Renderer::setDamageScissorTarget(QRhiRenderTarget *rt) +{ + if (m_damageTree) { + m_damageTree->scissorTarget = rt; + if (!rt) + m_damageTree->viewport = nullptr; + } +} + +bool Renderer::isCompositorOutputPass() const +{ + // Off mode has no damage-pass filtering; render every blitter normally. + if (!m_damageTree) + return true; + return m_damageTree->scissorTarget + && renderTarget().rt == m_damageTree->scissorTarget; +} + +void Renderer::expandDamageScissor(const WPixmanRegion &sceneRegion) +{ + if (!m_damageTree || sceneRegion.isEmpty()) + return; + m_damageTree->extraScissor += sceneRegion; + if (m_damageTree->flush.isFull) + return; + m_damageTree->flush.region += sceneRegion; +} + + +Renderer::DamageMode Renderer::defaultDamageMode() +{ + static const DamageMode parsed = []() { + const QByteArray raw = qgetenv("WAYLIB_DAMAGE"); + if (raw.isEmpty()) + return DamageMode::Full; + const QByteArray v = raw.trimmed().toLower(); + if (v == "0" || v == "off" || v == "false" || v == "no" || v == "none") { + qCInfo(lcWlRenderer) << "WAYLIB_DAMAGE=" << raw.constData() + << "(off: no tracker, present whole buffer)"; + return DamageMode::Off; + } + if (v == "commit" || v == "output" || v == "present" || v == "report") { + qCInfo(lcWlRenderer) << "WAYLIB_DAMAGE=" << raw.constData() + << "(commit: track and report, draw fullscreen)"; + return DamageMode::Commit; + } + if (v == "1" || v == "on" || v == "true" || v == "yes" || v == "full" || v == "manage") + return DamageMode::Full; + qCWarning(lcWlRenderer) << "Unknown WAYLIB_DAMAGE=" << raw.constData() << ", using full"; + return DamageMode::Full; + }(); + return parsed; +} + +void Renderer::setDamageMode(DamageMode mode) +{ + if (m_damageMode == mode) + return; + m_damageMode = mode; + if (mode == DamageMode::Off) { + destroyDamageTree(); + } else if (!m_damageTree) { + createDamageTree(); + } else { + m_damageTree->markFull(); + m_damageTree->modeSwitchFull = true; + } + qCDebug(lcWlRenderer) << "damage mode" + << (mode == DamageMode::Off ? "off" + : mode == DamageMode::Commit ? "commit" + : "full") + << "for renderer" << this; +} + +static bool textureRenderTargetPreservesColor(QRhiRenderTarget *rt) +{ + if (!rt || rt->resourceType() != QRhiResource::TextureRenderTarget) + return false; + return static_cast(rt)->flags().testFlag( + QRhiTextureRenderTarget::PreserveColorContents); +} + +QRect Renderer::sceneRectToNativeScissor(const QRectF &bbox, bool pad) const +{ + if (bbox.isEmpty()) + return { }; + + const QMatrix4x4 m = projectionMatrixWithNativeNDC(0); + // Same mapping as updateClipState(): only axis-aligned (or 90°) orthographic. + if (!qFuzzyIsNull(m(3, 0)) || !qFuzzyIsNull(m(3, 1))) + return { }; + + const bool noRotate = qFuzzyIsNull(m(0, 1)) && qFuzzyIsNull(m(1, 0)); + const bool isRotate90 = qFuzzyIsNull(m(0, 0)) && qFuzzyIsNull(m(1, 1)); + if (!noRotate && !isRotate90) + return { }; + + const qreal invW = qFuzzyIsNull(m(3, 3)) ? 1.0 : 1.0 / m(3, 3); + qreal fx1, fy1, fx2, fy2; + if (noRotate) { + fx1 = (bbox.left() * m(0, 0) + m(0, 3)) * invW; + fy1 = (bbox.bottom() * m(1, 1) + m(1, 3)) * invW; + fx2 = (bbox.right() * m(0, 0) + m(0, 3)) * invW; + fy2 = (bbox.top() * m(1, 1) + m(1, 3)) * invW; + } else { + fx1 = (bbox.bottom() * m(0, 1) + m(0, 3)) * invW; + fy1 = (bbox.left() * m(1, 0) + m(1, 3)) * invW; + fx2 = (bbox.top() * m(0, 1) + m(0, 3)) * invW; + fy2 = (bbox.right() * m(1, 0) + m(1, 3)) * invW; + } + + if (fx1 > fx2) + qSwap(fx1, fx2); + if (fy1 > fy2) + qSwap(fy1, fy2); + + const QRect deviceRect = this->deviceRect(); + const qint32 ix1 = qRound((fx1 + 1) * deviceRect.width() * qreal(0.5)); + const qint32 iy1 = qRound((fy1 + 1) * deviceRect.height() * qreal(0.5)); + const qint32 ix2 = qRound((fx2 + 1) * deviceRect.width() * qreal(0.5)); + const qint32 iy2 = qRound((fy2 + 1) * deviceRect.height() * qreal(0.5)); + QRect native(ix1, iy1, ix2 - ix1, iy2 - iy1); + if (pad) + native = native.adjusted(-1, -1, 1, 1); + return native.intersected(deviceRect); +} + +// Same cap as wlr_damage_ring: too many rects collapse to extents. +static constexpr int kMaxDamageScissorRects = 20; + +void Renderer::commitFlushRegion() +{ + if (!m_damageTree) + return; + // Nested QSGLayer / extra-QRhi content that reuses this Renderer must + // not clip its draw to the output pass's scene-space scissors. + if (m_damageTree->scissorTarget + && renderTarget().rt != m_damageTree->scissorTarget) + return; + if (Q_UNLIKELY(lcWlDamage().isDebugEnabled())) { + const QString region = WSGDamageLog::describe(lastRenderRegion().region, + lastRenderRegion().isFull); + qCDebug(lcWlDamage) << WSGDamageLog::frameTag() + << "flush compositor dirty to damage ring:" << region; + } +} + + +void Renderer::nodeChanged(QSGNode *node, QSGNode::DirtyState state) +{ + // Pin the output scene root. Nested QSGLayer / blitter content + // setRootNode() on a reused renderer must not retarget the tracker to + // an offscreen WRenderBufferNode tree. + if (m_damageTree) { + if (QSGNode *root = rootNode()) { + if (!m_damageTrackedRoot) + m_damageTrackedRoot = root; + m_damageTree->setTrackedRoot(m_damageTrackedRoot); + } + if (m_damageTree->scissorTarget) + WSGDamageLog::beginOutputFrame("scene graph dirty (sync)"); + } +#ifndef QT_NO_DEBUG_OUTPUT + if (Q_UNLIKELY(debug_change())) { + QDebug debug = qDebug(); + debug << "dirty:"; + if (state & QSGNode::DirtyGeometry) + debug << "Geometry"; + if (state & QSGNode::DirtyMaterial) + debug << "Material"; + if (state & QSGNode::DirtyMatrix) + debug << "Matrix"; + if (state & QSGNode::DirtyNodeAdded) + debug << "Added"; + if (state & QSGNode::DirtyNodeRemoved) + debug << "Removed"; + if (state & QSGNode::DirtyOpacity) + debug << "Opacity"; + if (state & QSGNode::DirtySubtreeBlocked) + debug << "SubtreeBlocked"; + if (state & QSGNode::DirtyForceUpdate) + debug << "ForceUpdate"; + + // when removed, some parts of the node could already have been destroyed + // so don't debug it out. + if (state & QSGNode::DirtyNodeRemoved) + debug << (void *) node << node->type(); + else + debug << node; + } +#endif + // As this function calls nodeChanged recursively, we do it at the top + // to avoid that any of the others are processed twice. + if (state & QSGNode::DirtySubtreeBlocked) { + Node *sn = m_nodes.value(node); + + // Force a batch rebuild if this includes an opacity change + if (state & QSGNode::DirtyOpacity) + m_rebuild |= FullRebuild; + + bool blocked = node->isSubtreeBlocked(); + if (blocked && sn) { + nodeChanged(node, QSGNode::DirtyNodeRemoved); + Q_ASSERT(m_nodes.value(node) == 0); + } else if (!blocked && !sn) { + nodeChanged(node, QSGNode::DirtyNodeAdded); + } else if (!blocked && sn && tracksDamage()) { + // hideSource unhide: already in the shadow tree, so Added did + // not run. Recopy descendant blitters. + m_damageTree->revealSubtree(node); + } + return; + } + + if (state & QSGNode::DirtyNodeAdded) { + if (nodeUpdater()->isNodeBlocked(node, rootNode())) { + QSGRenderer::nodeChanged(node, state); + return; + } + if (node == rootNode()) + nodeWasAdded(node, nullptr); + else + nodeWasAdded(node, m_nodes.value(node->parent())); + } + + Node *shadowNode = m_nodes.value(node); + if (m_damageTree && !(state & QSGNode::DirtyNodeRemoved) && shadowNode) + m_damageTree->nodeChanged(shadowNode, state); + + // Mark this node dirty in the shadow tree. + + // Blocked subtrees won't have shadow nodes, so we can safely abort + // here.. + if (!shadowNode) { + QSGRenderer::nodeChanged(node, state); + return; + } + + shadowNode->dirtyState |= state; + + if (state & QSGNode::DirtyMatrix && !shadowNode->isBatchRoot) { + Q_ASSERT(node->type() == QSGNode::TransformNodeType); + if (W_PRIVATE_MEMBER(*node, WSG_QSGNode_m_subtreeRenderableCount{}) > m_batchNodeThreshold) { + turnNodeIntoBatchRoot(shadowNode); + } else { + int vertices = 0; + nodeWasTransformed(shadowNode, &vertices); + if (vertices > m_batchVertexThreshold) { + turnNodeIntoBatchRoot(shadowNode); + } + } + } + + if (state & QSGNode::DirtyGeometry && node->type() == QSGNode::GeometryNodeType) { + QSGGeometryNode *gn = static_cast(node); + Element *e = shadowNode->element(); + if (e) { + const quint8 nodeMutabilityGroup = wsgNodeMutabilityGroup(gn); + if (nodeMutabilityGroup != e->mutabilityGroup) { + m_rebuild |= Renderer::FullRebuild; + e->mutabilityGroup = nodeMutabilityGroup; + } + + e->boundsComputed = false; + Batch *b = e->batch; + if (b) { + if (!e->batch->geometryWasChanged(gn) || !e->batch->isOpaque) { + invalidateBatchAndOverlappingRenderOrders(e->batch); + } else { + b->needsUpload = true; + } + } + } + } + + if (state & QSGNode::DirtyMaterial && node->type() == QSGNode::GeometryNodeType) { + Element *e = shadowNode->element(); + if (e) { + bool blended = hasMaterialWithBlending(static_cast(node)); + if (e->isMaterialBlended != blended) { + m_rebuild |= Renderer::FullRebuild; + e->isMaterialBlended = blended; + } else if (e->batch) { + if (e->batch->isMaterialCompatible(e) == BatchBreaksOnCompare) + invalidateBatchAndOverlappingRenderOrders(e->batch); + } else { + m_rebuild |= Renderer::BuildBatches; + } + } + } + + // Mark the shadow tree dirty all the way back to the root... + QSGNode::DirtyState dirtyChain = state & (QSGNode::DirtyNodeAdded + | QSGNode::DirtyOpacity + | QSGNode::DirtyMatrix + | QSGNode::DirtySubtreeBlocked + | QSGNode::DirtyForceUpdate); + if (dirtyChain != 0) { + dirtyChain = QSGNode::DirtyState(dirtyChain << 16); + Node *sn = shadowNode->parent(); + while (sn) { + sn->dirtyState |= dirtyChain; + sn = sn->parent(); + } + } + + // Delete happens at the very end because it deletes the shadownode. + if (state & QSGNode::DirtyNodeRemoved) { + Node *parent = shadowNode->parent(); + if (parent) + parent->remove(shadowNode); + nodeWasRemoved(shadowNode); + Q_ASSERT(m_nodes.value(node) == 0); + } + + QSGRenderer::nodeChanged(node, state); +} + +/* + * Traverses the tree and builds two list of geometry nodes. One for + * the opaque and one for the translucent. These are populated + * in the order they should visually appear in, meaning first + * to the back and last to the front. + * + * We split opaque and translucent as we can perform different + * types of reordering / batching strategies on them, depending + * + * Note: It would be tempting to use the shadow nodes instead of the QSGNodes + * for traversal to avoid hash lookups, but the order of the children + * is important and they are not preserved in the shadow tree, so we must + * use the actual QSGNode tree. + */ +void Renderer::buildRenderLists(QSGNode *node) +{ + if (node->isSubtreeBlocked()) + return; + + Node *shadowNode = m_nodes.value(node); + Q_ASSERT(shadowNode); + + if (node->type() == QSGNode::GeometryNodeType) { + QSGGeometryNode *gn = static_cast(node); + + Element *e = shadowNode->element(); + Q_ASSERT(e); + + bool opaque = gn->inheritedOpacity() > OPAQUE_LIMIT && !(gn->activeMaterial()->flags() & QSGMaterial::Blending); + if (opaque && useDepthBuffer()) + m_opaqueRenderList << e; + else + m_alphaRenderList << e; + + e->order = ++m_nextRenderOrder; + // Used while rebuilding partial roots. + if (m_partialRebuild) + e->orphaned = false; + + } else if (node->type() == QSGNode::ClipNodeType || shadowNode->isBatchRoot) { + Q_ASSERT(m_nodes.contains(node)); + BatchRootInfo *info = batchRootInfo(shadowNode); + if (node == m_partialRebuildRoot) { + m_nextRenderOrder = info->firstOrder; + QSGNODE_TRAVERSE(node) + buildRenderLists(child); + m_nextRenderOrder = info->lastOrder + 1; + } else { + int currentOrder = m_nextRenderOrder; + QSGNODE_TRAVERSE(node) + buildRenderLists(child); + int padding = qMax((m_nextRenderOrder - currentOrder) >> 2, m_minimumOrderPadding); + info->firstOrder = currentOrder; + info->availableOrders = padding; + info->lastOrder = m_nextRenderOrder + padding; + m_nextRenderOrder = info->lastOrder; + } + return; + } else if (node->type() == QSGNode::RenderNodeType) { + RenderNodeElement *e = shadowNode->renderNodeElement(); + m_alphaRenderList << e; + e->order = ++m_nextRenderOrder; + Q_ASSERT(e); + } + + QSGNODE_TRAVERSE(node) + buildRenderLists(child); +} + +void Renderer::tagSubRoots(Node *node) +{ + BatchRootInfo *i = batchRootInfo(node); + m_taggedRoots << node; + for (QSet::const_iterator it = i->subRoots.constBegin(); + it != i->subRoots.constEnd(); ++it) { + tagSubRoots(*it); + } +} + +static void qsg_addOrphanedElements(QDataBuffer &orphans, const QDataBuffer &renderList) +{ + orphans.reset(); + for (int i=0; iremoved) { + e->orphaned = true; + orphans.add(e); + } + } +} + +static void qsg_addBackOrphanedElements(QDataBuffer &orphans, QDataBuffer &renderList) +{ + for (int i=0; iorphaned) + renderList.add(e); + } + orphans.reset(); +} + +/* + * To rebuild the tagged roots, we start by putting all subroots of tagged + * roots into the list of tagged roots. This is to make the rest of the + * algorithm simpler. + * + * Second, we invalidate all batches which belong to tagged roots, which now + * includes the entire subtree under a given root + * + * Then we call buildRenderLists for all tagged subroots which do not have + * parents which are tagged, aka, we traverse only the topmosts roots. + * + * Then we sort the render lists based on their render order, to restore the + * right order for rendering. + */ +void Renderer::buildRenderListsForTaggedRoots() +{ + // Flag any element that is currently in the render lists, but which + // is not in a batch. This happens when we have a partial rebuild + // in one sub tree while we have a BuildBatches change in another + // isolated subtree. So that batch-building takes into account + // these "orphaned" nodes, we flag them now. The ones under tagged + // roots will be cleared again. The remaining ones are added into the + // render lists so that they contain all visual nodes after the + // function completes. + qsg_addOrphanedElements(m_tmpOpaqueElements, m_opaqueRenderList); + qsg_addOrphanedElements(m_tmpAlphaElements, m_alphaRenderList); + + // Take a copy now, as we will be adding to this while traversing.. + QSet roots = m_taggedRoots; + for (QSet::const_iterator it = roots.constBegin(); + it != roots.constEnd(); ++it) { + tagSubRoots(*it); + } + + for (int i=0; iroot)) + invalidateAndRecycleBatch(b); + + } + for (int i=0; iroot)) + invalidateAndRecycleBatch(b); + } + + m_opaqueRenderList.reset(); + m_alphaRenderList.reset(); + int maxRenderOrder = m_nextRenderOrder; + m_partialRebuild = true; + // Traverse each root, assigning it + for (QSet::const_iterator it = m_taggedRoots.constBegin(); + it != m_taggedRoots.constEnd(); ++it) { + Node *root = *it; + BatchRootInfo *i = batchRootInfo(root); + if ((!i->parentRoot || !m_taggedRoots.contains(i->parentRoot)) + && !nodeUpdater()->isNodeBlocked(root->sgNode, rootNode())) { + m_nextRenderOrder = i->firstOrder; + m_partialRebuildRoot = root->sgNode; + buildRenderLists(root->sgNode); + } + } + m_partialRebuild = false; + m_partialRebuildRoot = nullptr; + m_taggedRoots.clear(); + m_nextRenderOrder = qMax(m_nextRenderOrder, maxRenderOrder); + + // Add orphaned elements back into the list and then sort it.. + qsg_addBackOrphanedElements(m_tmpOpaqueElements, m_opaqueRenderList); + qsg_addBackOrphanedElements(m_tmpAlphaElements, m_alphaRenderList); + + if (m_opaqueRenderList.size()) + std::sort(&m_opaqueRenderList.first(), &m_opaqueRenderList.last() + 1, qsg_sort_element_decreasing_order); + if (m_alphaRenderList.size()) + std::sort(&m_alphaRenderList.first(), &m_alphaRenderList.last() + 1, qsg_sort_element_increasing_order); + +} + +void Renderer::buildRenderListsFromScratch() +{ + m_opaqueRenderList.reset(); + m_alphaRenderList.reset(); + + for (int i=0; ifirst); + +#if defined(QSGBATCHRENDERER_INVALIDATE_WEDGED_NODES) + if (m_renderOrderRebuildLower < 0 || batch->first->order < m_renderOrderRebuildLower) + m_renderOrderRebuildLower = batch->first->order; + if (m_renderOrderRebuildUpper < 0 || batch->lastOrderInBatch > m_renderOrderRebuildUpper) + m_renderOrderRebuildUpper = batch->lastOrderInBatch; + + int first = m_renderOrderRebuildLower; + int last = m_renderOrderRebuildUpper; +#else + int first = batch->first->order; + int last = batch->lastOrderInBatch; +#endif + + batch->invalidate(); + + for (int i=0; ifirst) { + int bf = b->first->order; + int bl = b->lastOrderInBatch; + if (bl > first && bf < last) + b->invalidate(); + } + } + + m_rebuild |= BuildBatches; +} + +/* Clean up batches by making it a consecutive list of "valid" + * batches and moving all invalidated batches to the batches pool. + */ +void Renderer::cleanupBatches(QDataBuffer *batches) { + qsizetype n = batches->size(); + if (n == 0) + return; + + qsizetype writeIndex = 0; + for (qsizetype i = 0; i < n; ++i) { + Batch *b = batches->at(i); + if (b->first) + (*batches).data()[writeIndex++] = b; + else + invalidateAndRecycleBatch(b); + } + batches->resize(writeIndex); +} + +void Renderer::prepareOpaqueBatches() +{ + for (int i=m_opaqueRenderList.size() - 1; i >= 0; --i) { + Element *ei = m_opaqueRenderList.at(i); + if (!ei || ei->batch || ei->node->geometry()->vertexCount() == 0) + continue; + Batch *batch = newBatch(); + batch->first = ei; + batch->root = ei->root; + batch->isOpaque = true; + batch->needsUpload = true; + batch->positionAttribute = qsg_positionAttribute(ei->node->geometry()); + + m_opaqueBatches.add(batch); + + ei->batch = batch; + Element *next = ei; + + QSGGeometryNode *gni = ei->node; + + for (int j = i - 1; j >= 0; --j) { + Element *ej = m_opaqueRenderList.at(j); + if (!ej) + continue; + if (ej->root != ei->root) + break; + if (ej->batch || ej->node->geometry()->vertexCount() == 0) + continue; + + QSGGeometryNode *gnj = ej->node; + + const QSGGeometry *gniGeometry = gni->geometry(); + const QSGMaterial *gniMaterial = gni->activeMaterial(); + const QSGGeometry *gnjGeometry = gnj->geometry(); + const QSGMaterial *gnjMaterial = gnj->activeMaterial(); + if (gni->clipList() == gnj->clipList() + && gniGeometry->drawingMode() == gnjGeometry->drawingMode() + && (gniGeometry->lineWidth() == gnjGeometry->lineWidth() + || (gniGeometry->drawingMode() != QSGGeometry::DrawLines + && gniGeometry->drawingMode() != QSGGeometry::DrawLineStrip)) + && gniGeometry->attributes() == gnjGeometry->attributes() + && gniGeometry->indexType() == gnjGeometry->indexType() + && gni->inheritedOpacity() == gnj->inheritedOpacity() + && gniMaterial->type() == gnjMaterial->type() + && gniMaterial->viewCount() == gnjMaterial->viewCount() + && gniMaterial->compare(gnjMaterial) == 0 + && ei->mutabilityGroup == ej->mutabilityGroup) + { + ej->batch = batch; + next->nextInBatch = ej; + next = ej; + } + } + + batch->lastOrderInBatch = next->order; + } +} + +bool Renderer::checkOverlap(int first, int last, const Rect &bounds) +{ + for (int i=first; i<=last; ++i) { + Element *e = m_alphaRenderList.at(i); +#if defined(QSGBATCHRENDERER_INVALIDATE_WEDGED_NODES) + if (!e || e->batch) +#else + if (!e) +#endif + continue; + Q_ASSERT(e->boundsComputed); + if (e->bounds.intersects(bounds)) + return true; + } + return false; +} + +/* + * + * To avoid the O(n^2) checkOverlap check in most cases, we have the + * overlapBounds which is the union of all bounding rects to check overlap + * for. We know that if it does not overlap, then none of the individual + * ones will either. For the typical list case, this results in no calls + * to checkOverlap what-so-ever. This also ensures that when all consecutive + * items are matching (such as a table of text), we don't build up an + * overlap bounds and thus do not require full overlap checks. + */ + +void Renderer::prepareAlphaBatches() +{ + for (int i=0; iisRenderNode) + continue; + Q_ASSERT(!e->removed); + e->ensureBoundsValid(); + } + + for (int i=0; ibatch) + continue; + + if (ei->isRenderNode) { + Batch *rnb = newBatch(); + rnb->first = ei; + rnb->root = ei->root; + rnb->isOpaque = false; + rnb->isRenderNode = true; + ei->batch = rnb; + m_alphaBatches.add(rnb); + continue; + } + + if (ei->node->geometry()->vertexCount() == 0) + continue; + + Batch *batch = newBatch(); + batch->first = ei; + batch->root = ei->root; + batch->isOpaque = false; + batch->needsUpload = true; + m_alphaBatches.add(batch); + ei->batch = batch; + + QSGGeometryNode *gni = ei->node; + batch->positionAttribute = qsg_positionAttribute(gni->geometry()); + + Rect overlapBounds; + overlapBounds.set(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + + Element *next = ei; + + for (int j = i + 1; j < m_alphaRenderList.size(); ++j) { + Element *ej = m_alphaRenderList.at(j); + if (!ej) + continue; + if (ej->root != ei->root || ej->isRenderNode) + break; + if (ej->batch) { +#if !defined(QSGBATCHRENDERER_INVALIDATE_WEDGED_NODES) + overlapBounds |= ej->bounds; +#endif + continue; + } + + QSGGeometryNode *gnj = ej->node; + if (gnj->geometry()->vertexCount() == 0) + continue; + + const QSGGeometry *gniGeometry = gni->geometry(); + const QSGMaterial *gniMaterial = gni->activeMaterial(); + const QSGGeometry *gnjGeometry = gnj->geometry(); + const QSGMaterial *gnjMaterial = gnj->activeMaterial(); + if (gni->clipList() == gnj->clipList() + && gniGeometry->drawingMode() == gnjGeometry->drawingMode() + && (gniGeometry->drawingMode() != QSGGeometry::DrawLines + || (gniGeometry->lineWidth() == gnjGeometry->lineWidth() + // Must not do overlap checks when the line width is not 1, + // we have no knowledge how such lines are rasterized. + && gniGeometry->lineWidth() == 1.0f)) + && gniGeometry->attributes() == gnjGeometry->attributes() + && gniGeometry->indexType() == gnjGeometry->indexType() + && gni->inheritedOpacity() == gnj->inheritedOpacity() + && gniMaterial->type() == gnjMaterial->type() + && gniMaterial->viewCount() == gnjMaterial->viewCount() + && gniMaterial->compare(gnjMaterial) == 0 + && ei->mutabilityGroup == ej->mutabilityGroup) + { + if (!overlapBounds.intersects(ej->bounds) || !checkOverlap(i+1, j - 1, ej->bounds)) { + ej->batch = batch; + next->nextInBatch = ej; + next = ej; + } else { + /* When we come across a compatible element which hits an overlap, we + * need to stop the batch right away. We cannot add more elements + * to the current batch as they will be rendered before the batch that the + * current 'ej' will be added to. + */ + break; + } + } else { + overlapBounds |= ej->bounds; + } + } + + batch->lastOrderInBatch = next->order; + } + + +} + +static inline int qsg_fixIndexCount(int iCount, int drawMode) +{ + switch (drawMode) { + case QSGGeometry::DrawTriangleStrip: + // Merged triangle strips need to contain degenerate triangles at the beginning and end. + // One could save 2 uploaded ushorts here by ditching the padding for the front of the + // first and the end of the last, but for simplicity, we simply don't care. + // Those extra triangles will be skipped while drawing to preserve the strip's parity + // anyhow. + return iCount + 2; + case QSGGeometry::DrawLines: + // For lines we drop the last vertex if the number of vertices is uneven. + return iCount - (iCount % 2); + case QSGGeometry::DrawTriangles: + // For triangles we drop trailing vertices until the result is divisible by 3. + return iCount - (iCount % 3); + default: + return iCount; + } +} + +static inline float calculateElementZOrder(const Element *e, qreal zRange) +{ + // Clamp the zOrder to within the min and max depth of the viewport. + return std::clamp(1.0f - float(e->order * zRange), VIEWPORT_MIN_DEPTH, VIEWPORT_MAX_DEPTH); +} + +/* These parameters warrant some explanation... + * + * vaOffset: The byte offset into the vertex data to the location of the + * 2D float point vertex attributes. + * + * vertexData: destination where the geometry's vertex data should go + * + * zData: destination of geometries injected Z positioning + * + * indexData: destination of the indices for this element + * + * iBase: The starting index for this element in the batch + */ + +void Renderer::uploadMergedElement(Element *e, int vaOffset, char **vertexData, char **zData, char **indexData, void *iBasePtr, int *indexCount) +{ + if (Q_UNLIKELY(debug_upload())) qDebug() << " - uploading element:" << e << e->node << (void *) *vertexData << (qintptr) (*zData - *vertexData) << (qintptr) (*indexData - *vertexData); + QSGGeometry *g = e->node->geometry(); + + const QMatrix4x4 &localx = *e->node->matrix(); + const float *localxdata = localx.constData(); + + const int vCount = g->vertexCount(); + const int vSize = g->sizeOfVertex(); + memcpy(*vertexData, g->vertexData(), vSize * vCount); + + // apply vertex transform.. + char *vdata = *vertexData + vaOffset; + if (localx.flags() == QMatrix4x4::Translation) { + for (int i=0; ix += localxdata[12]; + p->y += localxdata[13]; + vdata += vSize; + } + } else if (localx.flags() > QMatrix4x4::Translation) { + for (int i=0; imap(localx); + vdata += vSize; + } + } + + if (useDepthBuffer()) { + float *vzorder = (float *) *zData; + float zorder = calculateElementZOrder(e, m_zRange); + for (int i=0; iindexCount(); + if (m_uint32IndexForRhi) { + // can only happen when using the rhi + quint32 *iBase = (quint32 *) iBasePtr; + quint32 *indices = (quint32 *) *indexData; + if (iCount == 0) { + iCount = vCount; + if (g->drawingMode() == QSGGeometry::DrawTriangleStrip) + *indices++ = *iBase; + else + iCount = qsg_fixIndexCount(iCount, g->drawingMode()); + + for (int i=0; iindexDataAsUShort(); + if (g->drawingMode() == QSGGeometry::DrawTriangleStrip) + *indices++ = *iBase + srcIndices[0]; + else + iCount = qsg_fixIndexCount(iCount, g->drawingMode()); + + for (int i=0; idrawingMode() == QSGGeometry::DrawTriangleStrip) { + indices[iCount] = indices[iCount - 1]; + iCount += 2; + } + *iBase += vCount; + } else { + // normally batching is only done for ushort index data + quint16 *iBase = (quint16 *) iBasePtr; + quint16 *indices = (quint16 *) *indexData; + if (iCount == 0) { + iCount = vCount; + if (g->drawingMode() == QSGGeometry::DrawTriangleStrip) + *indices++ = *iBase; + else + iCount = qsg_fixIndexCount(iCount, g->drawingMode()); + + for (int i=0; iindexDataAsUShort(); + if (g->drawingMode() == QSGGeometry::DrawTriangleStrip) + *indices++ = *iBase + srcIndices[0]; + else + iCount = qsg_fixIndexCount(iCount, g->drawingMode()); + + for (int i=0; idrawingMode() == QSGGeometry::DrawTriangleStrip) { + indices[iCount] = indices[iCount - 1]; + iCount += 2; + } + *iBase += vCount; + } + + *vertexData += vCount * vSize; + *indexData += iCount * mergedIndexElemSize(); + *indexCount += iCount; +} + +QMatrix4x4 qsg_matrixForRoot(Node *node) +{ + if (node->type() == QSGNode::TransformNodeType) + return static_cast(node->sgNode)->combinedMatrix(); + Q_ASSERT(node->type() == QSGNode::ClipNodeType); + QSGClipNode *c = static_cast(node->sgNode); + return *c->matrix(); +} + +void Renderer::uploadBatch(Batch *b) +{ + // Early out if nothing has changed in this batch.. + if (!b->needsUpload) { + if (Q_UNLIKELY(debug_upload())) qDebug() << " Batch:" << b << "already uploaded..."; + return; + } + + if (!b->first) { + if (Q_UNLIKELY(debug_upload())) qDebug() << " Batch:" << b << "is invalid..."; + return; + } + + if (b->isRenderNode) { + if (Q_UNLIKELY(debug_upload())) qDebug() << " Batch: " << b << "is a render node..."; + return; + } + + // Figure out if we can merge or not, if not, then just render the batch as is.. + Q_ASSERT(b->first); + Q_ASSERT(b->first->node); + + QSGGeometryNode *gn = b->first->node; + QSGGeometry *g = gn->geometry(); + QSGMaterial::Flags flags = gn->activeMaterial()->flags(); + bool canMerge = (g->drawingMode() == QSGGeometry::DrawTriangles || g->drawingMode() == QSGGeometry::DrawTriangleStrip || + g->drawingMode() == QSGGeometry::DrawLines || g->drawingMode() == QSGGeometry::DrawPoints) + && b->positionAttribute >= 0 + && g->indexType() == QSGGeometry::UnsignedShortType + && (flags & (QSGMaterial::NoBatching | QSGMaterial_FullMatrix)) == 0 + && ((flags & QSGMaterial::RequiresFullMatrixExceptTranslate) == 0 || b->isTranslateOnlyToRoot()) + && b->isSafeToBatch(); + + b->merged = canMerge; + + // Figure out how much memory we need... + b->vertexCount = 0; + b->indexCount = 0; + int unmergedIndexSize = 0; + Element *e = b->first; + + // Merged batches always do indexed draw calls. Non-indexed geometry gets + // indices generated automatically, when merged. + while (e) { + QSGGeometry *eg = e->node->geometry(); + b->vertexCount += eg->vertexCount(); + int iCount = eg->indexCount(); + if (b->merged) { + if (iCount == 0) + iCount = eg->vertexCount(); + iCount = qsg_fixIndexCount(iCount, g->drawingMode()); + } else { + const int effectiveIndexSize = m_uint32IndexForRhi ? sizeof(quint32) : eg->sizeOfIndex(); + unmergedIndexSize += iCount * effectiveIndexSize; + } + b->indexCount += iCount; + e = e->nextInBatch; + } + + // Abort if there are no vertices in this batch.. We abort this late as + // this is a broken usecase which we do not care to optimize for... + if (b->vertexCount == 0 || (b->merged && b->indexCount == 0)) + return; + + /* Allocate memory for this batch. Merged batches are divided into three separate blocks + 1. Vertex data for all elements, as they were in the QSGGeometry object, but + with the tranform relative to this batch's root applied. The vertex data + is otherwise unmodified. + 2. Z data for all elements, derived from each elements "render order". + This is present for merged data only. + 3. Indices for all elements, as they were in the QSGGeometry object, but + adjusted so that each index matches its. + And for TRIANGLE_STRIPs, we need to insert degenerate between each + primitive. These are unsigned shorts for merged and arbitrary for + non-merged. + */ + int bufferSize = b->vertexCount * g->sizeOfVertex(); + int ibufferSize = 0; + if (b->merged) { + ibufferSize = b->indexCount * mergedIndexElemSize(); + if (useDepthBuffer()) + bufferSize += b->vertexCount * sizeof(float); + } else { + ibufferSize = unmergedIndexSize; + } + + map(&b->ibo, ibufferSize, true); + map(&b->vbo, bufferSize); + + if (Q_UNLIKELY(debug_upload())) qDebug() << " - batch" << b << " first:" << b->first << " root:" + << b->root << " merged:" << b->merged << " positionAttribute" << b->positionAttribute + << " vbo:" << b->vbo.buf << ":" << b->vbo.size; + + if (b->merged) { + char *vertexData = b->vbo.data; + char *zData = vertexData + b->vertexCount * g->sizeOfVertex(); + char *indexData = b->ibo.data; + + quint16 iOffset16 = 0; + quint32 iOffset32 = 0; + e = b->first; + uint verticesInSet = 0; + // Start a new set already after 65534 vertices because 0xFFFF may be + // used for an always-on primitive restart with some apis (adapt for + // uint32 indices as appropriate). + const uint verticesInSetLimit = m_uint32IndexForRhi ? 0xfffffffe : 0xfffe; + int indicesInSet = 0; + b->drawSets.reset(); + int drawSetIndices = 0; + const char *indexBase = b->ibo.data; + b->drawSets << DrawSet(0, zData - vertexData, drawSetIndices); + while (e) { + verticesInSet += e->node->geometry()->vertexCount(); + if (verticesInSet > verticesInSetLimit) { + b->drawSets.last().indexCount = indicesInSet; + if (g->drawingMode() == QSGGeometry::DrawTriangleStrip) { + b->drawSets.last().indices += 1 * mergedIndexElemSize(); + b->drawSets.last().indexCount -= 2; + } + drawSetIndices = indexData - indexBase; + b->drawSets << DrawSet(vertexData - b->vbo.data, + zData - b->vbo.data, + drawSetIndices); + iOffset16 = 0; + iOffset32 = 0; + verticesInSet = e->node->geometry()->vertexCount(); + indicesInSet = 0; + } + void *iBasePtr = &iOffset16; + if (m_uint32IndexForRhi) + iBasePtr = &iOffset32; + uploadMergedElement(e, b->positionAttribute, &vertexData, &zData, &indexData, iBasePtr, &indicesInSet); + e = e->nextInBatch; + } + b->drawSets.last().indexCount = indicesInSet; + // We skip the very first and very last degenerate triangles since they aren't needed + // and the first one would reverse the vertex ordering of the merged strips. + if (g->drawingMode() == QSGGeometry::DrawTriangleStrip) { + b->drawSets.last().indices += 1 * mergedIndexElemSize(); + b->drawSets.last().indexCount -= 2; + } + } else { + char *vboData = b->vbo.data; + char *iboData = b->ibo.data; + Element *e = b->first; + while (e) { + QSGGeometry *g = e->node->geometry(); + int vbs = g->vertexCount() * g->sizeOfVertex(); + memcpy(vboData, g->vertexData(), vbs); + vboData = vboData + vbs; + const int indexCount = g->indexCount(); + if (indexCount) { + const int effectiveIndexSize = m_uint32IndexForRhi ? sizeof(quint32) : g->sizeOfIndex(); + const int ibs = indexCount * effectiveIndexSize; + if (g->sizeOfIndex() == effectiveIndexSize) { + memcpy(iboData, g->indexData(), ibs); + } else { + if (g->sizeOfIndex() == sizeof(quint16) && effectiveIndexSize == sizeof(quint32)) { + quint16 *src = g->indexDataAsUShort(); + quint32 *dst = (quint32 *) iboData; + for (int i = 0; i < indexCount; ++i) + dst[i] = src[i]; + } else { + Q_ASSERT_X(false, "uploadBatch (unmerged)", "uint index with ushort effective index - cannot happen"); + } + } + iboData += ibs; + } + e = e->nextInBatch; + } + } +#ifndef QT_NO_DEBUG_OUTPUT + if (Q_UNLIKELY(debug_upload())) { + const char *vd = b->vbo.data; + qDebug() << " -- Vertex Data, count:" << b->vertexCount << " - " << g->sizeOfVertex() << "bytes/vertex"; + for (int i=0; ivertexCount; ++i) { + QDebug dump = qDebug().nospace(); + dump << " --- " << i << ": "; + int offset = 0; + for (int a=0; aattributeCount(); ++a) { + const QSGGeometry::Attribute &attr = g->attributes()[a]; + dump << attr.position << ":(" << attr.tupleSize << ","; + if (attr.type == QSGGeometry::FloatType) { + dump << "float "; + if (attr.isVertexCoordinate) + dump << "* "; + for (int t=0; tmerged && useDepthBuffer()) { + float zorder = ((float*)(b->vbo.data + b->vertexCount * g->sizeOfVertex()))[i]; + dump << " Z:(" << zorder << ")"; + } + vd += g->sizeOfVertex(); + } + + if (!b->drawSets.isEmpty()) { + if (m_uint32IndexForRhi) { + const quint32 *id = (const quint32 *) b->ibo.data; + { + QDebug iDump = qDebug(); + iDump << " -- Index Data, count:" << b->indexCount; + for (int i=0; iindexCount; ++i) { + if ((i % 24) == 0) + iDump << Qt::endl << " --- "; + iDump << id[i]; + } + } + } else { + const quint16 *id = (const quint16 *) b->ibo.data; + { + QDebug iDump = qDebug(); + iDump << " -- Index Data, count:" << b->indexCount; + for (int i=0; iindexCount; ++i) { + if ((i % 24) == 0) + iDump << Qt::endl << " --- "; + iDump << id[i]; + } + } + } + + for (int i=0; idrawSets.size(); ++i) { + const DrawSet &s = b->drawSets.at(i); + qDebug() << " -- DrawSet: indexCount:" << s.indexCount << " vertices:" << s.vertices << " z:" << s.zorders << " indices:" << s.indices; + } + } + } +#endif // QT_NO_DEBUG_OUTPUT + + unmap(&b->vbo); + unmap(&b->ibo, true); + + if (Q_UNLIKELY(debug_upload() || debug_pools())) + qDebug() << " --- vertex/index buffers unmapped, batch upload completed... vbo pool size" << m_vboPoolCost << "ibo pool size" << m_iboPoolCost; + + b->needsUpload = false; + + if (Q_UNLIKELY(debug_render())) + b->uploadedThisFrame = true; +} + +void Renderer::applyClipStateToGraphicsState() +{ + m_gstate.usesScissor = (m_damageTree && m_damageTree->scissorEnabled) + || (m_currentClipState.type & ClipState::ScissorClip); + m_gstate.stencilTest = (m_currentClipState.type & ClipState::StencilClip); +} + +QRhiGraphicsPipeline *Renderer::buildStencilPipeline(const Batch *batch, bool firstStencilClipInBatch) +{ + QRhiGraphicsPipeline *ps = m_rhi->newGraphicsPipeline(); + ps->setFlags(QRhiGraphicsPipeline::UsesStencilRef); + QRhiGraphicsPipeline::TargetBlend blend; + blend.colorWrite = {}; + ps->setTargetBlends({ blend }); + ps->setSampleCount(renderTarget().rt->sampleCount()); + ps->setStencilTest(true); + QRhiGraphicsPipeline::StencilOpState stencilOp; + if (firstStencilClipInBatch) { + stencilOp.compareOp = QRhiGraphicsPipeline::Always; + stencilOp.failOp = QRhiGraphicsPipeline::Keep; + stencilOp.depthFailOp = QRhiGraphicsPipeline::Keep; + stencilOp.passOp = QRhiGraphicsPipeline::Replace; + } else { + stencilOp.compareOp = QRhiGraphicsPipeline::Equal; + stencilOp.failOp = QRhiGraphicsPipeline::Keep; + stencilOp.depthFailOp = QRhiGraphicsPipeline::Keep; + stencilOp.passOp = QRhiGraphicsPipeline::IncrementAndClamp; + } + ps->setStencilFront(stencilOp); + ps->setStencilBack(stencilOp); + + ps->setTopology(m_stencilClipCommon.topology); + + ps->setMultiViewCount(renderTarget().multiViewCount); + + ps->setShaderStages({ QRhiShaderStage(QRhiShaderStage::Vertex, m_stencilClipCommon.vs), + QRhiShaderStage(QRhiShaderStage::Fragment, m_stencilClipCommon.fs) }); + ps->setVertexInputLayout(m_stencilClipCommon.inputLayout); + ps->setShaderResourceBindings(batch->stencilClipState.srb); // use something, it just needs to be layout-compatible + ps->setRenderPassDescriptor(renderTarget().rpDesc); + + if (!ps->create()) { + qWarning("Failed to build stencil clip pipeline"); + delete ps; + return nullptr; + } + + return ps; +} + +void Renderer::updateClipState(const QSGClipNode *clipList, Batch *batch) +{ + // Note: No use of the clip-related speparate m_current* vars is allowed + // here. All stored in batch->clipState instead. To collect state during + // the prepare steps, m_currentClipState is used. It should not be used in + // the render steps afterwards. + + // The stenciling logic is slightly different from Qt 5's direct OpenGL version + // as we cannot just randomly clear the stencil buffer. We now put all clip + // shapes into the stencil buffer for all batches in the frame. This means + // that the number of total clips in a scene is reduced (since the stencil + // value cannot exceed 255) but we do not need any clears inbetween. + + Q_ASSERT(m_rhi); + batch->stencilClipState.updateStencilBuffer = false; + if (clipList == m_currentClipState.clipList || Q_UNLIKELY(debug_noclip())) { + applyClipStateToGraphicsState(); + batch->clipState = m_currentClipState; + return; + } + + ClipState::ClipType clipType = ClipState::NoClip; + QRect scissorRect; + QVarLengthArray stencilClipNodes; + const QSGClipNode *clip = clipList; + + batch->stencilClipState.drawCalls.reset(); + quint32 totalVSize = 0; + quint32 totalISize = 0; + quint32 totalUSize = 0; + const quint32 StencilClipUbufSize = 64; + + while (clip) { + QMatrix4x4 m = m_current_projection_matrix_native_ndc[0]; // never hit for 3D and so multiview + if (clip->matrix()) + m *= *clip->matrix(); + + bool isRectangleWithNoPerspective = clip->isRectangular() + && qFuzzyIsNull(m(3, 0)) && qFuzzyIsNull(m(3, 1)); + bool noRotate = qFuzzyIsNull(m(0, 1)) && qFuzzyIsNull(m(1, 0)); + bool isRotate90 = qFuzzyIsNull(m(0, 0)) && qFuzzyIsNull(m(1, 1)); + + if (isRectangleWithNoPerspective && (noRotate || isRotate90)) { + QRectF bbox = clip->clipRect(); + qreal invW = 1 / m(3, 3); + qreal fx1, fy1, fx2, fy2; + if (noRotate) { + fx1 = (bbox.left() * m(0, 0) + m(0, 3)) * invW; + fy1 = (bbox.bottom() * m(1, 1) + m(1, 3)) * invW; + fx2 = (bbox.right() * m(0, 0) + m(0, 3)) * invW; + fy2 = (bbox.top() * m(1, 1) + m(1, 3)) * invW; + } else { + Q_ASSERT(isRotate90); + fx1 = (bbox.bottom() * m(0, 1) + m(0, 3)) * invW; + fy1 = (bbox.left() * m(1, 0) + m(1, 3)) * invW; + fx2 = (bbox.top() * m(0, 1) + m(0, 3)) * invW; + fy2 = (bbox.right() * m(1, 0) + m(1, 3)) * invW; + } + + if (fx1 > fx2) + qSwap(fx1, fx2); + if (fy1 > fy2) + qSwap(fy1, fy2); + + QRect deviceRect = this->deviceRect(); + + qint32 ix1 = qRound((fx1 + 1) * deviceRect.width() * qreal(0.5)); + qint32 iy1 = qRound((fy1 + 1) * deviceRect.height() * qreal(0.5)); + qint32 ix2 = qRound((fx2 + 1) * deviceRect.width() * qreal(0.5)); + qint32 iy2 = qRound((fy2 + 1) * deviceRect.height() * qreal(0.5)); + + if (!(clipType & ClipState::ScissorClip)) { + clipType |= ClipState::ScissorClip; + scissorRect = QRect(ix1, iy1, ix2 - ix1, iy2 - iy1); + } else { + scissorRect &= QRect(ix1, iy1, ix2 - ix1, iy2 - iy1); + } + } else { + clipType |= ClipState::StencilClip; + + const QSGGeometry *g = clip->geometry(); + if (!g) { + clip = clip->clipList(); + continue; + } + Q_ASSERT(g->attributeCount() > 0); + + const int vertexByteSize = g->sizeOfVertex() * g->vertexCount(); + // the 4 byte alignment may not actually be needed here + totalVSize = aligned(totalVSize, 4u) + vertexByteSize; + if (g->indexCount()) { + const int indexByteSize = g->sizeOfIndex() * g->indexCount(); + // so no need to worry about NonFourAlignedEffectiveIndexBufferOffset + totalISize = aligned(totalISize, 4u) + indexByteSize; + } + // ubuf start offsets must be aligned (typically to 256 bytes) + totalUSize = aligned(totalUSize, m_ubufAlignment) + StencilClipUbufSize; + + stencilClipNodes.append(clip); + } + + clip = clip->clipList(); + } + + if (clipType & ClipState::StencilClip) { + bool rebuildVBuf = false; + if (!batch->stencilClipState.vbuf) { + batch->stencilClipState.vbuf = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, totalVSize); + rebuildVBuf = true; + } else if (batch->stencilClipState.vbuf->size() < totalVSize) { + batch->stencilClipState.vbuf->setSize(totalVSize); + rebuildVBuf = true; + } + if (rebuildVBuf) { + if (!batch->stencilClipState.vbuf->create()) { + qWarning("Failed to build stencil clip vertex buffer"); + delete batch->stencilClipState.vbuf; + batch->stencilClipState.vbuf = nullptr; + return; + } + } + + if (totalISize) { + bool rebuildIBuf = false; + if (!batch->stencilClipState.ibuf) { + batch->stencilClipState.ibuf = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::IndexBuffer, totalISize); + rebuildIBuf = true; + } else if (batch->stencilClipState.ibuf->size() < totalISize) { + batch->stencilClipState.ibuf->setSize(totalISize); + rebuildIBuf = true; + } + if (rebuildIBuf) { + if (!batch->stencilClipState.ibuf->create()) { + qWarning("Failed to build stencil clip index buffer"); + delete batch->stencilClipState.ibuf; + batch->stencilClipState.ibuf = nullptr; + return; + } + } + } + + bool rebuildUBuf = false; + if (!batch->stencilClipState.ubuf) { + batch->stencilClipState.ubuf = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalUSize); + rebuildUBuf = true; + } else if (batch->stencilClipState.ubuf->size() < totalUSize) { + batch->stencilClipState.ubuf->setSize(totalUSize); + rebuildUBuf = true; + } + if (rebuildUBuf) { + if (!batch->stencilClipState.ubuf->create()) { + qWarning("Failed to build stencil clip uniform buffer"); + delete batch->stencilClipState.ubuf; + batch->stencilClipState.ubuf = nullptr; + return; + } + } + + if (!batch->stencilClipState.srb) { + batch->stencilClipState.srb = m_rhi->newShaderResourceBindings(); + const QRhiShaderResourceBinding ubufBinding = QRhiShaderResourceBinding::uniformBufferWithDynamicOffset( + 0, QRhiShaderResourceBinding::VertexStage, batch->stencilClipState.ubuf, StencilClipUbufSize); + batch->stencilClipState.srb->setBindings({ ubufBinding }); + if (!batch->stencilClipState.srb->create()) { + qWarning("Failed to build stencil clip srb"); + delete batch->stencilClipState.srb; + batch->stencilClipState.srb = nullptr; + return; + } + } + + quint32 vOffset = 0; + quint32 iOffset = 0; + quint32 uOffset = 0; + for (const QSGClipNode *clip : stencilClipNodes) { + const QSGGeometry *g = clip->geometry(); + const QSGGeometry::Attribute *a = g->attributes(); + StencilClipState::StencilDrawCall drawCall; + const bool firstStencilClipInBatch = batch->stencilClipState.drawCalls.isEmpty(); + + if (firstStencilClipInBatch) { + m_stencilClipCommon.inputLayout.setBindings({ QRhiVertexInputBinding(g->sizeOfVertex()) }); + m_stencilClipCommon.inputLayout.setAttributes({ QRhiVertexInputAttribute(0, 0, qsg_vertexInputFormat(*a), 0) }); + m_stencilClipCommon.topology = qsg_topology(g->drawingMode(), m_rhi); + } +#ifndef QT_NO_DEBUG + else { + if (qsg_topology(g->drawingMode(), m_rhi) != m_stencilClipCommon.topology) + qWarning("updateClipState: Clip list entries have different primitive topologies, this is not currently supported."); + if (qsg_vertexInputFormat(*a) != m_stencilClipCommon.inputLayout.cbeginAttributes()->format()) + qWarning("updateClipState: Clip list entries have different vertex input layouts, this is must not happen."); + } +#endif + + drawCall.vbufOffset = aligned(vOffset, 4u); + const int vertexByteSize = g->sizeOfVertex() * g->vertexCount(); + vOffset = drawCall.vbufOffset + vertexByteSize; + + int indexByteSize = 0; + if (g->indexCount()) { + drawCall.ibufOffset = aligned(iOffset, 4u); + indexByteSize = g->sizeOfIndex() * g->indexCount(); + iOffset = drawCall.ibufOffset + indexByteSize; + } + + drawCall.ubufOffset = aligned(uOffset, m_ubufAlignment); + uOffset = drawCall.ubufOffset + StencilClipUbufSize; + + QMatrix4x4 matrixYUpNDC = m_current_projection_matrix[0]; + if (clip->matrix()) + matrixYUpNDC *= *clip->matrix(); + + m_resourceUpdates->updateDynamicBuffer(batch->stencilClipState.ubuf, drawCall.ubufOffset, 64, matrixYUpNDC.constData()); + m_resourceUpdates->updateDynamicBuffer(batch->stencilClipState.vbuf, drawCall.vbufOffset, vertexByteSize, g->vertexData()); + if (indexByteSize) + m_resourceUpdates->updateDynamicBuffer(batch->stencilClipState.ibuf, drawCall.ibufOffset, indexByteSize, g->indexData()); + + // stencil ref goes 1, 1, 2, 3, 4, ..., N for the clips in the first batch, + // then N+1, N+1, N+2, N+3, ... for the next batch, + // and so on. + // Note the different stencilOp for the first and the subsequent clips. + drawCall.stencilRef = firstStencilClipInBatch ? m_currentClipState.stencilRef + 1 : m_currentClipState.stencilRef; + m_currentClipState.stencilRef += 1; + + drawCall.vertexCount = g->vertexCount(); + drawCall.indexCount = g->indexCount(); + drawCall.indexFormat = qsg_indexFormat(g); + batch->stencilClipState.drawCalls.add(drawCall); + } + + if (!m_stencilClipCommon.vs.isValid()) + m_stencilClipCommon.vs = QSGMaterialShaderPrivate::loadShader(QLatin1String(":/qt-project.org/scenegraph/shaders_ng/stencilclip.vert.qsb")); + + if (!m_stencilClipCommon.fs.isValid()) + m_stencilClipCommon.fs = QSGMaterialShaderPrivate::loadShader(QLatin1String(":/qt-project.org/scenegraph/shaders_ng/stencilclip.frag.qsb")); + + if (!m_stencilClipCommon.replacePs) + m_stencilClipCommon.replacePs = buildStencilPipeline(batch, true); + + if (!m_stencilClipCommon.incrPs) + m_stencilClipCommon.incrPs = buildStencilPipeline(batch, false); + + batch->stencilClipState.updateStencilBuffer = true; + } + + m_currentClipState.clipList = clipList; + m_currentClipState.type = clipType; + m_currentClipState.scissor = QRhiScissor(scissorRect.x(), scissorRect.y(), + scissorRect.width(), scissorRect.height()); + + applyClipStateToGraphicsState(); + batch->clipState = m_currentClipState; +} + +void Renderer::enqueueStencilDraw(const Batch *batch) +{ + // cliptype stencil + updateStencilBuffer==false means the batch uses + // stenciling but relies on the stencil data generated by a previous batch + // (due to the having the same clip node). Do not enqueue draw calls for + // stencil in this case as the stencil buffer is already up-to-date. + if (!batch->stencilClipState.updateStencilBuffer) + return; + + QRhiCommandBuffer *cb = renderTarget().cb; + const int count = batch->stencilClipState.drawCalls.size(); + for (int i = 0; i < count; ++i) { + const StencilClipState::StencilDrawCall &drawCall(batch->stencilClipState.drawCalls.at(i)); + QRhiShaderResourceBindings *srb = batch->stencilClipState.srb; + QRhiCommandBuffer::DynamicOffset ubufOffset(0, drawCall.ubufOffset); + if (i == 0) { + cb->setGraphicsPipeline(m_stencilClipCommon.replacePs); + cb->setViewport(m_pstate.viewport); + } else if (i == 1) { + cb->setGraphicsPipeline(m_stencilClipCommon.incrPs); + cb->setViewport(m_pstate.viewport); + } + // else incrPs is already bound + cb->setShaderResources(srb, 1, &ubufOffset); + cb->setStencilRef(drawCall.stencilRef); + const QRhiCommandBuffer::VertexInput vbufBinding(batch->stencilClipState.vbuf, drawCall.vbufOffset); + if (drawCall.indexCount) { + cb->setVertexInput(0, 1, &vbufBinding, + batch->stencilClipState.ibuf, drawCall.ibufOffset, drawCall.indexFormat); + cb->drawIndexed(drawCall.indexCount); + } else { + cb->setVertexInput(0, 1, &vbufBinding); + cb->draw(drawCall.vertexCount); + } + } +} + +void Renderer::setActiveRhiShader(QSGMaterialShader *program, ShaderManager::Shader *shader) +{ + Q_ASSERT(m_rhi); + m_currentProgram = program; + m_currentShader = shader; + m_currentMaterial = nullptr; +} + +static inline bool needsBlendConstant(QRhiGraphicsPipeline::BlendFactor f) +{ + return f == QRhiGraphicsPipeline::ConstantColor + || f == QRhiGraphicsPipeline::OneMinusConstantColor + || f == QRhiGraphicsPipeline::ConstantAlpha + || f == QRhiGraphicsPipeline::OneMinusConstantAlpha; +} + +// With QRhi renderBatches() is split to two steps: prepare and render. +// +// Prepare goes through the batches and elements, and set up a graphics +// pipeline, srb, uniform buffer, calculates clipping, based on m_gstate, the +// material (shaders), and the batches. This step does not touch the command +// buffer or renderpass-related state (m_pstate). +// +// The render step then starts a renderpass, and goes through all +// batches/elements again and records setGraphicsPipeline, drawIndexed, etc. on +// the command buffer. The prepare step's accumulated global state like +// m_gstate must not be used here. Rather, all data needed for rendering is +// available from Batch/Element at this stage. Bookkeeping of state in the +// renderpass is done via m_pstate. + +bool Renderer::ensurePipelineState(Element *e, const ShaderManager::Shader *sms, bool depthPostPass) +{ + // Note the key's == and qHash implementations: the renderpass descriptor + // and srb are tested for compatibility, not pointer equality. + // + // We do not store the srb pointer itself because the ownership stays with + // the Element and that can go away more often that we would like it + // to. (think scrolling a list view, constantly dropping and creating new + // nodes) Rather, use an opaque blob of a few uints and store and compare + // that. This works because once the pipeline is built, we will always call + // setShaderResources with an explicitly specified srb which is fine even if + // e->srb we used here to bake the pipeline is already gone by that point. + // + // A typical QSGMaterial's serialized srb layout is 8 uints. (uniform buffer + // + texture, 4 fields each) Regardless, using an implicitly shared + // container is essential here. (won't detach so no more allocs and copies + // are done, unless the Element decides to rebake the srb with a different + // layout - but then the detach is exactly what we need) + // + // Same story for the renderpass descriptor: the object can go away but + // that's fine because that has no effect on an already built pipeline, and + // for comparison we only rely on the serialized blob in order decide if the + // render target is compatible with the pipeline. + + const GraphicsPipelineStateKey k = GraphicsPipelineStateKey::create(m_gstate, sms, renderTarget().rpDesc, e->srb); + + // Note: dynamic state (viewport rect, scissor rect, stencil ref, blend + // constant) is never a part of GraphicsState/QRhiGraphicsPipeline. + + // See if there is an existing, matching pipeline state object. + auto it = m_shaderManager->pipelineCache.constFind(k); + if (it != m_shaderManager->pipelineCache.constEnd()) { + if (depthPostPass) + e->depthPostPassPs = *it; + else + e->ps = *it; + return true; + } + + // Build a new one. This is potentially expensive. + QRhiGraphicsPipeline *ps = m_rhi->newGraphicsPipeline(); + ps->setShaderStages(sms->stages.cbegin(), sms->stages.cend()); + ps->setVertexInputLayout(sms->inputLayout); + ps->setShaderResourceBindings(e->srb); + ps->setRenderPassDescriptor(renderTarget().rpDesc); + + QRhiGraphicsPipeline::Flags flags; + if (needsBlendConstant(m_gstate.srcColor) || needsBlendConstant(m_gstate.dstColor) + || needsBlendConstant(m_gstate.srcAlpha) || needsBlendConstant(m_gstate.dstAlpha)) + { + flags |= QRhiGraphicsPipeline::UsesBlendConstants; + } + if (m_gstate.usesScissor) + flags |= QRhiGraphicsPipeline::UsesScissor; + if (m_gstate.stencilTest) + flags |= QRhiGraphicsPipeline::UsesStencilRef; + + ps->setFlags(flags); + ps->setTopology(qsg_topology(m_gstate.drawMode, m_rhi)); + ps->setCullMode(m_gstate.cullMode); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 2) + ps->setFrontFace(invertFrontFace() ? QRhiGraphicsPipeline::CW : QRhiGraphicsPipeline::CCW); +#endif + ps->setPolygonMode(m_gstate.polygonMode); + ps->setMultiViewCount(m_gstate.multiViewCount); + + QRhiGraphicsPipeline::TargetBlend blend; + blend.colorWrite = m_gstate.colorWrite; + blend.enable = m_gstate.blending; + blend.srcColor = m_gstate.srcColor; + blend.dstColor = m_gstate.dstColor; + blend.srcAlpha = m_gstate.srcAlpha; + blend.dstAlpha = m_gstate.dstAlpha; + blend.opColor = m_gstate.opColor; + blend.opAlpha = m_gstate.opAlpha; + ps->setTargetBlends({ blend }); + + ps->setDepthTest(m_gstate.depthTest); + ps->setDepthWrite(m_gstate.depthWrite); + ps->setDepthOp(m_gstate.depthFunc); + + if (m_gstate.stencilTest) { + ps->setStencilTest(true); + QRhiGraphicsPipeline::StencilOpState stencilOp; + stencilOp.compareOp = QRhiGraphicsPipeline::Equal; + stencilOp.failOp = QRhiGraphicsPipeline::Keep; + stencilOp.depthFailOp = QRhiGraphicsPipeline::Keep; + stencilOp.passOp = QRhiGraphicsPipeline::Keep; + ps->setStencilFront(stencilOp); + ps->setStencilBack(stencilOp); + } + + ps->setSampleCount(m_gstate.sampleCount); + + ps->setLineWidth(m_gstate.lineWidth); + + if (!ps->create()) { + qWarning("Failed to build graphics pipeline state"); + delete ps; + return false; + } + + m_shaderManager->pipelineCache.insert(k, ps); + if (depthPostPass) + e->depthPostPassPs = ps; + else + e->ps = ps; + return true; +} + +static QRhiSampler *newSampler(QRhi *rhi, const QSGSamplerDescription &desc) +{ + QRhiSampler::Filter magFilter; + QRhiSampler::Filter minFilter; + QRhiSampler::Filter mipmapMode; + QRhiSampler::AddressMode u; + QRhiSampler::AddressMode v; + + switch (desc.filtering) { + case QSGTexture::None: + Q_FALLTHROUGH(); + case QSGTexture::Nearest: + magFilter = minFilter = QRhiSampler::Nearest; + break; + case QSGTexture::Linear: + magFilter = minFilter = QRhiSampler::Linear; + break; + default: + Q_UNREACHABLE(); + magFilter = minFilter = QRhiSampler::Nearest; + break; + } + + switch (desc.mipmapFiltering) { + case QSGTexture::None: + mipmapMode = QRhiSampler::None; + break; + case QSGTexture::Nearest: + mipmapMode = QRhiSampler::Nearest; + break; + case QSGTexture::Linear: + mipmapMode = QRhiSampler::Linear; + break; + default: + Q_UNREACHABLE(); + mipmapMode = QRhiSampler::None; + break; + } + + switch (desc.horizontalWrap) { + case QSGTexture::Repeat: + u = QRhiSampler::Repeat; + break; + case QSGTexture::ClampToEdge: + u = QRhiSampler::ClampToEdge; + break; + case QSGTexture::MirroredRepeat: + u = QRhiSampler::Mirror; + break; + default: + Q_UNREACHABLE(); + u = QRhiSampler::ClampToEdge; + break; + } + + switch (desc.verticalWrap) { + case QSGTexture::Repeat: + v = QRhiSampler::Repeat; + break; + case QSGTexture::ClampToEdge: + v = QRhiSampler::ClampToEdge; + break; + case QSGTexture::MirroredRepeat: + v = QRhiSampler::Mirror; + break; + default: + Q_UNREACHABLE(); + v = QRhiSampler::ClampToEdge; + break; + } + + return rhi->newSampler(magFilter, minFilter, mipmapMode, u, v); +} + +QRhiTexture *Renderer::dummyTexture() +{ + if (!m_dummyTexture) { + m_dummyTexture = m_rhi->newTexture(QRhiTexture::RGBA8, QSize(64, 64)); + if (m_dummyTexture->create()) { + if (m_resourceUpdates) { + QImage img(m_dummyTexture->pixelSize(), QImage::Format_RGBA8888_Premultiplied); + img.fill(0); + m_resourceUpdates->uploadTexture(m_dummyTexture, img); + } + } + } + return m_dummyTexture; +} + +static void rendererToMaterialGraphicsState(QSGMaterialShader::GraphicsPipelineState *dst, + GraphicsState *src) +{ + dst->blendEnable = src->blending; + + // the enum values should match, sanity check it + Q_ASSERT(int(QSGMaterialShader::GraphicsPipelineState::OneMinusSrc1Alpha) == int(QRhiGraphicsPipeline::OneMinusSrc1Alpha)); + Q_ASSERT(int(QSGMaterialShader::GraphicsPipelineState::BlendOp::Max) == int(QRhiGraphicsPipeline::Max)); + Q_ASSERT(int(QSGMaterialShader::GraphicsPipelineState::A) == int(QRhiGraphicsPipeline::A)); + Q_ASSERT(int(QSGMaterialShader::GraphicsPipelineState::CullBack) == int(QRhiGraphicsPipeline::Back)); + Q_ASSERT(int(QSGMaterialShader::GraphicsPipelineState::Line) == int(QRhiGraphicsPipeline::Line)); + dst->srcColor = QSGMaterialShader::GraphicsPipelineState::BlendFactor(src->srcColor); + dst->dstColor = QSGMaterialShader::GraphicsPipelineState::BlendFactor(src->dstColor); + + // For compatibility with any existing code, separateBlendFactors defaults + // to _false_ which means that materials that do not touch srcAlpha and + // dstAlpha will continue to use srcColor and dstColor as the alpha + // blending factors. New code that needs different values for color/alpha, + // can explicitly set separateBlendFactors to true and then set srcAlpha + // and dstAlpha as well. + dst->separateBlendFactors = false; + + dst->srcAlpha = QSGMaterialShader::GraphicsPipelineState::BlendFactor(src->srcAlpha); + dst->dstAlpha = QSGMaterialShader::GraphicsPipelineState::BlendFactor(src->dstAlpha); + + dst->opColor = QSGMaterialShader::GraphicsPipelineState::BlendOp(src->opColor); + dst->opAlpha = QSGMaterialShader::GraphicsPipelineState::BlendOp(src->opAlpha); + + dst->colorWrite = QSGMaterialShader::GraphicsPipelineState::ColorMask(int(src->colorWrite)); + + dst->cullMode = QSGMaterialShader::GraphicsPipelineState::CullMode(src->cullMode); + dst->polygonMode = QSGMaterialShader::GraphicsPipelineState::PolygonMode(src->polygonMode); +} + +static void materialToRendererGraphicsState(GraphicsState *dst, + QSGMaterialShader::GraphicsPipelineState *src) +{ + dst->blending = src->blendEnable; + dst->srcColor = QRhiGraphicsPipeline::BlendFactor(src->srcColor); + dst->dstColor = QRhiGraphicsPipeline::BlendFactor(src->dstColor); + if (src->separateBlendFactors) { + dst->srcAlpha = QRhiGraphicsPipeline::BlendFactor(src->srcAlpha); + dst->dstAlpha = QRhiGraphicsPipeline::BlendFactor(src->dstAlpha); + } else { + dst->srcAlpha = dst->srcColor; + dst->dstAlpha = dst->dstColor; + } + dst->opColor = QRhiGraphicsPipeline::BlendOp(src->opColor); + dst->opAlpha = QRhiGraphicsPipeline::BlendOp(src->opAlpha); + dst->colorWrite = QRhiGraphicsPipeline::ColorMask(int(src->colorWrite)); + dst->cullMode = QRhiGraphicsPipeline::CullMode(src->cullMode); + dst->polygonMode = QRhiGraphicsPipeline::PolygonMode(src->polygonMode); +} + +void Renderer::updateMaterialDynamicData(ShaderManager::Shader *sms, + QSGMaterialShader::RenderState &renderState, + QSGMaterial *material, + const Batch *batch, + Element *e, + int ubufOffset, + int ubufRegionSize, + char *directUpdatePtr) +{ + m_current_resource_update_batch = m_resourceUpdates; + + QSGMaterialShader *shader = sms->materialShader; + QSGMaterialShaderPrivate *pd = QSGMaterialShaderPrivate::get(shader); + QVarLengthArray bindings; + + if (pd->ubufBinding >= 0) { + m_current_uniform_data = &pd->masterUniformData; + const bool changed = shader->updateUniformData(renderState, material, m_currentMaterial); + m_current_uniform_data = nullptr; + + if (changed || !batch->ubufDataValid) { + if (directUpdatePtr) + memcpy(directUpdatePtr + ubufOffset, pd->masterUniformData.constData(), ubufRegionSize); + else + m_resourceUpdates->updateDynamicBuffer(batch->ubuf, ubufOffset, ubufRegionSize, pd->masterUniformData.constData()); + } + + bindings.append(QRhiShaderResourceBinding::uniformBuffer(pd->ubufBinding, + pd->ubufStages, + batch->ubuf, + ubufOffset, + ubufRegionSize)); + } + + for (int binding = 0; binding < QSGMaterialShaderPrivate::MAX_SHADER_RESOURCE_BINDINGS; ++binding) { + const QRhiShaderResourceBinding::StageFlags stages = pd->combinedImageSamplerBindings[binding]; + if (!stages) + continue; + + const QVarLengthArray &prevTex(pd->textureBindingTable[binding]); + QVarLengthArray nextTex = prevTex; + + const int count = pd->combinedImageSamplerCount[binding]; + nextTex.resize(count); + + shader->updateSampledImage(renderState, binding, nextTex.data(), material, + m_currentMaterial); + + if (nextTex.contains(nullptr)) { + qWarning("No QSGTexture provided from updateSampledImage(). This is wrong."); + continue; + } + + bool hasDirtySamplerOptions = false; + bool isAnisotropic = false; + for (QSGTexture *t : nextTex) { + QSGTexturePrivate *td = QSGTexturePrivate::get(t); + hasDirtySamplerOptions |= td->hasDirtySamplerOptions(); + isAnisotropic |= t->anisotropyLevel() != QSGTexture::AnisotropyNone; + td->resetDirtySamplerOptions(); + } + + // prevTex may be invalid at this point, avoid dereferencing it + if (nextTex != prevTex || hasDirtySamplerOptions) { + + // The QSGTexture, and so the sampler parameters, may have changed. + // The rhiTexture is not relevant here. + pd->textureBindingTable[binding] = nextTex; // does not own + pd->samplerBindingTable[binding].clear(); + + if (isAnisotropic) // ### + qWarning("QSGTexture anisotropy levels are not currently supported"); + + QVarLengthArray samplers; + + for (QSGTexture *t : nextTex) { + const QSGSamplerDescription samplerDesc = QSGSamplerDescription::fromTexture(t); + + QRhiSampler *sampler = m_samplers[samplerDesc]; + + if (!sampler) { + sampler = newSampler(m_rhi, samplerDesc); + if (!sampler->create()) { + qWarning("Failed to build sampler"); + delete sampler; + continue; + } + m_samplers[samplerDesc] = sampler; + } + samplers.append(sampler); + } + + pd->samplerBindingTable[binding] = samplers; // does not own + } + + if (pd->textureBindingTable[binding].size() == pd->samplerBindingTable[binding].size()) { + + QVarLengthArray textureSamplers; + + for (int i = 0; i < pd->textureBindingTable[binding].size(); ++i) { + + QRhiTexture *texture = pd->textureBindingTable[binding].at(i)->rhiTexture(); + + // texture may be null if the update above failed for any reason, + // or if the QSGTexture chose to return null intentionally. This is + // valid and we still need to provide something to the shader. + if (!texture) + texture = dummyTexture(); + + QRhiSampler *sampler = pd->samplerBindingTable[binding].at(i); + + textureSamplers.append( + QRhiShaderResourceBinding::TextureAndSampler { texture, sampler }); + } + + if (!textureSamplers.isEmpty()) + bindings.append(QRhiShaderResourceBinding::sampledTextures( + binding, stages, count, textureSamplers.constData())); + } + } + +#ifndef QT_NO_DEBUG + if (bindings.isEmpty()) + qWarning("No shader resources for material %p, this is odd.", material); +#endif + + enum class SrbAction { + Unknown, + DoNothing, + UpdateResources, + Rebake + } srbAction = SrbAction::Unknown; + + // First, if the Element has no srb created at all, then try to find an existing, + // currently unused srb that is layout-compatible with our binding list. + if (!e->srb) { + // reuse a QVector as our work area, thus possibly reusing the underlying allocation too + QList &layoutDesc(m_shaderManager->srbLayoutDescSerializeWorkspace); + layoutDesc.clear(); + QRhiShaderResourceBinding::serializeLayoutDescription(bindings.cbegin(), bindings.cend(), std::back_inserter(layoutDesc)); + e->srb = m_shaderManager->srbPool.take(layoutDesc); + if (e->srb) { + // Here we know layout compatibility is satisfied, but do not spend time on full + // comparison. The chance of getting an srb that refers to the same resources + // (buffer, textures) is low in practice. So reuse, but write new resources. + srbAction = SrbAction::UpdateResources; + } + } + + // If the Element had an existing srb, investigate: + // - It may be used as-is (when nothing changed in the scene regarding this node compared to the previous frame). + // - Otherwise it may be able to go with a lightweight update (replace resources, binding list layout is the same). + // - If all else fails rebake the full thing, meaning we reuse the memory allocation but will recreate everything underneath. + if (srbAction == SrbAction::Unknown && e->srb) { + if (std::equal(e->srb->cbeginBindings(), e->srb->cendBindings(), bindings.cbegin(), bindings.cend())) { + srbAction = SrbAction::DoNothing; + } else if (std::equal(e->srb->cbeginBindings(), e->srb->cendBindings(), bindings.cbegin(), bindings.cend(), + [](const auto &a, const auto &b) { return a.isLayoutCompatible(b); })) + { + srbAction = SrbAction::UpdateResources; + } else { + srbAction = SrbAction::Rebake; + } + } + + // If the Element had no srb associated at all and could not find a layout-compatible + // one from the pool, then create a whole new object. + if (!e->srb) { + e->srb = m_rhi->newShaderResourceBindings(); + srbAction = SrbAction::Rebake; + } + + Q_ASSERT(srbAction != SrbAction::Unknown && e->srb); + + switch (srbAction) { + case SrbAction::DoNothing: + break; + case SrbAction::UpdateResources: + { + e->srb->setBindings(bindings.cbegin(), bindings.cend()); + QRhiShaderResourceBindings::UpdateFlags flags; + // Due to the way the binding list is built up above, if we have a uniform buffer + // at binding point 0 (or none at all) then the sampledTexture bindings are added + // with increasing binding points afterwards, so the list is already sorted based + // on the binding points, thus we can save some time by telling the QRhi backend + // not to sort again. + if (pd->ubufBinding <= 0 || bindings.size() <= 1) + flags |= QRhiShaderResourceBindings::BindingsAreSorted; + + e->srb->updateResources(flags); + } + break; + case SrbAction::Rebake: + e->srb->setBindings(bindings.cbegin(), bindings.cend()); + if (!e->srb->create()) + qWarning("Failed to build srb"); + break; + default: + Q_ASSERT_X(false, "updateMaterialDynamicData", "No srb action set, this cannot happen"); + } +} + +void Renderer::updateMaterialStaticData(ShaderManager::Shader *sms, + QSGMaterialShader::RenderState &renderState, + QSGMaterial *material, + Batch *batch, + bool *gstateChanged) +{ + QSGMaterialShader *shader = sms->materialShader; + *gstateChanged = false; + if (shader->flags().testFlag(QSGMaterialShader::UpdatesGraphicsPipelineState)) { + // generate the public mini-state from m_gstate, invoke the material, + // write the changes, if any, back to m_gstate, together with a way to + // roll those back. + QSGMaterialShader::GraphicsPipelineState shaderPs; + rendererToMaterialGraphicsState(&shaderPs, &m_gstate); + const bool changed = shader->updateGraphicsPipelineState(renderState, &shaderPs, material, m_currentMaterial); + if (changed) { + m_gstateStack.push(m_gstate); + materialToRendererGraphicsState(&m_gstate, &shaderPs); + if (needsBlendConstant(m_gstate.srcColor) || needsBlendConstant(m_gstate.dstColor) + || needsBlendConstant(m_gstate.srcAlpha) || needsBlendConstant(m_gstate.dstAlpha)) + { + batch->blendConstant = shaderPs.blendConstant; + } + *gstateChanged = true; + } + } +} + +bool Renderer::prepareRenderMergedBatch(Batch *batch, PreparedRenderBatch *renderBatch) +{ + if (batch->vertexCount == 0 || batch->indexCount == 0) + return false; + + if (isCompositorOutputPass() && clipsDrawToDamage()) { + bool anyVisible = false; + for (Element *el = batch->first; el; el = el->nextInBatch) { + if (el->node && !m_damageTree->isCulled(el)) { + anyVisible = true; + break; + } + } + if (!anyVisible) + return false; + } + + Element *e = batch->first; + Q_ASSERT(e); + +#ifndef QT_NO_DEBUG_OUTPUT + if (Q_UNLIKELY(debug_render())) { + QDebug debug = qDebug(); + debug << " -" + << batch + << (batch->uploadedThisFrame ? "[ upload]" : "[retained]") + << (e->node->clipList() ? "[ clip]" : "[noclip]") + << (batch->isOpaque ? "[opaque]" : "[ alpha]") + << "[ merged]" + << " Nodes:" << QString::fromLatin1("%1").arg(qsg_countNodesInBatch(batch), 4).toLatin1().constData() + << " Vertices:" << QString::fromLatin1("%1").arg(batch->vertexCount, 5).toLatin1().constData() + << " Indices:" << QString::fromLatin1("%1").arg(batch->indexCount, 5).toLatin1().constData() + << " root:" << batch->root; + if (batch->drawSets.size() > 1) + debug << "sets:" << batch->drawSets.size(); + if (!batch->isOpaque) + debug << "opacity:" << e->node->inheritedOpacity(); + batch->uploadedThisFrame = false; + } +#endif + + QSGGeometryNode *gn = e->node; + + // We always have dirty matrix as all batches are at a unique z range. + QSGMaterialShader::RenderState::DirtyStates dirty = QSGMaterialShader::RenderState::DirtyMatrix; + if (batch->root) + m_current_model_view_matrix = qsg_matrixForRoot(batch->root); + else + m_current_model_view_matrix.setToIdentity(); + m_current_determinant = m_current_model_view_matrix.determinant(); + + const int viewCount = projectionMatrixCount(); + m_current_projection_matrix.resize(viewCount); + for (int viewIndex = 0; viewIndex < viewCount; ++viewIndex) + m_current_projection_matrix[viewIndex] = projectionMatrix(viewIndex); + + m_current_projection_matrix_native_ndc.resize(projectionMatrixWithNativeNDCCount()); + for (int viewIndex = 0; viewIndex < projectionMatrixWithNativeNDCCount(); ++viewIndex) + m_current_projection_matrix_native_ndc[viewIndex] = projectionMatrixWithNativeNDC(viewIndex); + + QSGMaterial *material = gn->activeMaterial(); + if (m_renderMode != QSGRendererInterface::RenderMode3D) + updateClipState(gn->clipList(), batch); + + const QSGGeometry *g = gn->geometry(); + const int multiViewCount = renderTarget().multiViewCount; + ShaderManager::Shader *sms = useDepthBuffer() ? m_shaderManager->prepareMaterial(material, g, m_renderMode, multiViewCount) + : m_shaderManager->prepareMaterialNoRewrite(material, g, m_renderMode, multiViewCount); + if (!sms) + return false; + + Q_ASSERT(sms->materialShader); + if (m_currentShader != sms) + setActiveRhiShader(sms->materialShader, sms); + + m_current_opacity = gn->inheritedOpacity(); + if (!qFuzzyCompare(sms->lastOpacity, float(m_current_opacity))) { + dirty |= QSGMaterialShader::RenderState::DirtyOpacity; + sms->lastOpacity = m_current_opacity; + } + + QSGMaterialShaderPrivate *pd = QSGMaterialShaderPrivate::get(sms->materialShader); + const quint32 ubufSize = quint32(pd->masterUniformData.size()); + if (pd->ubufBinding >= 0) { + bool ubufRebuild = false; + if (!batch->ubuf) { + batch->ubuf = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufSize); + ubufRebuild = true; + } else { + if (batch->ubuf->size() < ubufSize) { + batch->ubuf->setSize(ubufSize); + ubufRebuild = true; + } + } + if (ubufRebuild) { + batch->ubufDataValid = false; + if (!batch->ubuf->create()) { + qWarning("Failed to build uniform buffer of size %u bytes", ubufSize); + delete batch->ubuf; + batch->ubuf = nullptr; + return false; + } + } + } + + QSGMaterialShader::RenderState renderState = state(QSGMaterialShader::RenderState::DirtyStates(int(dirty))); + + bool pendingGStatePop = false; + updateMaterialStaticData(sms, renderState, material, batch, &pendingGStatePop); + + char *directUpdatePtr = nullptr; + if (batch->ubuf->nativeBuffer().slotCount == 0) + directUpdatePtr = batch->ubuf->beginFullDynamicBufferUpdateForCurrentFrame(); + + updateMaterialDynamicData(sms, renderState, material, batch, e, 0, ubufSize, directUpdatePtr); + + if (directUpdatePtr) + batch->ubuf->endFullDynamicBufferUpdateForCurrentFrame(); + + m_gstate.drawMode = QSGGeometry::DrawingMode(g->drawingMode()); + m_gstate.lineWidth = g->lineWidth(); + + const bool hasPipeline = ensurePipelineState(e, sms); + + if (pendingGStatePop) + m_gstate = m_gstateStack.pop(); + + if (!hasPipeline) + return false; + + if (m_renderMode == QSGRendererInterface::RenderMode3D) { + m_gstateStack.push(m_gstate); + setStateForDepthPostPass(); + ensurePipelineState(e, sms, true); + m_gstate = m_gstateStack.pop(); + } + + batch->ubufDataValid = true; + + m_currentMaterial = material; + + renderBatch->batch = batch; + renderBatch->sms = sms; + + return true; +} + +void Renderer::checkLineWidth(QSGGeometry *g) +{ + if (g->drawingMode() == QSGGeometry::DrawLines || g->drawingMode() == QSGGeometry::DrawLineLoop + || g->drawingMode() == QSGGeometry::DrawLineStrip) + { + if (g->lineWidth() != 1.0f) { + static bool checkedWideLineSupport = false; + if (!checkedWideLineSupport) { + checkedWideLineSupport = true; + if (!m_rhi->isFeatureSupported(QRhi::WideLines)) + qWarning("Line widths other than 1 are not supported by the graphics API"); + } + } + } else if (g->drawingMode() == QSGGeometry::DrawPoints) { + if (g->lineWidth() != 1.0f) { + static bool warnedPointSize = false; + if (!warnedPointSize) { + warnedPointSize = true; + qWarning("Point size is not controllable by QSGGeometry. " + "Set gl_PointSize from the vertex shader instead."); + } + } + } +} + +void Renderer::renderMergedBatch(PreparedRenderBatch *renderBatch, bool depthPostPass) +{ + const Batch *batch = renderBatch->batch; + if (!batch->vbo.buf || !batch->ibo.buf) + return; + if (!batchIntersectsDamage(batch)) + return; + + Element *e = batch->first; + QSGGeometryNode *gn = e->node; + QSGGeometry *g = gn->geometry(); + checkLineWidth(g); + + if (batch->clipState.type & ClipState::StencilClip) + enqueueStencilDraw(batch); + + QRhiCommandBuffer *cb = renderTarget().cb; + setGraphicsPipeline(cb, batch, e, depthPostPass); + + for (int i = 0, ie = batch->drawSets.size(); i != ie; ++i) { + const DrawSet &draw = batch->drawSets.at(i); + const QRhiCommandBuffer::VertexInput vbufBindings[] = { + { batch->vbo.buf, quint32(draw.vertices) }, + { batch->vbo.buf, quint32(draw.zorders) } + }; + cb->setVertexInput(VERTEX_BUFFER_BINDING, useDepthBuffer() ? 2 : 1, vbufBindings, + batch->ibo.buf, draw.indices, + m_uint32IndexForRhi ? QRhiCommandBuffer::IndexUInt32 : QRhiCommandBuffer::IndexUInt16); + drawWithDamageScissors(cb, batch, [&] { + cb->drawIndexed(draw.indexCount); + }); + } +} + +bool Renderer::prepareRenderUnmergedBatch(Batch *batch, PreparedRenderBatch *renderBatch) +{ + if (batch->vertexCount == 0) + return false; + + if (isCompositorOutputPass() && clipsDrawToDamage()) { + bool anyVisible = false; + for (Element *el = batch->first; el; el = el->nextInBatch) { + if (el->node && !m_damageTree->isCulled(el)) { + anyVisible = true; + break; + } + } + if (!anyVisible) + return false; + } + + Element *e = batch->first; + Q_ASSERT(e); + + if (Q_UNLIKELY(debug_render())) { + qDebug() << " -" + << batch + << (batch->uploadedThisFrame ? "[ upload]" : "[retained]") + << (e->node->clipList() ? "[ clip]" : "[noclip]") + << (batch->isOpaque ? "[opaque]" : "[ alpha]") + << "[unmerged]" + << " Nodes:" << QString::fromLatin1("%1").arg(qsg_countNodesInBatch(batch), 4).toLatin1().constData() + << " Vertices:" << QString::fromLatin1("%1").arg(batch->vertexCount, 5).toLatin1().constData() + << " Indices:" << QString::fromLatin1("%1").arg(batch->indexCount, 5).toLatin1().constData() + << " root:" << batch->root; + + batch->uploadedThisFrame = false; + } + + const int viewCount = projectionMatrixCount(); + m_current_projection_matrix.resize(viewCount); + for (int viewIndex = 0; viewIndex < viewCount; ++viewIndex) + m_current_projection_matrix[viewIndex] = projectionMatrix(viewIndex); + + m_current_projection_matrix_native_ndc.resize(projectionMatrixWithNativeNDCCount()); + for (int viewIndex = 0; viewIndex < projectionMatrixWithNativeNDCCount(); ++viewIndex) + m_current_projection_matrix_native_ndc[viewIndex] = projectionMatrixWithNativeNDC(viewIndex); + + QSGGeometryNode *gn = e->node; + if (m_renderMode != QSGRendererInterface::RenderMode3D) + updateClipState(gn->clipList(), batch); + + // We always have dirty matrix as all batches are at a unique z range. + QSGMaterialShader::RenderState::DirtyStates dirty = QSGMaterialShader::RenderState::DirtyMatrix; + + // The vertex attributes are assumed to be the same for all elements in the + // unmerged batch since the material (and so the shaders) is the same. + QSGGeometry *g = gn->geometry(); + QSGMaterial *material = gn->activeMaterial(); + ShaderManager::Shader *sms = m_shaderManager->prepareMaterialNoRewrite(material, g, m_renderMode, renderTarget().multiViewCount); + if (!sms) + return false; + + Q_ASSERT(sms->materialShader); + if (m_currentShader != sms) + setActiveRhiShader(sms->materialShader, sms); + + m_current_opacity = gn->inheritedOpacity(); + if (sms->lastOpacity != m_current_opacity) { + dirty |= QSGMaterialShader::RenderState::DirtyOpacity; + sms->lastOpacity = m_current_opacity; + } + + QMatrix4x4 rootMatrix = batch->root ? qsg_matrixForRoot(batch->root) : QMatrix4x4(); + + QSGMaterialShaderPrivate *pd = QSGMaterialShaderPrivate::get(sms->materialShader); + const quint32 ubufSize = quint32(pd->masterUniformData.size()); + if (pd->ubufBinding >= 0) { + quint32 totalUBufSize = 0; + while (e) { + totalUBufSize += aligned(ubufSize, m_ubufAlignment); + e = e->nextInBatch; + } + bool ubufRebuild = false; + if (!batch->ubuf) { + batch->ubuf = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalUBufSize); + ubufRebuild = true; + } else { + if (batch->ubuf->size() < totalUBufSize) { + batch->ubuf->setSize(totalUBufSize); + ubufRebuild = true; + } + } + if (ubufRebuild) { + batch->ubufDataValid = false; + if (!batch->ubuf->create()) { + qWarning("Failed to build uniform buffer of size %u bytes", totalUBufSize); + delete batch->ubuf; + batch->ubuf = nullptr; + return false; + } + } + } + + QSGMaterialShader::RenderState renderState = state(QSGMaterialShader::RenderState::DirtyStates(int(dirty))); + bool pendingGStatePop = false; + updateMaterialStaticData(sms, renderState, + material, batch, &pendingGStatePop); + + int ubufOffset = 0; + QRhiGraphicsPipeline *ps = nullptr; + QRhiGraphicsPipeline *depthPostPassPs = nullptr; + e = batch->first; + + char *directUpdatePtr = nullptr; + if (batch->ubuf->nativeBuffer().slotCount == 0) + directUpdatePtr = batch->ubuf->beginFullDynamicBufferUpdateForCurrentFrame(); + + while (e) { + gn = e->node; + + m_current_model_view_matrix = rootMatrix * *gn->matrix(); + m_current_determinant = m_current_model_view_matrix.determinant(); + + const int viewCount = projectionMatrixCount(); + m_current_projection_matrix.resize(viewCount); + for (int viewIndex = 0; viewIndex < viewCount; ++viewIndex) + m_current_projection_matrix[viewIndex] = projectionMatrix(viewIndex); + + m_current_projection_matrix_native_ndc.resize(projectionMatrixWithNativeNDCCount()); + for (int viewIndex = 0; viewIndex < projectionMatrixWithNativeNDCCount(); ++viewIndex) + m_current_projection_matrix_native_ndc[viewIndex] = projectionMatrixWithNativeNDC(viewIndex); + + if (useDepthBuffer()) { + // this cannot be multiview + m_current_projection_matrix[0](2, 2) = m_zRange; + m_current_projection_matrix[0](2, 3) = calculateElementZOrder(e, m_zRange); + } + + QSGMaterialShader::RenderState renderState = state(QSGMaterialShader::RenderState::DirtyStates(int(dirty))); + updateMaterialDynamicData(sms, renderState, material, batch, e, ubufOffset, ubufSize, directUpdatePtr); + + ubufOffset += aligned(ubufSize, m_ubufAlignment); + + const QSGGeometry::DrawingMode prevDrawMode = m_gstate.drawMode; + const float prevLineWidth = m_gstate.lineWidth; + m_gstate.drawMode = QSGGeometry::DrawingMode(g->drawingMode()); + m_gstate.lineWidth = g->lineWidth(); + + // Do not bother even looking up the ps if the topology has not changed + // since everything else is the same for all elements in the batch. + // (except if the material modified blend state) + if (!ps || m_gstate.drawMode != prevDrawMode || m_gstate.lineWidth != prevLineWidth || pendingGStatePop) { + if (!ensurePipelineState(e, sms)) { + if (pendingGStatePop) + m_gstate = m_gstateStack.pop(); + return false; + } + ps = e->ps; + if (m_renderMode == QSGRendererInterface::RenderMode3D) { + m_gstateStack.push(m_gstate); + setStateForDepthPostPass(); + ensurePipelineState(e, sms, true); + m_gstate = m_gstateStack.pop(); + depthPostPassPs = e->depthPostPassPs; + } + } else { + e->ps = ps; + if (m_renderMode == QSGRendererInterface::RenderMode3D) + e->depthPostPassPs = depthPostPassPs; + } + + // We don't need to bother with asking each node for its material as they + // are all identical (compare==0) since they are in the same batch. + m_currentMaterial = material; + + // We only need to push this on the very first iteration... + dirty &= ~QSGMaterialShader::RenderState::DirtyOpacity; + + e = e->nextInBatch; + } + + if (directUpdatePtr) + batch->ubuf->endFullDynamicBufferUpdateForCurrentFrame(); + + if (pendingGStatePop) + m_gstate = m_gstateStack.pop(); + + batch->ubufDataValid = true; + + renderBatch->batch = batch; + renderBatch->sms = sms; + + return true; +} + +void Renderer::renderUnmergedBatch(PreparedRenderBatch *renderBatch, bool depthPostPass) +{ + const Batch *batch = renderBatch->batch; + if (!batch->vbo.buf) + return; + if (!batchIntersectsDamage(batch)) + return; + + Element *e = batch->first; + + if (batch->clipState.type & ClipState::StencilClip) + enqueueStencilDraw(batch); + + quint32 vOffset = 0; + quint32 iOffset = 0; + QRhiCommandBuffer *cb = renderTarget().cb; + + while (e) { + QSGGeometry *g = e->node->geometry(); + checkLineWidth(g); + const int effectiveIndexSize = m_uint32IndexForRhi ? sizeof(quint32) : g->sizeOfIndex(); + + setGraphicsPipeline(cb, batch, e, depthPostPass); + + const QRhiCommandBuffer::VertexInput vbufBinding(batch->vbo.buf, vOffset); + drawWithDamageScissors(cb, batch, [&] { + if (g->indexCount()) { + if (batch->ibo.buf) { + cb->setVertexInput(VERTEX_BUFFER_BINDING, 1, &vbufBinding, + batch->ibo.buf, iOffset, + effectiveIndexSize == sizeof(quint32) ? QRhiCommandBuffer::IndexUInt32 + : QRhiCommandBuffer::IndexUInt16); + cb->drawIndexed(g->indexCount()); + } + } else { + cb->setVertexInput(VERTEX_BUFFER_BINDING, 1, &vbufBinding); + cb->draw(g->vertexCount()); + } + }); + + vOffset += g->sizeOfVertex() * g->vertexCount(); + iOffset += g->indexCount() * effectiveIndexSize; + + e = e->nextInBatch; + } +} + +void Renderer::setViewportAndScissors(QRhiCommandBuffer *cb, const Batch *batch) +{ + if (!m_pstate.viewportSet) { + m_pstate.viewportSet = true; + cb->setViewport(m_pstate.viewport); + } + + const bool needScissor = (m_damageTree && m_damageTree->scissorEnabled) + || (batch->clipState.type & ClipState::ScissorClip); + if (!needScissor) { + // Regardless of the ps not using scissor, the scissor may need to be + // reset, depending on the backend. So set the viewport again, which in + // turn also sets the scissor on backends where a scissor rect is + // always-on (Vulkan). + if (m_pstate.scissorSet) { + m_pstate.scissorSet = false; + cb->setViewport(m_pstate.viewport); + } + return; + } + + QVarLengthArray rects; + collectScissorRectsForBatch(batch, &rects); + QRect cover; + for (const QRect &r : std::as_const(rects)) + cover = cover.united(r); + if (cover.width() <= 0 || cover.height() <= 0) + cover = this->deviceRect(); + m_pstate.scissorSet = true; + cb->setScissor(QRhiScissor(cover.x(), cover.y(), cover.width(), cover.height())); +} + +void Renderer::collectScissorRectsForBatch(const Batch *batch, + QVarLengthArray *out) const +{ + out->clear(); + QRect clipRect; + const bool hasClip = batch && (batch->clipState.type & ClipState::ScissorClip); + if (hasClip) { + const auto s = batch->clipState.scissor.scissor(); + clipRect = QRect(s[0], s[1], s[2], s[3]); + } + + if (m_damageTree && m_damageTree->scissorEnabled + && !m_damageTree->nativeScissors.isEmpty()) { + for (const QRect &r : m_damageTree->nativeScissors) { + const QRect s = hasClip ? r.intersected(clipRect) : r; + if (s.width() > 0 && s.height() > 0) + out->append(s); + } + return; + } + if (hasClip && clipRect.width() > 0 && clipRect.height() > 0) + out->append(clipRect); +} + +bool Renderer::batchIntersectsDamage(const Batch *batch) const +{ + if (!m_damageTree || !m_damageTree->scissorEnabled) + return true; + if (m_damageTree->sceneScissors.isEmpty()) + return false; + for (Element *el = batch->first; el; el = el->nextInBatch) { + if (!el->damage) + return true; + // Unknown raster extent stores no world bounds; an empty rect would + // intersect nothing and wrongly skip the batch in Full mode. + const QSGNode *sg = el->isRenderNode + ? static_cast(static_cast(el)->renderNode) + : static_cast(el->node); + if (sg && m_damageTree->hasUnknownBounds(sg)) + return true; + if (m_damageTree->sceneScissors.intersects(el->damage->worldBounds())) + return true; + } + return false; +} + +template +void Renderer::drawWithDamageScissors(QRhiCommandBuffer *cb, const Batch *batch, DrawFn &&draw) +{ + if (!m_pstate.viewportSet) { + m_pstate.viewportSet = true; + cb->setViewport(m_pstate.viewport); + } + + const bool needScissor = (m_damageTree && m_damageTree->scissorEnabled) + || (batch->clipState.type & ClipState::ScissorClip); + if (!needScissor) { + // Regardless of the ps not using scissor, the scissor may need to be + // reset, depending on the backend. So set the viewport again, which in + // turn also sets the scissor on backends where a scissor rect is + // always-on (Vulkan). + if (m_pstate.scissorSet) { + m_pstate.scissorSet = false; + cb->setViewport(m_pstate.viewport); + } + draw(); + return; + } + + QVarLengthArray rects; + collectScissorRectsForBatch(batch, &rects); + for (const QRect &r : std::as_const(rects)) { + m_pstate.scissorSet = true; + cb->setScissor(QRhiScissor(r.x(), r.y(), r.width(), r.height())); + draw(); + } +} + +void Renderer::setGraphicsPipeline(QRhiCommandBuffer *cb, const Batch *batch, Element *e, bool depthPostPass) +{ + cb->setGraphicsPipeline(depthPostPass ? e->depthPostPassPs : e->ps); + + if (!m_pstate.viewportSet) { + m_pstate.viewportSet = true; + cb->setViewport(m_pstate.viewport); + } + + if (batch->clipState.type & ClipState::StencilClip) { + Q_ASSERT(e->ps->flags().testFlag(QRhiGraphicsPipeline::UsesStencilRef)); + cb->setStencilRef(batch->clipState.stencilRef); + } + if (!depthPostPass && e->ps->flags().testFlag(QRhiGraphicsPipeline::UsesBlendConstants)) + cb->setBlendConstants(batch->blendConstant); + + cb->setShaderResources(e->srb); +} + +void Renderer::releaseElement(Element *e, bool inDestructor) +{ + if (e->isRenderNode) { + delete static_cast(e); + } else { + if (e->srb) { + if (!inDestructor) { + if (m_shaderManager->srbPool.size() < m_srbPoolThreshold) + m_shaderManager->srbPool.insert(e->srb->serializedLayoutDescription(), e->srb); + else + delete e->srb; + } else { + delete e->srb; + } + e->srb = nullptr; + } + m_elementAllocator.release(e); + } +} + +void Renderer::deleteRemovedElements() +{ + if (!m_elementsToDelete.size()) + return; + + for (int i=0; iremoved) + *e = nullptr; + } + for (int i=0; iremoved) + *e = nullptr; + } + + for (int i=0; iscissorTarget)) { + WSGDamageLog::endOutputFrame(recorded ? "draw recorded" : "prepare only, nothing to draw"); + return; + } + if (m_damageTree->scissorTarget) + WSGDamageLog::endNestedPass(); +} + +void Renderer::render() +{ + // Gracefully handle the lack of a render target - some autotests may rely + // on this in odd cases. + if (!renderTarget().rt) { + commitFlushRegion(); + finishDamagePassLogs(false); + return; + } + + prepareRenderPass(&m_mainRenderPassContext); + if (m_damageTree && m_damageTree->scissorEnabled + && m_damageTree->nativeScissors.isEmpty()) { + m_mainRenderPassContext.valid = false; + finishDamagePassLogs(false); + return; + } + + beginRenderPass(&m_mainRenderPassContext); + recordRenderPass(&m_mainRenderPassContext); + endRenderPass(&m_mainRenderPassContext); + finishDamagePassLogs(true); +} + +// An alternative to render() is to call prepareInline() and renderInline() at +// the appropriate times (i.e. outside of a QRhi::beginPass() and then inside, +// respectively) These allow rendering within a render pass that is started by +// another component. In contrast, render() records a full render pass on its +// own. + +void Renderer::prepareInline() +{ + prepareRenderPass(&m_mainRenderPassContext); +} + +void Renderer::renderInline() +{ + recordRenderPass(&m_mainRenderPassContext); +} + +void Renderer::prepareRenderPass(RenderPassContext *ctx) +{ + if (ctx->valid) + qWarning("prepareRenderPass() called with an already prepared render pass context"); + + ctx->valid = true; + if (tracksDamage()) { + if (isCompositorOutputPass()) { + m_damageTree->idleBlitters.clear(); + m_damageTree->nestedSkipBlitters.clear(); + WSGDamageLog::beginOutputPrepare(); + } else { + m_damageTree->nestedSkipBlitters.clear(); + WSGDamageLog::beginNestedPass(); + } + } + + if (Q_UNLIKELY(debug_dump())) { + qDebug("\n"); + QSGNodeDumper::dump(rootNode()); + } + + ctx->timeRenderLists = 0; + ctx->timePrepareOpaque = 0; + ctx->timePrepareAlpha = 0; + ctx->timeSorting = 0; + ctx->timeUploadOpaque = 0; + ctx->timeUploadAlpha = 0; + + if (Q_UNLIKELY(debug_render() || debug_build())) { + QByteArray type("rebuild:"); + if (m_rebuild == 0) + type += " none"; + if (m_rebuild == FullRebuild) + type += " full"; + else { + if (m_rebuild & BuildRenderLists) + type += " renderlists"; + else if (m_rebuild & BuildRenderListsForTaggedRoots) + type += " partial"; + else if (m_rebuild & BuildBatches) + type += " batches"; + } + + qDebug() << "Renderer::render()" << this << type; + ctx->timer.start(); + } + + m_resourceUpdates = m_rhi->nextResourceUpdateBatch(); + + if (m_rebuild & (BuildRenderLists | BuildRenderListsForTaggedRoots)) { + bool complete = (m_rebuild & BuildRenderLists) != 0; + if (complete) + buildRenderListsFromScratch(); + else + buildRenderListsForTaggedRoots(); + m_rebuild |= BuildBatches; + + if (Q_UNLIKELY(debug_build())) { + qDebug("Opaque render lists %s:", (complete ? "(complete)" : "(partial)")); + for (int i=0; ibatch << " node:" << e->node << " order:" << e->order; + } + qDebug("Alpha render list %s:", complete ? "(complete)" : "(partial)"); + for (int i=0; ibatch << " node:" << e->node << " order:" << e->order; + } + } + } + if (Q_UNLIKELY(debug_render())) ctx->timeRenderLists = ctx->timer.restart(); + + for (int i=0; icleanupRemovedElements(); + for (int i=0; icleanupRemovedElements(); + deleteRemovedElements(); + + cleanupBatches(&m_opaqueBatches); + cleanupBatches(&m_alphaBatches); + + if (m_rebuild & BuildBatches) { + prepareOpaqueBatches(); + if (Q_UNLIKELY(debug_render())) ctx->timePrepareOpaque = ctx->timer.restart(); + prepareAlphaBatches(); + if (Q_UNLIKELY(debug_render())) ctx->timePrepareAlpha = ctx->timer.restart(); + + if (Q_UNLIKELY(debug_build())) { + qDebug("Opaque Batches:"); + for (int i=0; ineedsUpload ? "upload" : "") << " root:" << b->root; + for (Element *e = b->first; e; e = e->nextInBatch) { + qDebug() << " - element:" << e << " node:" << e->node << e->order; + } + } + qDebug("Alpha Batches:"); + for (int i=0; ineedsUpload ? "upload" : "") << " root:" << b->root; + for (Element *e = b->first; e; e = e->nextInBatch) { + qDebug() << " - element:" << e << e->bounds << " node:" << e->node << " order:" << e->order; + } + } + } + } else { + if (Q_UNLIKELY(debug_render())) ctx->timePrepareOpaque = ctx->timePrepareAlpha = ctx->timer.restart(); + } + + + deleteRemovedElements(); + + if (m_rebuild != 0) { + // Then sort opaque batches so that we're drawing the batches with the highest + // order first, maximizing the benefit of front-to-back z-ordering. + if (m_opaqueBatches.size()) + std::sort(&m_opaqueBatches.first(), &m_opaqueBatches.last() + 1, qsg_sort_batch_decreasing_order); + + // Sort alpha batches back to front so that they render correctly. + if (m_alphaBatches.size()) + std::sort(&m_alphaBatches.first(), &m_alphaBatches.last() + 1, qsg_sort_batch_increasing_order); + + m_zRange = m_nextRenderOrder != 0 + ? 1.0 / (m_nextRenderOrder) + : 0; + } + + if (Q_UNLIKELY(debug_render())) ctx->timeSorting = ctx->timer.restart(); + + // Set size to 0, nothing is deallocated, they will "grow" again + // as part of uploadBatch. + m_vertexUploadPool.reset(); + m_indexUploadPool.reset(); + + if (Q_UNLIKELY(debug_upload())) qDebug("Uploading Opaque Batches:"); + for (int i=0; itimeUploadOpaque = ctx->timer.restart(); + + if (Q_UNLIKELY(debug_upload())) qDebug("Uploading Alpha Batches:"); + for (int i=0; itimeUploadAlpha = ctx->timer.restart(); + + if (Q_UNLIKELY(debug_render())) { + qDebug().nospace() << "Rendering:" << Qt::endl + << " -> Opaque: " << qsg_countNodesInBatches(m_opaqueBatches) << " nodes in " << m_opaqueBatches.size() << " batches..." << Qt::endl + << " -> Alpha: " << qsg_countNodesInBatches(m_alphaBatches) << " nodes in " << m_alphaBatches.size() << " batches..."; + } + + m_current_opacity = 1; + m_currentMaterial = nullptr; + m_currentShader = nullptr; + m_currentProgram = nullptr; + m_currentClipState.reset(); + + const QRect viewport = viewportRect(); + + bool renderOpaque = !debug_noopaque(); + bool renderAlpha = !debug_noalpha(); + + m_pstate.viewport = + QRhiViewport(viewport.x(), deviceRect().bottom() - viewport.bottom(), viewport.width(), + viewport.height(), VIEWPORT_MIN_DEPTH, VIEWPORT_MAX_DEPTH); + m_pstate.clearColor = clearColor(); + m_pstate.dsClear = QRhiDepthStencilClearValue(1.0f, 0); + m_pstate.viewportSet = false; + m_pstate.scissorSet = false; + + m_gstate.depthTest = useDepthBuffer(); + m_gstate.depthWrite = useDepthBuffer(); + m_gstate.depthFunc = QRhiGraphicsPipeline::Less; + m_gstate.blending = false; + + m_gstate.cullMode = QRhiGraphicsPipeline::None; + m_gstate.polygonMode = QRhiGraphicsPipeline::Fill; + m_gstate.colorWrite = QRhiGraphicsPipeline::R | QRhiGraphicsPipeline::G + | QRhiGraphicsPipeline::B | QRhiGraphicsPipeline::A; + const bool isOutputPass = isCompositorOutputPass(); + if (m_damageTree) { + m_damageTree->blitterPendingFull = + !clipsDrawToDamage() || lastRenderRegion().isFull; + m_damageTree->blitterPending = m_damageTree->blitterPendingFull + ? WPixmanRegion() : m_damageTree->pendingRegion(); + m_damageTree->scissorEnabled = + isOutputPass && clipsDrawToDamage() && !lastRenderRegion().isFull + && textureRenderTargetPreservesColor(renderTarget().rt); + } + m_gstate.usesScissor = m_damageTree && m_damageTree->scissorEnabled; + m_gstate.stencilTest = false; + + m_gstate.sampleCount = renderTarget().rt->sampleCount(); + m_gstate.multiViewCount = renderTarget().multiViewCount; + + ctx->opaqueRenderBatches.clear(); + if (Q_LIKELY(renderOpaque)) { + for (int i = 0, ie = m_opaqueBatches.size(); i != ie; ++i) { + Batch *b = m_opaqueBatches.at(i); + PreparedRenderBatch renderBatch; + bool ok; + if (b->merged) + ok = prepareRenderMergedBatch(b, &renderBatch); + else + ok = prepareRenderUnmergedBatch(b, &renderBatch); + if (ok) + ctx->opaqueRenderBatches.append(renderBatch); + } + } + + m_gstate.blending = true; + // factors never change, always set for premultiplied alpha based blending + + // depth test stays enabled (if useDepthBuffer(), that is) but no need + // to write out depth from the transparent (back-to-front) pass + m_gstate.depthWrite = false; + + // special case: the 3D plane mode tests against the depth buffer, but does + // not write (and all batches are alpha because this render mode evaluates + // to useDepthBuffer()==false) + if (m_renderMode == QSGRendererInterface::RenderMode3D) { + Q_ASSERT(m_opaqueBatches.isEmpty()); + m_gstate.depthTest = true; + } + + ctx->alphaRenderBatches.clear(); + if (Q_LIKELY(renderAlpha)) { + for (int i = 0, ie = m_alphaBatches.size(); i != ie; ++i) { + Batch *b = m_alphaBatches.at(i); + PreparedRenderBatch renderBatch; + bool ok; + if (b->merged) + ok = prepareRenderMergedBatch(b, &renderBatch); + else if (b->isRenderNode) + ok = prepareRhiRenderNode(b, &renderBatch); + else + ok = prepareRenderUnmergedBatch(b, &renderBatch); + if (ok) + ctx->alphaRenderBatches.append(renderBatch); + } + } + + m_rebuild = 0; + +#if defined(QSGBATCHRENDERER_INVALIDATE_WEDGED_NODES) + m_renderOrderRebuildLower = -1; + m_renderOrderRebuildUpper = -1; +#endif + + if (m_visualizer->mode() != Visualizer::VisualizeNothing) + m_visualizer->prepareVisualize(); + + renderTarget().cb->resourceUpdate(m_resourceUpdates); + m_resourceUpdates = nullptr; + + commitFlushRegion(); + if (tracksDamage() && Q_UNLIKELY(lcWlDamage().isDebugEnabled())) { + if (isCompositorOutputPass() && !m_damageTree->idleBlitters.isEmpty()) { + qCDebug(lcWlDamage) + << WSGDamageLog::frameTag() + << "skip drawing idle blitters (keep last capture; pending does not punch):" + << m_damageTree->idleBlitters.join(QLatin1String("; ")); + } else if (!isCompositorOutputPass() && clipsDrawToDamage() + && !m_damageTree->nestedSkipBlitters.isEmpty()) { + qCDebug(lcWlDamage) << WSGDamageLog::frameTag() << "nested pass skipped blitter draw:" + << m_damageTree->nestedSkipBlitters.join(QLatin1String("; ")); + } + if (isCompositorOutputPass()) { + WSGDamageLog::endOutputPrepare( + WSGDamageLog::describe(lastRenderRegion().region, lastRenderRegion().isFull)); + } + } + + if (!m_damageTree) + return; + const WPixmanRegion extraScissor = m_damageTree->extraScissor; + m_damageTree->extraScissor = { }; + m_damageTree->nativeScissors.clear(); + m_damageTree->sceneScissors = { }; + if (m_damageTree->scissorEnabled) { + WPixmanRegion scene = m_damageTree->lastRenderRegion().region; + scene += extraScissor; + m_damageTree->sceneScissors = scene; + if (!scene.isEmpty()) { + if (scene.rectCount() > kMaxDamageScissorRects) + scene = WPixmanRegion(scene.boundingRect()); + QRegion native; + int count = 0; + const pixman_box32_t *rects = scene.rectangles(&count); + for (int i = 0; i < count && rects; ++i) { + const QRect r(rects[i].x1, rects[i].y1, + rects[i].x2 - rects[i].x1, rects[i].y2 - rects[i].y1); + const QRect s = sceneRectToNativeScissor(r); + if (s.width() > 0 && s.height() > 0) + native += s; + } + if (native.isEmpty()) { + // Mapping failed; pipelines already have UsesScissor. + QRect fallback = viewportRect(); + if (fallback.width() <= 0 || fallback.height() <= 0) + fallback = this->deviceRect(); + if (fallback.width() > 0 && fallback.height() > 0) + m_damageTree->nativeScissors.append(fallback); + } else { + if (native.rectCount() > kMaxDamageScissorRects) + native = QRegion(native.boundingRect()); + for (const QRect &r : native) + m_damageTree->nativeScissors.append(r); + } + } + } +} + +void Renderer::beginRenderPass(RenderPassContext *) +{ + const QSGRenderTarget &rt(renderTarget()); + rt.cb->beginPass(rt.rt, m_pstate.clearColor, m_pstate.dsClear, nullptr, + // we cannot tell if the application will have + // native rendering thrown in to this pass + // (QQuickWindow::beginExternalCommands()), so + // we have no choice but to set the flag always + // (thus triggering using secondary command + // buffers with Vulkan) + QRhiCommandBuffer::ExternalContent + // We do not use GPU compute at all at the moment, this means we can + // get a small performance gain with OpenGL by declaring this. + | QRhiCommandBuffer::DoNotTrackResourcesForCompute); + + if (m_renderPassRecordingCallbacks.start) + m_renderPassRecordingCallbacks.start(m_renderPassRecordingCallbacks.userData); +} + +void Renderer::recordRenderPass(RenderPassContext *ctx) +{ + // prepareRenderPass and recordRenderPass must always be called together. + // They are separate because beginRenderPass and endRenderPass are optional. + // + // The valid call sequence are therefore: + // prepare, begin, record, end + // or + // prepare, record + + if (!ctx->valid) + qWarning("recordRenderPass() called without a prepared render pass context"); + + ctx->valid = false; + + if (tracksDamage() && isCompositorOutputPass()) + WSGDamageLog::beginOutputDraw(); + + QRhiCommandBuffer *cb = renderTarget().cb; + cb->debugMarkBegin(QByteArrayLiteral("Qt Quick scene render")); + + for (int i = 0, ie = ctx->opaqueRenderBatches.size(); i != ie; ++i) { + if (i == 0) + cb->debugMarkMsg(QByteArrayLiteral("Qt Quick opaque batches")); + PreparedRenderBatch *renderBatch = &ctx->opaqueRenderBatches[i]; + if (renderBatch->batch->merged) + renderMergedBatch(renderBatch); + else + renderUnmergedBatch(renderBatch); + } + + for (int i = 0, ie = ctx->alphaRenderBatches.size(); i != ie; ++i) { + if (i == 0) { + if (m_renderMode == QSGRendererInterface::RenderMode3D) + cb->debugMarkMsg(QByteArrayLiteral("Qt Quick 2D-in-3D batches")); + else + cb->debugMarkMsg(QByteArrayLiteral("Qt Quick alpha batches")); + } + PreparedRenderBatch *renderBatch = &ctx->alphaRenderBatches[i]; + if (renderBatch->batch->merged) + renderMergedBatch(renderBatch); + else if (renderBatch->batch->isRenderNode) + renderRhiRenderNode(renderBatch->batch); + else + renderUnmergedBatch(renderBatch); + } + + if (m_renderMode == QSGRendererInterface::RenderMode3D) { + // Depth post-pass to fill up the depth buffer in a way that it + // corresponds to what got rendered to the color buffer in the previous + // (alpha) pass. The previous pass cannot enable depth write due to Z + // fighting. Rather, do it separately in a dedicated color-write-off, + // depth-write-on pass. This enables the 3D content drawn afterwards to + // depth test against the 2D items' rendering. + for (int i = 0, ie = ctx->alphaRenderBatches.size(); i != ie; ++i) { + if (i == 0) + cb->debugMarkMsg(QByteArrayLiteral("Qt Quick 2D-in-3D depth post-pass")); + PreparedRenderBatch *renderBatch = &ctx->alphaRenderBatches[i]; + if (renderBatch->batch->merged) + renderMergedBatch(renderBatch, true); + else if (!renderBatch->batch->isRenderNode) // rendernodes are skipped here for now + renderUnmergedBatch(renderBatch, true); + } + } + + if (m_currentShader) + setActiveRhiShader(nullptr, nullptr); + + cb->debugMarkEnd(); + + if (Q_UNLIKELY(debug_render())) { + qDebug(" -> times: build: %d, prepare(opaque/alpha): %d/%d, sorting: %d, upload(opaque/alpha): %d/%d, record rendering: %d", + (int) ctx->timeRenderLists, + (int) ctx->timePrepareOpaque, (int) ctx->timePrepareAlpha, + (int) ctx->timeSorting, + (int) ctx->timeUploadOpaque, (int) ctx->timeUploadAlpha, + (int) ctx->timer.elapsed()); + } +} + +void Renderer::endRenderPass(RenderPassContext *) +{ + if (m_renderPassRecordingCallbacks.end) + m_renderPassRecordingCallbacks.end(m_renderPassRecordingCallbacks.userData); + + if (m_visualizer->mode() != Visualizer::VisualizeNothing) + m_visualizer->visualize(); + + renderTarget().cb->endPass(); +} + +struct RenderNodeState : public QSGRenderNode::RenderState +{ + const QMatrix4x4 *projectionMatrix() const override { return m_projectionMatrix; } + QRect scissorRect() const override { return m_scissorRect; } + bool scissorEnabled() const override { return m_scissorEnabled; } + int stencilValue() const override { return m_stencilValue; } + bool stencilEnabled() const override { return m_stencilEnabled; } + const QRegion *clipRegion() const override { return nullptr; } + + const QMatrix4x4 *m_projectionMatrix; + QRect m_scissorRect; + int m_stencilValue; + bool m_scissorEnabled; + bool m_stencilEnabled; +}; + +// damageExpansion is item pixels; the recopy bookkeeping is scene space. Map +// the margin through the node's world transform before dilating. The uniform +// max-axis scale stays conservative under rotation. +static QMargins sceneExpansionFor(const QMatrix4x4 &world, const QMargins &itemPx) +{ + if (itemPx.isNull()) + return { }; + const qreal sx = qSqrt(qreal(world(0, 0)) * world(0, 0) + qreal(world(1, 0)) * world(1, 0)); + const qreal sy = qSqrt(qreal(world(0, 1)) * world(0, 1) + qreal(world(1, 1)) * world(1, 1)); + const qreal s = qMax(sx, sy); + if (!(s > 0.0)) + return { }; + return QMargins(qCeil(itemPx.left() * s), qCeil(itemPx.top() * s), + qCeil(itemPx.right() * s), qCeil(itemPx.bottom() * s)); +} + +bool Renderer::prepareRhiRenderNode(Batch *batch, PreparedRenderBatch *renderBatch) +{ + if (Q_UNLIKELY(debug_render())) + qDebug() << " -" << batch << "rendernode"; + + const int viewCount = projectionMatrixCount(); + m_current_projection_matrix.resize(viewCount); + for (int viewIndex = 0; viewIndex < viewCount; ++viewIndex) + m_current_projection_matrix[viewIndex] = projectionMatrix(viewIndex); + m_current_projection_matrix_native_ndc.resize(projectionMatrixWithNativeNDCCount()); + for (int viewIndex = 0; viewIndex < projectionMatrixWithNativeNDCCount(); ++viewIndex) + m_current_projection_matrix_native_ndc[viewIndex] = projectionMatrixWithNativeNDC(viewIndex); + + Q_ASSERT(batch->first->isRenderNode); + RenderNodeElement *e = static_cast(batch->first); + if (auto *blit = dynamic_cast(e->renderNode)) { + // Nested QSGLayer / MultiEffect reuses this Renderer with a Qt RT. + // While damage tracking is active, only blit while the compositor + // output RT is current. Without tracking, preserve the original + // full-recopy behavior. + if (!isCompositorOutputPass()) { + if (Q_UNLIKELY(lcWlDamage().isDebugEnabled())) + m_damageTree->nestedSkipBlitters.append(blit->debugLabel()); + return false; + } + if (clipsDrawToDamage() && m_damageTree->isCulled(e)) { + if (Q_UNLIKELY(lcWlDamage().isDebugEnabled())) + m_damageTree->idleBlitters.append(blit->debugLabel()); + return false; + } + const bool pendingFull = !m_damageTree || m_damageTree->blitterPendingFull; + WPixmanRegion pending = m_damageTree + ? m_damageTree->blitterPending : WPixmanRegion(); + if (m_damageTree && !pendingFull) + pending += m_damageTree->extraScissor; + QRect mapped; + QMatrix4x4 sceneMatrix; + WPixmanRegion recapture; + WSGViewport *recopyViewport = m_damageTree ? m_damageTree->viewport : nullptr; + if (e->damage) { + mapped = e->damage->worldBounds(); + sceneMatrix = e->damage->worldTransform(); + if (auto *backdrop = e->damage->toBackdrop()) { + // damageExpansion is item pixels; map it through the world + // transform before dilating in scene space. + const QMargins expansion = sceneExpansionFor(sceneMatrix, + backdrop->recopyExpansion()); + const QRect sampled = blit->clipDamageExpansion() + ? mapped : mapped.marginsAdded(expansion); + recapture = backdrop->pendingRecopy(recopyViewport); + if (!recapture.isEmpty()) { + recapture = dilateRegion(recapture, expansion); + recapture &= sampled; + } + // Render-only repairs (buffer age and preceding source + // redraws) can also invalidate this capture without changing + // the scene-content damage returned by the tracker. + recapture += pending & sampled; + if (!recapture.isEmpty()) + expandDamageScissor(recapture); + } + } else { + mapped = blit->rect().toAlignedRect(); + } + if (pendingFull || !blit->hasCompositorCapture()) + recapture = WPixmanRegion(mapped); + blit->applyFrame(recapture, mapped, sceneMatrix); + const bool willDraw = blit->needsRender(pending, pendingFull); + if (m_damageTree && Q_UNLIKELY(lcWlDamage().isDebugEnabled())) { + const QString decision = blit->needsSourceCopy() || !blit->hasCompositorCapture() + ? QStringLiteral("will recapture compositor pixels under this blitter") + : QStringLiteral("keep last capture; copy source is empty"); + qCDebug(lcWlDamage) << WSGDamageLog::frameTag() << "blitter" << blit->debugLabel() + << "on-screen" << WSGDamageLog::describe(WPixmanRegion(mapped), false) + << "copy" << WSGDamageLog::describe(recapture, false) << decision; + } + if (!willDraw) { + if (m_damageTree && Q_UNLIKELY(lcWlDamage().isDebugEnabled())) + m_damageTree->idleBlitters.append(blit->debugLabel()); + return false; + } + if (auto *backdrop = e->damage ? e->damage->toBackdrop() : nullptr) + backdrop->consumeRecopy(recapture, recopyViewport); + } + + setActiveRhiShader(nullptr, nullptr); + + QSGRenderNodePrivate *rd = QSGRenderNodePrivate::get(e->renderNode); + rd->m_clip_list = nullptr; + if (m_renderMode != QSGRendererInterface::RenderMode3D) { + QSGNode *clip = e->renderNode->parent(); + while (clip != rootNode()) { + if (clip->type() == QSGNode::ClipNodeType) { + rd->m_clip_list = static_cast(clip); + break; + } + clip = clip->parent(); + } + updateClipState(rd->m_clip_list, batch); + } + + if (auto *blit = dynamic_cast(e->renderNode)) { + rd->m_localMatrix = blit->sceneMatrix(); + } else { + QSGNode *xform = e->renderNode->parent(); + QMatrix4x4 matrix; + QSGNode *root = rootNode(); + if (e->root) { + matrix = qsg_matrixForRoot(e->root); + root = e->root->sgNode; + } + while (xform != root) { + if (xform->type() == QSGNode::TransformNodeType) { + matrix = matrix * static_cast(xform)->combinedMatrix(); + break; + } + xform = xform->parent(); + } + rd->m_localMatrix = matrix; + } + rd->m_matrix = &rd->m_localMatrix; + + QSGNode *opacity = e->renderNode->parent(); + rd->m_opacity = 1.0; + while (opacity != rootNode()) { + if (opacity->type() == QSGNode::OpacityNodeType) { + rd->m_opacity = static_cast(opacity)->combinedOpacity(); + break; + } + opacity = opacity->parent(); + } + + rd->m_rt = renderTarget(); + + rd->m_projectionMatrix.resize(viewCount); + for (int viewIndex = 0; viewIndex < viewCount; ++viewIndex) + rd->m_projectionMatrix[viewIndex] = projectionMatrix(viewIndex); + + if (useDepthBuffer()) { + // this cannot be multiview + rd->m_projectionMatrix[0](2, 2) = m_zRange; + rd->m_projectionMatrix[0](2, 3) = calculateElementZOrder(e, m_zRange); + } + + e->renderNode->prepare(); + + renderBatch->batch = batch; + renderBatch->sms = nullptr; + + return true; +} + +void Renderer::renderRhiRenderNode(const Batch *batch) +{ + if (!batchIntersectsDamage(batch)) + return; + if (batch->clipState.type & ClipState::StencilClip) + enqueueStencilDraw(batch); + + RenderNodeElement *e = static_cast(batch->first); + QSGRenderNodePrivate *rd = QSGRenderNodePrivate::get(e->renderNode); + + RenderNodeState state; + // Expose only the first matrix through the state object, the rest are + // queriable through the QSGRenderNode getters anyway. + state.m_projectionMatrix = &rd->m_projectionMatrix[0]; + state.m_stencilValue = batch->clipState.stencilRef; + state.m_stencilEnabled = batch->clipState.type & ClipState::StencilClip; + + const bool clipScissor = batch->clipState.type & ClipState::ScissorClip; + QRect clipRect; + if (clipScissor) { + const std::array s = batch->clipState.scissor.scissor(); + clipRect = QRect(s[0], s[1], s[2], s[3]); + if (clipRect.width() <= 0 || clipRect.height() <= 0) + return; + } + + const QSGRenderNode::StateFlags changes = e->renderNode->changedStates(); + + QRhiCommandBuffer *cb = renderTarget().cb; + const bool needsExternal = !e->renderNode->flags().testFlag(QSGRenderNode::NoExternalRendering); + + // Repeatable (bounded-rect) nodes redraw exactly the damaged scissors, + // exposing each scissor through RenderState so the node can clip itself. + // Anything else renders once without a damage scissor: repeating render() + // per damage rect is not safe for nodes with side effects, and a single + // render under a damage-bounding-rect union would re-blend clean pixels + // for translucent nodes. Clip scissors still apply in both paths. + if (e->renderNode->flags().testFlag(QSGRenderNode::BoundedRectRendering) + && m_damageTree && m_damageTree->scissorEnabled) { + QVarLengthArray rects; + collectScissorRectsForBatch(batch, &rects); + if (rects.isEmpty()) { + // No damaged pixels under the clip. Bounded nodes draw nothing + // new, but unbounded ones (empty rect) still run once for their + // side effects under the clip. + if (!e->renderNode->rect().isEmpty()) + return; + } else { + if (!m_pstate.viewportSet) { + m_pstate.viewportSet = true; + cb->setViewport(m_pstate.viewport); + } + for (const QRect &r : std::as_const(rects)) { + m_pstate.scissorSet = true; + cb->setScissor(QRhiScissor(r.x(), r.y(), r.width(), r.height())); + state.m_scissorRect = r; + state.m_scissorEnabled = true; + if (needsExternal) + cb->beginExternal(); + e->renderNode->render(&state); + if (needsExternal) + cb->endExternal(); + } + rd->m_matrix = nullptr; + rd->m_clip_list = nullptr; + if ((changes & QSGRenderNode::ViewportState) + || (changes & QSGRenderNode::ScissorState)) + { + // Reset both flags if either is reported as changed, since with the rhi + // it could be setViewport() that will record the resetting of the scissor. + m_pstate.viewportSet = false; + m_pstate.scissorSet = false; + } + return; + } + } else if (m_damageTree && m_damageTree->scissorEnabled) { + // Non-repeatable node with damage scissors on: only draw when some + // damage reaches it (bounded), or always (unbounded). The draw below + // is a full redraw, never a damage-union scissor. + QVarLengthArray rects; + collectScissorRectsForBatch(batch, &rects); + if (rects.isEmpty() && !e->renderNode->rect().isEmpty()) + return; + } + state.m_scissorRect = clipRect; + state.m_scissorEnabled = clipScissor; + if (!m_pstate.viewportSet) { + m_pstate.viewportSet = true; + cb->setViewport(m_pstate.viewport); + } + if (clipScissor) { + m_pstate.scissorSet = true; + cb->setScissor(QRhiScissor(clipRect.x(), clipRect.y(), clipRect.width(), clipRect.height())); + } else if (m_pstate.scissorSet) { + m_pstate.scissorSet = false; + cb->setViewport(m_pstate.viewport); + } + if (needsExternal) + cb->beginExternal(); + e->renderNode->render(&state); + if (needsExternal) + cb->endExternal(); + + rd->m_matrix = nullptr; + rd->m_clip_list = nullptr; + + if ((changes & QSGRenderNode::ViewportState) + || (changes & QSGRenderNode::ScissorState)) + { + // Reset both flags if either is reported as changed, since with the rhi + // it could be setViewport() that will record the resetting of the scissor. + m_pstate.viewportSet = false; + m_pstate.scissorSet = false; + } + + // Do not bother with RenderTargetState. Where applicable, endExternal() + // ensures the correct target is rebound. For others (like Vulkan) it makes + // no sense since render() could not possibly do that on our command buffer + // which is in renderpass recording state. +} + +void Renderer::setVisualizationMode(const QByteArray &mode) +{ + if (mode.isEmpty()) + m_visualizer->setMode(Visualizer::VisualizeNothing); + else if (mode == "clip") + m_visualizer->setMode(Visualizer::VisualizeClipping); + else if (mode == "overdraw") + m_visualizer->setMode(Visualizer::VisualizeOverdraw); + else if (mode == "batches") + m_visualizer->setMode(Visualizer::VisualizeBatches); + else if (mode == "changes") + m_visualizer->setMode(Visualizer::VisualizeChanges); +} + +bool Renderer::hasVisualizationModeWithContinuousUpdate() const +{ + return m_visualizer->mode() == Visualizer::VisualizeOverdraw; +} + +bool operator==(const GraphicsState &a, const GraphicsState &b) noexcept +{ + return a.depthTest == b.depthTest + && a.depthWrite == b.depthWrite + && a.depthFunc == b.depthFunc + && a.blending == b.blending + && a.srcColor == b.srcColor + && a.dstColor == b.dstColor + && a.srcAlpha == b.srcAlpha + && a.dstAlpha == b.dstAlpha + && a.opColor == b.opColor + && a.opAlpha == b.opAlpha + && a.colorWrite == b.colorWrite + && a.cullMode == b.cullMode + && a.usesScissor == b.usesScissor + && a.stencilTest == b.stencilTest + && a.sampleCount == b.sampleCount + && a.drawMode == b.drawMode + && a.lineWidth == b.lineWidth + && a.polygonMode == b.polygonMode + && a.multiViewCount == b.multiViewCount; +} + +bool operator!=(const GraphicsState &a, const GraphicsState &b) noexcept +{ + return !(a == b); +} + +size_t qHash(const GraphicsState &s, size_t seed) noexcept +{ + // do not bother with all fields + return seed + + s.depthTest * 1000 + + s.depthWrite * 100 + + s.depthFunc + + s.blending * 10 + + s.srcColor + + s.cullMode + + s.usesScissor + + s.stencilTest + + s.sampleCount + + s.multiViewCount; +} + +bool operator==(const GraphicsPipelineStateKey &a, const GraphicsPipelineStateKey &b) noexcept +{ + return a.state == b.state + && a.sms->materialShader == b.sms->materialShader + && a.renderTargetDescription == b.renderTargetDescription + && a.srbLayoutDescription == b.srbLayoutDescription; +} + +bool operator!=(const GraphicsPipelineStateKey &a, const GraphicsPipelineStateKey &b) noexcept +{ + return !(a == b); +} + +size_t qHash(const GraphicsPipelineStateKey &k, size_t seed) noexcept +{ + return qHash(k.state, seed) + ^ qHash(k.sms->materialShader) + ^ k.extra.renderTargetDescriptionHash + ^ k.extra.srbLayoutDescriptionHash; +} + +bool operator==(const ShaderKey &a, const ShaderKey &b) noexcept +{ + return a.type == b.type + && a.renderMode == b.renderMode + && a.multiViewCount == b.multiViewCount; +} + +bool operator!=(const ShaderKey &a, const ShaderKey &b) noexcept +{ + return !(a == b); +} + +size_t qHash(const ShaderKey &k, size_t seed) noexcept +{ + return qHash(k.type, seed) ^ int(k.renderMode) ^ k.multiViewCount; +} + +Visualizer::Visualizer(Renderer *renderer) + : m_renderer(renderer), + m_visualizeMode(VisualizeNothing) +{ +} + +Visualizer::~Visualizer() +{ +} + +#define QSGNODE_DIRTY_PARENT (QSGNode::DirtyNodeAdded \ + | QSGNode::DirtyOpacity \ + | QSGNode::DirtyMatrix \ + | QSGNode::DirtyNodeRemoved) + +void Visualizer::visualizeChangesPrepare(Node *n, uint parentChanges) +{ + uint childDirty = (parentChanges | n->dirtyState) & QSGNODE_DIRTY_PARENT; + uint selfDirty = n->dirtyState | parentChanges; + if (n->type() == QSGNode::GeometryNodeType && selfDirty != 0) + m_visualizeChangeSet.insert(n, selfDirty); + SHADOWNODE_TRAVERSE(n) { + visualizeChangesPrepare(child, childDirty); + } +} + +} // namespace WSGBatchRenderer + +WAYLIB_SERVER_END_NAMESPACE + +#include "moc_wsgbatchrenderer_p.cpp" diff --git a/waylib/src/server/qtquick/scenegraph/wsgbatchrenderer_p.h b/waylib/src/server/qtquick/scenegraph/wsgbatchrenderer_p.h new file mode 100644 index 0000000000..517fb1422f --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgbatchrenderer_p.h @@ -0,0 +1,1141 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2016 Jolla Ltd, author: +// Copyright (C) 2016 Robin Burchell +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef WSGBATCHRENDERER_P_H +#define WSGBATCHRENDERER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 12, 0) +#include +#endif + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WSGDamageNode; +class WSGDamageGeometryNode; +class WSGDamageTracker; +class WSGViewport; + +namespace WSGBatchRenderer +{ + +#define QSG_RENDERER_COORD_LIMIT 1000000.0f + +inline quint8 wsgNodeMutabilityGroup(const QSGNode *node) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 12, 0) + return QSGNodePrivate::mutabilityGroup(node); +#else + Q_UNUSED(node); + return 0; +#endif +} + +struct Vec; +struct Rect; +struct Buffer; +struct Chunk; +struct Batch; +struct Node; +class Updater; +class Renderer; +class ShaderManager; + +template class AllocatorPage +{ +public: + // The memory used by this allocator + char data[sizeof(Type) * PageSize]; + + // 'blocks' contains a list of free indices which can be allocated. + // The first available index is found in PageSize - available. + int blocks[PageSize]; + + // 'available' is the number of available instances this page has left to allocate. + int available; + + // This is not strictly needed, but useful for sanity checking and anyway + // pretty small.. + QBitArray allocated; + + AllocatorPage() + : available(PageSize) + , allocated(PageSize) + { + for (int i=0; i class Allocator +{ +public: + Allocator() + { + pages.push_back(new AllocatorPage()); + } + + ~Allocator() + { + qDeleteAll(pages); + } + + Type *allocate() + { + AllocatorPage *p = 0; + for (int i = m_freePage; i < pages.size(); i++) { + if (pages.at(i)->available > 0) { + p = pages.at(i); + m_freePage = i; + break; + } + } + + // we couldn't find a free page from m_freePage to the last page. + // either there is no free pages, or there weren't any in the area we + // scanned: rescanning is expensive, so let's just assume there isn't + // one. when an item is released, we'll reset m_freePage anyway. + if (!p) { + p = new AllocatorPage(); + m_freePage = pages.size(); + pages.push_back(p); + } + uint pos = p->blocks[PageSize - p->available]; + void *mem = p->at(pos); + p->available--; + p->allocated.setBit(pos); + Type *t = (Type*)mem; + return t; + } + + void releaseExplicit(uint pageIndex, uint index) + { + AllocatorPage *page = pages.at(pageIndex); + if (!page->allocated.testBit(index)) + qFatal("Double delete in allocator: page=%d, index=%d", pageIndex , index); + + // Zero this instance as we're done with it. + void *mem = page->at(index); + memset(mem, 0, sizeof(Type)); + + page->allocated[index] = false; + page->available++; + page->blocks[PageSize - page->available] = index; + + // Remove the pages if they are empty and they are the last ones. We need to keep the + // order of pages since we have references to their index, so we can only remove + // from the end. + while (page->available == PageSize && pages.size() > 1 && pages.back() == page) { + pages.pop_back(); + delete page; + page = pages.back(); + } + + // Reset the free page to force a scan for a new free point. + m_freePage = 0; + } + + void release(Type *t) + { + int pageIndex = -1; + for (int i=0; i *p = pages.at(i); + if ((Type *) (&p->data[0]) <= t && (Type *) (&p->data[PageSize * sizeof(Type)]) > t) { + pageIndex = i; + break; + } + } + Q_ASSERT(pageIndex >= 0); + + AllocatorPage *page = pages.at(pageIndex); + int index = (quint64(t) - quint64(&page->data[0])) / sizeof(Type); + + releaseExplicit(pageIndex, index); + } + + QList *> pages; + int m_freePage = 0; +}; + + +inline bool hasMaterialWithBlending(QSGGeometryNode *n) +{ + return (n->opaqueMaterial() ? n->opaqueMaterial()->flags() & QSGMaterial::Blending + : n->material()->flags() & QSGMaterial::Blending); +} + +struct Pt { + float x, y; + + void map(const QMatrix4x4 &mat) { + Pt r; + const float *m = mat.constData(); + r.x = x * m[0] + y * m[4] + m[12]; + r.y = x * m[1] + y * m[5] + m[13]; + x = r.x; + y = r.y; + } + + void set(float nx, float ny) { + x = nx; + y = ny; + } +}; + +inline QDebug operator << (QDebug d, const Pt &p) { + d << "Pt(" << p.x << p.y << ")"; + return d; +} + + + +struct Rect { + Pt tl, br; // Top-Left (min) and Bottom-Right (max) + + void operator |= (const Pt &pt) { + if (pt.x < tl.x) + tl.x = pt.x; + if (pt.x > br.x) + br.x = pt.x; + if (pt.y < tl.y) + tl.y = pt.y; + if (pt.y > br.y) + br.y = pt.y; + } + + void operator |= (const Rect &r) { + if (r.tl.x < tl.x) + tl.x = r.tl.x; + if (r.tl.y < tl.y) + tl.y = r.tl.y; + if (r.br.x > br.x) + br.x = r.br.x; + if (r.br.y > br.y) + br.y = r.br.y; + } + + void map(const QMatrix4x4 &m); + + void set(float left, float top, float right, float bottom) { + tl.set(left, top); + br.set(right, bottom); + } + + bool intersects(const Rect &r) { + bool xOverlap = r.tl.x < br.x && r.br.x > tl.x; + bool yOverlap = r.tl.y < br.y && r.br.y > tl.y; + return xOverlap && yOverlap; + } + + bool isOutsideFloatRange() const { + return tl.x < -QSG_RENDERER_COORD_LIMIT + || tl.y < -QSG_RENDERER_COORD_LIMIT + || br.x > QSG_RENDERER_COORD_LIMIT + || br.y > QSG_RENDERER_COORD_LIMIT; + } +}; + +inline QDebug operator << (QDebug d, const Rect &r) { + d << "Rect(" << r.tl.x << r.tl.y << r.br.x << r.br.y << ")"; + return d; +} + +struct Buffer { + quint32 size; + // Data is only valid while preparing the upload. Exception is if we are using the + // broken IBO workaround or we are using a visualization mode. + char *data; + QRhiBuffer *buf; + uint nonDynamicChangeCount; +}; + +struct Element { + Element() + : boundsComputed(false) + , boundsOutsideFloatRange(false) + , translateOnlyToRoot(false) + , removed(false) + , orphaned(false) + , isRenderNode(false) + , isMaterialBlended(false) + , mutabilityGroup(0) + { + } + + void setNode(QSGGeometryNode *n) { + node = n; + isMaterialBlended = hasMaterialWithBlending(n); + mutabilityGroup = wsgNodeMutabilityGroup(n); + } + + inline void ensureBoundsValid() { + if (!boundsComputed) + computeBounds(); + } + void computeBounds(); + + QSGGeometryNode *node = nullptr; + WSGDamageNode *damage = nullptr; + Batch *batch = nullptr; + Element *nextInBatch = nullptr; + Node *root = nullptr; + + Rect bounds; // in device coordinates + + int order = 0; + QRhiShaderResourceBindings *srb = nullptr; + QRhiGraphicsPipeline *ps = nullptr; + QRhiGraphicsPipeline *depthPostPassPs = nullptr; + + uint boundsComputed : 1; + uint boundsOutsideFloatRange : 1; + uint translateOnlyToRoot : 1; + uint removed : 1; + uint orphaned : 1; + uint isRenderNode : 1; + uint isMaterialBlended : 1; + uint mutabilityGroup : 4; +}; + +struct RenderNodeElement : public Element { + + RenderNodeElement(QSGRenderNode *rn) + : renderNode(rn) + { + isRenderNode = true; + } + + QSGRenderNode *renderNode; +}; + +struct BatchRootInfo { + BatchRootInfo() {} + QSet subRoots; + Node *parentRoot = nullptr; + int lastOrder = -1; + int firstOrder = -1; + int availableOrders = 0; +}; + +struct ClipBatchRootInfo : public BatchRootInfo +{ + QMatrix4x4 matrix; +}; + +struct DrawSet +{ + DrawSet(int v, int z, int i) + : vertices(v) + , zorders(z) + , indices(i) + { + } + DrawSet() {} + int vertices = 0; + int zorders = 0; + int indices = 0; + int indexCount = 0; +}; + +enum BatchCompatibility +{ + BatchBreaksOnCompare, + BatchIsCompatible +}; + +struct ClipState +{ + enum ClipTypeBit + { + NoClip = 0x00, + ScissorClip = 0x01, + StencilClip = 0x02 + }; + Q_DECLARE_FLAGS(ClipType, ClipTypeBit) + + const QSGClipNode *clipList; + ClipType type; + QRhiScissor scissor; + int stencilRef; + + inline void reset(); +}; + +struct StencilClipState +{ + StencilClipState() : drawCalls(1) { } + + bool updateStencilBuffer = false; + QRhiShaderResourceBindings *srb = nullptr; + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ibuf = nullptr; + QRhiBuffer *ubuf = nullptr; + + struct StencilDrawCall { + int stencilRef; + int vertexCount; + int indexCount; + QRhiCommandBuffer::IndexFormat indexFormat; + quint32 vbufOffset; + quint32 ibufOffset; + quint32 ubufOffset; + }; + QDataBuffer drawCalls; + + inline void reset(); +}; + +struct Batch +{ + Batch() : drawSets(1) {} + bool geometryWasChanged(QSGGeometryNode *gn); + BatchCompatibility isMaterialCompatible(Element *e) const; + void invalidate(); + void cleanupRemovedElements(); + + bool isTranslateOnlyToRoot() const; + bool isSafeToBatch() const; + + // pseudo-constructor... + void init() { + // Only non-reusable members are reset here. See Renderer::newBatch(). + first = nullptr; + root = nullptr; + vertexCount = 0; + indexCount = 0; + isOpaque = false; + needsUpload = false; + merged = false; + positionAttribute = -1; + uploadedThisFrame = false; + isRenderNode = false; + ubufDataValid = false; + needsPurge = false; + clipState.reset(); + blendConstant = QColor(); + } + + Element *first; + Node *root; + + int positionAttribute; + + int vertexCount; + int indexCount; + + int lastOrderInBatch; + + uint isOpaque : 1; + uint needsUpload : 1; + uint merged : 1; + uint isRenderNode : 1; + uint ubufDataValid : 1; + uint needsPurge : 1; + + mutable uint uploadedThisFrame : 1; // solely for debugging purposes + + Buffer vbo; + Buffer ibo; + QRhiBuffer *ubuf; + ClipState clipState; + StencilClipState stencilClipState; + QColor blendConstant; + + QDataBuffer drawSets; +}; + +// NOTE: Node is zero-initialized by the Allocator. +struct Node +{ + using LocalRectFn = QRectF (*)(QSGNode *); + + QSGNode *sgNode; + void *data; + WSGDamageNode *damage; + WSGDamageGeometryNode *aggregateDamage; + LocalRectFn localRectFn; + + Node *m_parent; + Node *m_child; + Node *m_next; + Node *m_prev; + + Node *parent() const { return m_parent; } + + void append(Node *child) { + Q_ASSERT(child); + Q_ASSERT(!hasChild(child)); + Q_ASSERT(child->m_parent == nullptr); + Q_ASSERT(child->m_next == nullptr); + Q_ASSERT(child->m_prev == nullptr); + + if (!m_child) { + child->m_next = child; + child->m_prev = child; + m_child = child; + } else { + m_child->m_prev->m_next = child; + child->m_prev = m_child->m_prev; + m_child->m_prev = child; + child->m_next = m_child; + } + child->setParent(this); + } + + void remove(Node *child) { + Q_ASSERT(child); + Q_ASSERT(hasChild(child)); + + // only child.. + if (child->m_next == child) { + m_child = nullptr; + } else { + if (m_child == child) + m_child = child->m_next; + child->m_next->m_prev = child->m_prev; + child->m_prev->m_next = child->m_next; + } + child->m_next = nullptr; + child->m_prev = nullptr; + child->setParent(nullptr); + } + + Node *firstChild() const { return m_child; } + + Node *sibling() const { + Q_ASSERT(m_parent); + return m_next == m_parent->m_child ? nullptr : m_next; + } + + void setParent(Node *p) { + Q_ASSERT(m_parent == nullptr || p == nullptr); + m_parent = p; + } + + bool hasChild(Node *child) const { + Node *n = m_child; + while (n && n != child) + n = n->sibling(); + return n; + } + + + + QSGNode::DirtyState dirtyState; + + uint isOpaque : 1; + uint isBatchRoot : 1; + uint becameBatchRoot : 1; + + inline QSGNode::NodeType type() const { return sgNode->type(); } + + inline Element *element() const { + Q_ASSERT(sgNode->type() == QSGNode::GeometryNodeType); + return (Element *) data; + } + + inline RenderNodeElement *renderNodeElement() const { + Q_ASSERT(sgNode->type() == QSGNode::RenderNodeType); + return (RenderNodeElement *) data; + } + + inline ClipBatchRootInfo *clipInfo() const { + Q_ASSERT(sgNode->type() == QSGNode::ClipNodeType); + return (ClipBatchRootInfo *) data; + } + + inline BatchRootInfo *rootInfo() const { + Q_ASSERT(sgNode->type() == QSGNode::ClipNodeType + || (sgNode->type() == QSGNode::TransformNodeType && isBatchRoot)); + return (BatchRootInfo *) data; + } +}; + +class Updater : public QSGNodeUpdater +{ +public: + Updater(Renderer *r); + + void visitOpacityNode(Node *n); + void visitTransformNode(Node *n); + void visitGeometryNode(Node *n); + void visitClipNode(Node *n); + void updateRootTransforms(Node *n); + void updateRootTransforms(Node *n, Node *root, const QMatrix4x4 &combined); + + void updateStates(QSGNode *n) override; + void visitNode(Node *n); + void registerWithParentRoot(QSGNode *subRoot, QSGNode *parentRoot); + +private: + Renderer *renderer; + + QDataBuffer m_roots; + QDataBuffer m_rootMatrices; + + int m_added; + int m_transformChange; + int m_opacityChange; + + QMatrix4x4 m_identityMatrix; +}; + +struct GraphicsState +{ + bool depthTest = false; + bool depthWrite = false; + QRhiGraphicsPipeline::CompareOp depthFunc = QRhiGraphicsPipeline::Less; + bool blending = false; + QRhiGraphicsPipeline::BlendFactor srcColor = QRhiGraphicsPipeline::One; + QRhiGraphicsPipeline::BlendFactor dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha; + QRhiGraphicsPipeline::BlendFactor srcAlpha = QRhiGraphicsPipeline::One; + QRhiGraphicsPipeline::BlendFactor dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha; + QRhiGraphicsPipeline::BlendOp opColor = QRhiGraphicsPipeline::Add; + QRhiGraphicsPipeline::BlendOp opAlpha = QRhiGraphicsPipeline::Add; + QRhiGraphicsPipeline::ColorMask colorWrite = QRhiGraphicsPipeline::ColorMask(0xF); + QRhiGraphicsPipeline::CullMode cullMode = QRhiGraphicsPipeline::None; + bool usesScissor = false; + bool stencilTest = false; + int sampleCount = 1; + QSGGeometry::DrawingMode drawMode = QSGGeometry::DrawTriangles; + float lineWidth = 1.0f; + QRhiGraphicsPipeline::PolygonMode polygonMode = QRhiGraphicsPipeline::Fill; + int multiViewCount = 0; +}; + +bool operator==(const GraphicsState &a, const GraphicsState &b) noexcept; +bool operator!=(const GraphicsState &a, const GraphicsState &b) noexcept; +size_t qHash(const GraphicsState &s, size_t seed = 0) noexcept; + +struct ShaderManagerShader; + +struct GraphicsPipelineStateKey +{ + GraphicsState state; + const ShaderManagerShader *sms; + QList renderTargetDescription; + QList srbLayoutDescription; + struct { + size_t renderTargetDescriptionHash; + size_t srbLayoutDescriptionHash; + } extra; + static GraphicsPipelineStateKey create(const GraphicsState &state, + const ShaderManagerShader *sms, + const QRhiRenderPassDescriptor *rpDesc, + const QRhiShaderResourceBindings *srb) + { + const QList rtDesc = rpDesc->serializedFormat(); + const QList srbDesc = srb->serializedLayoutDescription(); + return { state, sms, rtDesc, srbDesc, { qHash(rtDesc), qHash(srbDesc) } }; + } +}; + +bool operator==(const GraphicsPipelineStateKey &a, const GraphicsPipelineStateKey &b) noexcept; +bool operator!=(const GraphicsPipelineStateKey &a, const GraphicsPipelineStateKey &b) noexcept; +size_t qHash(const GraphicsPipelineStateKey &k, size_t seed = 0) noexcept; + +struct ShaderKey +{ + QSGMaterialType *type; + QSGRendererInterface::RenderMode renderMode; + int multiViewCount; +}; + +bool operator==(const ShaderKey &a, const ShaderKey &b) noexcept; +bool operator!=(const ShaderKey &a, const ShaderKey &b) noexcept; +size_t qHash(const ShaderKey &k, size_t seed = 0) noexcept; + +struct ShaderManagerShader +{ + ~ShaderManagerShader() { + delete materialShader; + } + QSGMaterialShader *materialShader = nullptr; + QRhiVertexInputLayout inputLayout; + QVarLengthArray stages; + float lastOpacity; +}; + +class ShaderManager : public QObject +{ + Q_OBJECT +public: + using Shader = ShaderManagerShader; + + ShaderManager(QSGDefaultRenderContext *ctx) : context(ctx) { } + ~ShaderManager() { + qDeleteAll(rewrittenShaders); + qDeleteAll(stockShaders); + } + + void clearCachedRendererData(); + + QHash pipelineCache; + + QMultiHash, QRhiShaderResourceBindings *> srbPool; + QList srbLayoutDescSerializeWorkspace; + +public Q_SLOTS: + void invalidated(); + +public: + Shader *prepareMaterial(QSGMaterial *material, + const QSGGeometry *geometry = nullptr, + QSGRendererInterface::RenderMode renderMode = QSGRendererInterface::RenderMode2D, + int multiViewCount = 0); + Shader *prepareMaterialNoRewrite(QSGMaterial *material, + const QSGGeometry *geometry = nullptr, + QSGRendererInterface::RenderMode renderMode = QSGRendererInterface::RenderMode2D, + int multiViewCount = 0); + +private: + QHash rewrittenShaders; + QHash stockShaders; + + QSGDefaultRenderContext *context; +}; + +struct RenderPassState +{ + QRhiViewport viewport; + QColor clearColor; + QRhiDepthStencilClearValue dsClear; + bool viewportSet; + bool scissorSet; +}; + +class Visualizer +{ +public: + enum VisualizeMode { + VisualizeNothing, + VisualizeBatches, + VisualizeClipping, + VisualizeChanges, + VisualizeOverdraw + }; + + Visualizer(Renderer *renderer); + virtual ~Visualizer(); + + VisualizeMode mode() const { return m_visualizeMode; } + void setMode(VisualizeMode mode) { m_visualizeMode = mode; } + + virtual void visualizeChangesPrepare(Node *n, uint parentChanges = 0); + virtual void prepareVisualize() = 0; + virtual void visualize() = 0; + + virtual void releaseResources() = 0; + +protected: + Renderer *m_renderer; + VisualizeMode m_visualizeMode; + QHash m_visualizeChangeSet; +}; + +class Renderer : public QSGRenderer +{ +public: + Renderer(QSGDefaultRenderContext *ctx, QSGRendererInterface::RenderMode renderMode = QSGRendererInterface::RenderMode2D); + ~Renderer(); + + static Renderer *create(QSGRenderContext *rc, QSGRendererInterface::RenderMode renderMode = QSGRendererInterface::RenderMode2D) + { + auto *drc = qobject_cast(rc); + Q_ASSERT(drc); + return new Renderer(drc, renderMode); + } + + bool usesDepthBuffer() const { return useDepthBuffer(); } + ShaderManager *shaderManager() const { return m_shaderManager; } + + // GPU redraw region (including buffer age). Used for QRhiScissor. + const WDamageRegion &lastRenderRegion() const; + + // Content dirty region (scene graph damage only). Used for wlr_damage_ring. + const WDamageRegion &lastFlushRegion() const; + + WSGDamageTracker *damageTracker() const; + void setDamage(const WDamageRegion &flush, const WDamageRegion &render, + const QRect &sceneOutputRect = {}, WSGViewport *viewport = nullptr); + // Restrict damage scissors to this RT. Nested QSGLayer/MultiEffect + // renders reuse this Renderer with a different RT; they must not inherit + // the output's scene-space scissors. + void setDamageScissorTarget(QRhiRenderTarget *rt); + + // Replaces subtreeRoot's detailed damage graph with one conservative + // geometry proxy. localBounds is in subtreeRoot coordinates and must cover + // every pixel the subtree can render. Backdrop subtrees are rejected; a + // backdrop added later automatically restores exact damage tracking. + bool setDamageSubtreeAggregation(QSGNode *subtreeRoot, + bool enabled, + const QRectF &localBounds = { }); + + enum class DamageMode + { + Off, // No tracker. Draw and present like pre-damage QSGBatchRenderer. + Commit, // Track and report flush to the output. Draw is still fullscreen. + Full, // Track, scissor, cull, and recopy blitters by damage. + }; + static DamageMode defaultDamageMode(); + void setDamageMode(DamageMode mode); + DamageMode damageMode() const + { + return m_damageMode; + } + +protected: + void nodeChanged(QSGNode *node, QSGNode::DirtyState state) override; + void render() override; + void prepareInline() override; + void renderInline() override; + void releaseCachedResources() override; + + struct PreparedRenderBatch { + const Batch *batch; + ShaderManager::Shader *sms; + }; + + struct RenderPassContext { + bool valid = false; + QVarLengthArray opaqueRenderBatches; + QVarLengthArray alphaRenderBatches; + QElapsedTimer timer; + quint64 timeRenderLists; + quint64 timePrepareOpaque; + quint64 timePrepareAlpha; + quint64 timeSorting; + quint64 timeUploadOpaque; + quint64 timeUploadAlpha; + }; + + // update batches and queue and commit rhi resource updates + void prepareRenderPass(RenderPassContext *ctx); + // records the beginPass() + void beginRenderPass(RenderPassContext *ctx); + // records the draw calls, must be preceded by a prepareRenderPass at minimum, + // and also surrounded by begin/endRenderPass unless we are recording inside an + // already started pass. + void recordRenderPass(RenderPassContext *ctx); + // does visualizing if enabled and records the endPass() + void endRenderPass(RenderPassContext *ctx); + +private: + enum RebuildFlag { + BuildRenderListsForTaggedRoots = 0x0001, + BuildRenderLists = 0x0002, + BuildBatches = 0x0004, + FullRebuild = 0xffff + }; + + friend class Updater; + friend class RhiVisualizer; + + void destroyGraphicsResources(); + void map(Buffer *buffer, quint32 byteSize, bool isIndexBuf = false); + void unmap(Buffer *buffer, bool isIndexBuf = false); + + void buildRenderListsFromScratch(); + void buildRenderListsForTaggedRoots(); + void tagSubRoots(Node *node); + void buildRenderLists(QSGNode *node); + + void deleteRemovedElements(); + void cleanupBatches(QDataBuffer *batches); + void prepareOpaqueBatches(); + bool checkOverlap(int first, int last, const Rect &bounds); + void prepareAlphaBatches(); + void invalidateBatchAndOverlappingRenderOrders(Batch *batch); + + void uploadBatch(Batch *b); + void uploadMergedElement(Element *e, int vaOffset, char **vertexData, char **zData, char **indexData, void *iBasePtr, int *indexCount); + + bool ensurePipelineState(Element *e, const ShaderManager::Shader *sms, bool depthPostPass = false); + QRhiTexture *dummyTexture(); + void updateMaterialDynamicData(ShaderManager::Shader *sms, QSGMaterialShader::RenderState &renderState, + QSGMaterial *material, const Batch *batch, Element *e, int ubufOffset, int ubufRegionSize, + char *directUpdatePtr); + void updateMaterialStaticData(ShaderManager::Shader *sms, QSGMaterialShader::RenderState &renderState, + QSGMaterial *material, Batch *batch, bool *gstateChanged); + void checkLineWidth(QSGGeometry *g); + bool prepareRenderMergedBatch(Batch *batch, PreparedRenderBatch *renderBatch); + void renderMergedBatch(PreparedRenderBatch *renderBatch, bool depthPostPass = false); + bool prepareRenderUnmergedBatch(Batch *batch, PreparedRenderBatch *renderBatch); + void renderUnmergedBatch(PreparedRenderBatch *renderBatch, bool depthPostPass = false); + void setViewportAndScissors(QRhiCommandBuffer *cb, const Batch *batch); + void collectScissorRectsForBatch(const Batch *batch, QVarLengthArray *out) const; + bool batchIntersectsDamage(const Batch *batch) const; + template + void drawWithDamageScissors(QRhiCommandBuffer *cb, const Batch *batch, DrawFn &&draw); + void setGraphicsPipeline(QRhiCommandBuffer *cb, const Batch *batch, Element *e, bool depthPostPass = false); + ClipState::ClipType updateStencilClip(const QSGClipNode *clip); + void updateClip(const QSGClipNode *clipList, const Batch *batch); + void applyClipStateToGraphicsState(); + QRhiGraphicsPipeline *buildStencilPipeline(const Batch *batch, bool firstStencilClipInBatch); + void updateClipState(const QSGClipNode *clipList, Batch *batch); + void enqueueStencilDraw(const Batch *batch); + const QMatrix4x4 &matrixForRoot(Node *node); + void renderRenderNode(Batch *batch); + bool prepareRhiRenderNode(Batch *batch, PreparedRenderBatch *renderBatch); + void renderRhiRenderNode(const Batch *batch); + void setActiveShader(QSGMaterialShader *program, ShaderManager::Shader *shader); + void setActiveRhiShader(QSGMaterialShader *program, ShaderManager::Shader *shader); + + bool changeBatchRoot(Node *node, Node *newRoot); + void registerBatchRoot(Node *childRoot, Node *parentRoot); + void removeBatchRootFromParent(Node *childRoot); + void nodeChangedBatchRoot(Node *node, Node *root); + void turnNodeIntoBatchRoot(Node *node); + void nodeWasTransformed(Node *node, int *vertexCount); + void nodeWasRemoved(Node *node); + void nodeWasAdded(QSGNode *node, Node *shadowParent); + BatchRootInfo *batchRootInfo(Node *node); + void updateLineWidth(QSGGeometry *g); + + inline Batch *newBatch(); + void invalidateAndRecycleBatch(Batch *b); + void releaseElement(Element *e, bool inDestructor = false); + + void setVisualizationMode(const QByteArray &mode) override; + bool hasVisualizationModeWithContinuousUpdate() const override; + + bool tracksDamage() const + { + return m_damageTree != nullptr; + } + bool clipsDrawToDamage() const + { + return tracksDamage() && m_damageMode == DamageMode::Full; + } + void createDamageTree(); + void destroyDamageTree(); + void commitFlushRegion(); + void expandDamageScissor(const WPixmanRegion &sceneRegion); + void finishDamagePassLogs(bool recorded); + bool isCompositorOutputPass() const; + QRect sceneRectToNativeScissor(const QRectF &bbox, bool pad = true) const; + + QSGDefaultRenderContext *m_context; + QSGRendererInterface::RenderMode m_renderMode; + QSet m_taggedRoots; + QDataBuffer m_opaqueRenderList; + QDataBuffer m_alphaRenderList; + int m_nextRenderOrder; + bool m_partialRebuild; + QSGNode *m_partialRebuildRoot; + bool m_forceNoDepthBuffer; + struct DamageTree; + std::unique_ptr m_damageTree; + QSGNode *m_damageTrackedRoot = nullptr; + DamageMode m_damageMode = DamageMode::Full; + + QHash m_renderNodeElements; + QDataBuffer m_opaqueBatches; + QDataBuffer m_alphaBatches; + QHash m_nodes; + + QDataBuffer m_batchPool; + QDataBuffer m_elementsToDelete; + QDataBuffer m_tmpAlphaElements; + QDataBuffer m_tmpOpaqueElements; + + QDataBuffer m_vboPool; + QDataBuffer m_iboPool; + quint32 m_vboPoolCost; + quint32 m_iboPoolCost; + + uint m_rebuild; + qreal m_zRange; +#if defined(QSGBATCHRENDERER_INVALIDATE_WEDGED_NODES) + int m_renderOrderRebuildLower; + int m_renderOrderRebuildUpper; +#endif + + int m_batchNodeThreshold; + int m_batchVertexThreshold; + int m_srbPoolThreshold; + int m_bufferPoolSizeLimit; + int m_minimumOrderPadding; + + Visualizer *m_visualizer; + + ShaderManager *m_shaderManager; // per rendercontext, shared + QSGMaterial *m_currentMaterial; + QSGMaterialShader *m_currentProgram; + ShaderManager::Shader *m_currentShader; + ClipState m_currentClipState; + + QDataBuffer m_vertexUploadPool; + QDataBuffer m_indexUploadPool; + + Allocator m_nodeAllocator; + Allocator m_elementAllocator; + + RenderPassContext m_mainRenderPassContext; + QRhiResourceUpdateBatch *m_resourceUpdates = nullptr; + uint m_ubufAlignment; + bool m_uint32IndexForRhi; + GraphicsState m_gstate; + RenderPassState m_pstate; + QStack m_gstateStack; + QHash m_samplers; + QRhiTexture *m_dummyTexture = nullptr; + + struct StencilClipCommonData { + QRhiGraphicsPipeline *replacePs = nullptr; + QRhiGraphicsPipeline *incrPs = nullptr; + QShader vs; + QShader fs; + QRhiVertexInputLayout inputLayout; + QRhiGraphicsPipeline::Topology topology; + inline void reset(); + } m_stencilClipCommon; + + inline int mergedIndexElemSize() const; + inline bool useDepthBuffer() const; + inline void setStateForDepthPostPass(); +}; + +Batch *Renderer::newBatch() +{ + Batch *b; + int size = m_batchPool.size(); + if (size) { + b = m_batchPool.at(size - 1); + // vbo, ibo, ubuf, stencil-related buffers are reused + m_batchPool.resize(size - 1); + } else { + b = new Batch(); + Q_ASSERT(offsetof(Batch, ibo) == sizeof(Buffer) + offsetof(Batch, vbo)); + memset(&b->vbo, 0, sizeof(Buffer) * 2); // Clear VBO & IBO + b->ubuf = nullptr; + b->stencilClipState.reset(); + } + // initialize (when new batch) or reset (when reusing a batch) the non-reusable fields + b->init(); + return b; +} + +int Renderer::mergedIndexElemSize() const +{ + return m_uint32IndexForRhi ? sizeof(quint32) : sizeof(quint16); +} + +// "use" here means that both depth test and write is wanted (the latter for +// opaque batches only). Therefore neither RenderMode2DNoDepthBuffer nor +// RenderMode3D must result in true. So while RenderMode3D requires a depth +// buffer, this here must say false. In addition, m_forceNoDepthBuffer is a +// dynamic override relevant with QSGRenderNode. +// +bool Renderer::useDepthBuffer() const +{ + return !m_forceNoDepthBuffer && m_renderMode == QSGRendererInterface::RenderMode2D; +} + +void Renderer::setStateForDepthPostPass() +{ + m_gstate.colorWrite = {}; + m_gstate.depthWrite = true; + m_gstate.depthTest = true; + m_gstate.depthFunc = QRhiGraphicsPipeline::Less; +} + +void Renderer::StencilClipCommonData::reset() +{ + delete replacePs; + replacePs = nullptr; + + delete incrPs; + incrPs = nullptr; + + vs = QShader(); + fs = QShader(); +} + +void ClipState::reset() +{ + clipList = nullptr; + type = NoClip; + stencilRef = 0; +} + +void StencilClipState::reset() +{ + updateStencilBuffer = false; + + delete srb; + srb = nullptr; + + delete vbuf; + vbuf = nullptr; + + delete ibuf; + ibuf = nullptr; + + delete ubuf; + ubuf = nullptr; + + drawCalls.reset(); +} + +} // namespace WSGBatchRenderer + +WAYLIB_SERVER_END_NAMESPACE + +QT_BEGIN_NAMESPACE +Q_DECLARE_TYPEINFO(WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::GraphicsState, Q_RELOCATABLE_TYPE); +Q_DECLARE_TYPEINFO(WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::GraphicsPipelineStateKey, + Q_RELOCATABLE_TYPE); +Q_DECLARE_TYPEINFO(WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::RenderPassState, Q_RELOCATABLE_TYPE); +Q_DECLARE_TYPEINFO(WAYLIB_SERVER_NAMESPACE::WSGBatchRenderer::DrawSet, Q_PRIMITIVE_TYPE); +QT_END_NAMESPACE + +#endif // WSGBATCHRENDERER_P_H diff --git a/waylib/src/server/qtquick/scenegraph/wsgcontext.cpp b/waylib/src/server/qtquick/scenegraph/wsgcontext.cpp new file mode 100644 index 0000000000..bef917fd36 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgcontext.cpp @@ -0,0 +1,73 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgcontext_p.h" +#include "wsgbatchrenderer_p.h" +#include "wayliblogging.h" + +#include +#include +#include +#include "wsgimagenode_p.h" + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WSGRenderContext : public QSGDefaultRenderContext +{ +public: + using QSGDefaultRenderContext::QSGDefaultRenderContext; + + QSGRenderer *createRenderer(QSGRendererInterface::RenderMode renderMode = QSGRendererInterface::RenderMode2D) override + { + return WSGBatchRenderer::Renderer::create(this, renderMode); + } +}; + +class WSGDefaultContext : public QSGDefaultContext +{ +public: + using QSGDefaultContext::QSGDefaultContext; + + QSGRenderContext *createRenderContext() override + { + return new WSGRenderContext(this); + } + + QSGImageNode *createImageNode() override + { + return new WSGImageNode; + } +}; + +void WSGContext::ensureInstalled() +{ + static bool installed = false; + if (installed) + return; + + if (QQuickRenderControlPrivate::sg) { + if (dynamic_cast(QQuickRenderControlPrivate::sg)) { + installed = true; + return; + } + qCCritical(lcWlRenderer) << "QQuickRenderControlPrivate::sg already exists and is not WSGContext"; + qFatal("WSGContext must be installed before any QQuickRenderControl is constructed"); + } + + QSGContext *sg = QSGContext::createDefaultContext(); + // QSGDefaultContext has no Q_OBJECT, so qobject_cast cannot identify it. + if (dynamic_cast(sg)) { + delete sg; + sg = new WSGDefaultContext; + qCDebug(lcWlRenderer) << "Installed WSGContext as QQuickRenderControl scene graph factory"; + } else { + qCDebug(lcWlRenderer) << "Skip WSGContext; using Qt scene graph adaptation"; + } + + // Qt only registers this when it creates sg itself. + qAddPostRoutine(QQuickRenderControlPrivate::cleanup); + QQuickRenderControlPrivate::sg = sg; + installed = true; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgcontext_p.h b/waylib/src/server/qtquick/scenegraph/wsgcontext_p.h new file mode 100644 index 0000000000..98cb9d0790 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgcontext_p.h @@ -0,0 +1,20 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WSGContext +{ +public: + // Must run before any QQuickRenderControl is constructed, including the + // temporary one in WRenderHelper::getGraphicsApi(). If Qt's default + // context is QSGDefaultContext (RHI), replace it with ours; otherwise + // keep the adaptation Qt created (software, etc.). + static void ensureInstalled(); +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgdamagelog.cpp b/waylib/src/server/qtquick/scenegraph/wsgdamagelog.cpp new file mode 100644 index 0000000000..f31d438ef1 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgdamagelog.cpp @@ -0,0 +1,157 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgdamagelog_p.h" +#include "wayliblogging.h" + +#include + +namespace WSGDamageLog { + +namespace { + +thread_local quint64 t_damageFrame = 0; +thread_local bool t_outputOpen = false; +thread_local int t_nestedDepth = 0; +thread_local bool t_innerOpen = false; +thread_local quint64 t_frameSeq = 0; + +bool damageLogEnabled() +{ + return Q_UNLIKELY(lcWlDamage().isDebugEnabled()); +} + +} // namespace + +QString describe(const QRegion ®ion, bool full) +{ + if (full) + return QStringLiteral("full"); + QStringList parts; + for (const QRect &rect : region) { + parts << QStringLiteral("%1x%2+%3+%4") + .arg(rect.width()) + .arg(rect.height()) + .arg(rect.x()) + .arg(rect.y()); + } + return parts.isEmpty() ? QStringLiteral("empty") : parts.join(QLatin1Char(',')); +} + +QString describe(const Waylib::Server::WPixmanRegion ®ion, bool full) +{ + if (full) + return QStringLiteral("full"); + int count = 0; + const pixman_box32_t *rects = region.rectangles(&count); + if (!rects || count == 0) + return QStringLiteral("empty"); + QStringList parts; + for (int i = 0; i < count; ++i) { + parts << QStringLiteral("%1x%2+%3+%4") + .arg(rects[i].x2 - rects[i].x1) + .arg(rects[i].y2 - rects[i].y1) + .arg(rects[i].x1) + .arg(rects[i].y1); + } + return parts.join(QLatin1Char(',')); +} + +QString frameTag() +{ + if (t_damageFrame == 0) + return QStringLiteral("[f-]"); + return QStringLiteral("[f%1]").arg(t_damageFrame); +} + +void beginOutputFrame(const char *why) +{ + if (!damageLogEnabled()) + return; + if (t_outputOpen) + return; + t_damageFrame = ++t_frameSeq; + t_outputOpen = true; + qCDebug(lcWlDamage) << "========" << frameTag() + << "BEGIN compositor output frame:" << why << "========"; +} + +void beginOutputPrepare() +{ + if (!damageLogEnabled()) + return; + beginOutputFrame("compositor prepare (no extra scene dirty this sync)"); + qCDebug(lcWlDamage) << frameTag() + << "---- PREPARE begin: walk blitters, decide recapture, build scissors ----"; +} + +void endOutputPrepare(const QString &flush) +{ + if (!damageLogEnabled() || !t_outputOpen) + return; + qCDebug(lcWlDamage) << frameTag() + << "---- PREPARE end, flush to damage ring:" << flush << "----"; +} + +void beginOutputDraw() +{ + if (!damageLogEnabled() || !t_outputOpen) + return; + qCDebug(lcWlDamage) << frameTag() + << "---- DRAW begin: scene batches + blitter copy/composite ----"; +} + +void endOutputFrame(const char *why) +{ + if (!damageLogEnabled() || !t_outputOpen) + return; + qCDebug(lcWlDamage) << "========" << frameTag() + << "END compositor output frame:" << why << "========"; + t_outputOpen = false; +} + +void beginNestedPass() +{ + if (!damageLogEnabled()) + return; + if (++t_nestedDepth != 1) + return; + qCDebug(lcWlDamage) << frameTag() + << "---- nested MultiEffect/QSGLayer begin (not compositor output; do not recapture blitters) ----"; +} + +void endNestedPass() +{ + if (!damageLogEnabled()) + return; + if (t_nestedDepth == 0) + return; + if (--t_nestedDepth != 0) + return; + qCDebug(lcWlDamage) << frameTag() + << "---- nested MultiEffect/QSGLayer end ----"; +} + +void beginInnerPass() +{ + if (!damageLogEnabled()) + return; + if (t_innerOpen) + return; + t_innerOpen = true; + qCDebug(lcWlDamage) << frameTag() + << "---- inner extra-QRhi begin (copy compositor pixels into this blitter; damage tracking off) ----"; +} + +void endInnerPass() +{ + if (!damageLogEnabled()) + return; + if (!t_innerOpen) + return; + t_innerOpen = false; + qCDebug(lcWlDamage) << frameTag() + << "---- inner extra-QRhi end ----"; +} + +} // namespace WSGDamageLog diff --git a/waylib/src/server/qtquick/scenegraph/wsgdamagelog_p.h b/waylib/src/server/qtquick/scenegraph/wsgdamagelog_p.h new file mode 100644 index 0000000000..1cb5ba7be7 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgdamagelog_p.h @@ -0,0 +1,29 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +#include "wpixmanregion.h" + +// Text helpers for QT_LOGGING_RULES=waylib.renderer.damage.debug=true. +// Not a damage implementation. + +namespace WSGDamageLog { + +QString describe(const QRegion ®ion, bool full); +QString describe(const Waylib::Server::WPixmanRegion ®ion, bool full); +QString frameTag(); +void beginOutputFrame(const char *why); +void beginOutputPrepare(); +void endOutputPrepare(const QString &flush); +void beginOutputDraw(); +void endOutputFrame(const char *why); +void beginNestedPass(); +void endNestedPass(); +void beginInnerPass(); +void endInnerPass(); + +} // namespace WSGDamageLog diff --git a/waylib/src/server/qtquick/scenegraph/wsgdamagenode.cpp b/waylib/src/server/qtquick/scenegraph/wsgdamagenode.cpp new file mode 100644 index 0000000000..8bc68ed04e --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgdamagenode.cpp @@ -0,0 +1,881 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgdamagenode_p.h" +#include "wsgdamagetracker_p.h" +#include "wsgviewport_p.h" + +#include +#include +#include +#include +#include +WAYLIB_SERVER_BEGIN_NAMESPACE + +quint64 WSGDamageNode::s_nextId = 1; + +static QMatrix4x4 contentToWorld(const QMatrix4x4 &world, const QRectF &bounds) +{ + if (Q_LIKELY(qFuzzyIsNull(bounds.x()) && qFuzzyIsNull(bounds.y()))) + return world; + QMatrix4x4 offset; + offset.translate(float(bounds.x()), float(bounds.y())); + // Column-vector convention: world * offset applies the content offset + // first (content-local -> item-local), then the world matrix. The old + // QTransform row-vector code wrote this as offset * world. + return world * offset; +} + +// True when matrix is a pure 2D affine map, i.e. exactly what a QTransform +// can represent. Only then is QTransform-based (inner) mapping faithful. +static bool is2DAffine(const QMatrix4x4 &matrix) +{ + const float *m = matrix.constData(); + return qFuzzyIsNull(m[2]) && qFuzzyIsNull(m[6]) + && qFuzzyIsNull(m[8]) && qFuzzyIsNull(m[9]) + && qFuzzyCompare(m[10], 1.0f) && qFuzzyIsNull(m[11]) + && qFuzzyIsNull(m[3]) && qFuzzyIsNull(m[7]) + && qFuzzyIsNull(m[14]) && qFuzzyCompare(m[15], 1.0f); +} + + +// Inner mapping through a world matrix. Only 2D axis-aligned transforms can +// guarantee pixel coverage; 3D or sheared transforms conservatively cover +// nothing (damage still uses the outer bound above). +static QRect mapInner(const QMatrix4x4 &matrix, const QRectF &local) +{ + if (Q_UNLIKELY(!local.isValid() || local.width() <= 0.0 || local.height() <= 0.0)) + return { }; + if (!is2DAffine(matrix)) + return { }; + return mapInner(matrix.toTransform(), local); +} + +static WPixmanRegion mapRegionOuter(const QMatrix4x4 &matrix, const WPixmanRegion ®ion) +{ + if (region.isEmpty()) + return { }; + if (matrix.isIdentity()) + return region; + if (is2DAffine(matrix)) + return region.mappedOuter(matrix.toTransform()); + int count = 0; + const pixman_box32_t *boxes = region.rectangles(&count); + WPixmanRegion mapped; + for (int i = 0; i < count; ++i) { + mapped += mapOuter(matrix, QRectF(boxes[i].x1, boxes[i].y1, + boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1)); + } + return mapped; +} + +static WPixmanRegion mapRegionInner(const QMatrix4x4 &matrix, const WPixmanRegion ®ion) +{ + if (region.isEmpty()) + return { }; + if (matrix.isIdentity()) + return region; + if (!is2DAffine(matrix)) + return { }; + return region.mappedInner(matrix.toTransform()); +} + +static QRect expandedSampleBounds(const WSGDamageBackdropNode *backdrop) +{ + const QRectF local = backdrop->boundingRect().marginsAdded(backdrop->recopyExpansion()); + return mapOuter(backdrop->worldTransform().toTransform(), local); +} + +static WPixmanRegion expandedBackdropDamage(const WSGDamageBackdropNode *backdrop, + const WPixmanRegion &source, + const QRect &sample) +{ + const QMargins &expansion = backdrop->recopyExpansion(); + if (expansion.isNull()) + return source; + + // Expansion is item-local, not scene pixels. Invert the projected local + // plane rather than the 4D matrix: this also handles nested 3D transforms. + const QTransform world = backdrop->worldTransform().toTransform(); + bool invertible = false; + const QTransform local = world.inverted(&invertible); + if (!invertible) + return WPixmanRegion(sample); + WPixmanRegion expanded = dilateRegion(source.mappedOuter(local), expansion).mappedOuter(world); + expanded &= sample; + return expanded; +} + +WSGDamageNode::WSGDamageNode() + : WSGDamageNode(Type::Basic) +{ +} + +WSGDamageNode::WSGDamageNode(Type type) + : m_type(type) + , m_id(s_nextId++) +{ +} + +WSGDamageNode::~WSGDamageNode() +{ + if (m_parent) + m_parent->removeChild(this); + + while (m_firstChild) { + WSGDamageNode *child = m_firstChild; + unlink(child); + delete child; + } +} + +WSGDamageTransformNode *WSGDamageNode::toTransform() +{ + return m_type == Type::Transform ? static_cast(this) : nullptr; +} + +WSGDamageClipNode *WSGDamageNode::toClip() +{ + return m_type == Type::Clip ? static_cast(this) : nullptr; +} + +WSGDamageGeometryNode *WSGDamageNode::toGeometry() +{ + return m_type == Type::Geometry ? static_cast(this) : nullptr; +} + +const WSGDamageTransformNode *WSGDamageNode::toTransform() const +{ + return m_type == Type::Transform ? static_cast(this) : nullptr; +} + +const WSGDamageClipNode *WSGDamageNode::toClip() const +{ + return m_type == Type::Clip ? static_cast(this) : nullptr; +} + +const WSGDamageGeometryNode *WSGDamageNode::toGeometry() const +{ + return m_type == Type::Geometry ? static_cast(this) : nullptr; +} + +WSGDamageBackdropNode *WSGDamageNode::toBackdrop() +{ + return m_type == Type::Geometry ? dynamic_cast(this) : nullptr; +} + +const WSGDamageBackdropNode *WSGDamageNode::toBackdrop() const +{ + return m_type == Type::Geometry ? dynamic_cast(this) : nullptr; +} + +void WSGDamageNode::setVisible(bool visible) +{ + if (m_visible == visible) + return; + m_visible = visible; + markDirty(DirtyVisibility); +} + +void WSGDamageNode::setNeedsBackdrop(bool needsBackdrop) +{ + if (m_needsBackdrop == needsBackdrop) + return; + const int delta = needsBackdrop ? 1 : -1; + for (WSGDamageNode *node = this; node; node = node->m_parent) + node->m_subtreeBackdropCount += delta; + m_needsBackdrop = needsBackdrop; + markDirty(DirtyOpaque); +} + +void WSGDamageNode::setHasContent(bool hasContent) +{ + if (m_type != Type::Geometry) { + Q_ASSERT_X(!hasContent, + "WSGDamageNode::setHasContent", + "only geometry nodes can have content"); + return; + } + if (m_hasContent == hasContent) + return; + if (!hasContent && !m_committedWorldBounds.isEmpty()) + m_pendingRemovedDamage += m_committedWorldBounds; + m_hasContent = hasContent; + markDirty(DirtyGeometry); +} + +void WSGDamageNode::markDirty(DirtyBits bits) +{ + constexpr DirtyBits geometryBits = DirtyMatrix | DirtyGeometry | DirtyAdded | DirtyStructure + | DirtyVisibility | DirtyOpaque | DirtySubtreeGeometry; + const bool geometryDirty = bits & geometryBits; + + m_dirty |= bits; + for (WSGDamageNode *p = m_parent; p; p = p->m_parent) { + const bool alreadyDirty = p->m_dirty & DirtySubtree; + const bool alreadyGeometryDirty = !geometryDirty || (p->m_dirty & DirtySubtreeGeometry); + p->m_dirty |= DirtySubtree; + if (geometryDirty) + p->m_dirty |= DirtySubtreeGeometry; + if (alreadyDirty && alreadyGeometryDirty) + break; + } +} + +void WSGDamageNode::adopt(WSGDamageNode *child) +{ + if (child->m_parent) + child->m_parent->removeChild(child); +} + +void WSGDamageNode::attach(WSGDamageNode *child, WSGDamageNode *prev, WSGDamageNode *next) +{ + Q_ASSERT(!child->m_parent); + for (WSGDamageNode *ancestor = this; ancestor; ancestor = ancestor->m_parent) + Q_ASSERT(ancestor != child); + child->m_parent = this; + child->m_prev = prev; + child->m_next = next; + if (prev) + prev->m_next = child; + else + m_firstChild = child; + if (next) + next->m_prev = child; + else + m_lastChild = child; + ++m_childCount; + for (WSGDamageNode *p = this; p; p = p->m_parent) { + p->m_subtreeBackdropCount += child->m_subtreeBackdropCount; + if (!child->m_subtreeVisibilityEmpty) + p->m_subtreeVisibilityEmpty = false; + } + child->markDirty(DirtyAdded); + markDirty(DirtyStructure); +} + +void WSGDamageNode::unlink(WSGDamageNode *child) +{ + if (child->m_prev) + child->m_prev->m_next = child->m_next; + else + m_firstChild = child->m_next; + if (child->m_next) + child->m_next->m_prev = child->m_prev; + else + m_lastChild = child->m_prev; + child->m_parent = nullptr; + child->m_prev = nullptr; + child->m_next = nullptr; + --m_childCount; + for (WSGDamageNode *p = this; p; p = p->m_parent) { + p->m_subtreeBackdropCount -= child->m_subtreeBackdropCount; + } +} + +void WSGDamageNode::prependChild(WSGDamageNode *child) +{ + Q_ASSERT(child); + Q_ASSERT(child != this); + adopt(child); + attach(child, nullptr, m_firstChild); +} + +void WSGDamageNode::appendChild(WSGDamageNode *child) +{ + Q_ASSERT(child); + Q_ASSERT(child != this); + adopt(child); + attach(child, m_lastChild, nullptr); +} + +void WSGDamageNode::insertChildBefore(WSGDamageNode *child, WSGDamageNode *before) +{ + Q_ASSERT(child); + Q_ASSERT(child != this); + if (!before) { + appendChild(child); + return; + } + Q_ASSERT(before->m_parent == this); + adopt(child); + attach(child, before->m_prev, before); +} + +void WSGDamageNode::insertChildAfter(WSGDamageNode *child, WSGDamageNode *after) +{ + Q_ASSERT(child); + Q_ASSERT(child != this); + if (!after) { + prependChild(child); + return; + } + Q_ASSERT(after->m_parent == this); + adopt(child); + attach(child, after, after->m_next); +} + +void WSGDamageNode::removeChild(WSGDamageNode *child) +{ + Q_ASSERT(child); + Q_ASSERT(child->m_parent == this); + m_pendingRemovedDamage += child->m_committedSubtreeAABB; + unlink(child); + markDirty(DirtyStructure); +} + +void WSGDamageNode::removeAllChildren() +{ + while (m_firstChild) + removeChild(m_firstChild); +} + +void WSGDamageNode::updateWorld(const QMatrix4x4 &parentWorld, + bool parentWorldChanged, + WPixmanRegion &worldDamage, + WPixmanRegion &backdropDamage, + const QRect *clipOuter, + const WPixmanRegion *clipInner, + WSGDamageTracker *tracker) +{ + m_commitPending = true; + m_dirty |= m_deferredDirty; + m_deferredDirty = { }; + m_deferDirtyWhileInactive = false; + m_ownDamage = m_pendingRemovedDamage; + m_pendingRemovedDamage = { }; + + constexpr DirtyBits localGeometryBits = + DirtyMatrix | DirtyGeometry | DirtyAdded | DirtyVisibility | DirtyOpaque; + const bool matrixChanged = + parentWorldChanged || (m_type == Type::Transform && (m_dirty & DirtyMatrix)); + const bool localGeometryDirty = matrixChanged || (m_dirty & localGeometryBits); + const bool subtreeGeometryDirty = + localGeometryDirty || (m_dirty & (DirtyStructure | DirtySubtreeGeometry)); + const bool contentDirty = m_dirty & DirtyContent; + + if (localGeometryDirty) { + if (m_type == Type::Transform) + m_worldTransform = parentWorld * static_cast(this)->m_matrix; + else + m_worldTransform = parentWorld; + } + + // Once a clipped or hidden node has committed as inactive, content-only + // changes cannot make it visible. Defer them until visibility or geometry + // changes invalidate the cached inactive state. + if (Q_UNLIKELY(!m_visible || (clipOuter && !m_committedVisible && !subtreeGeometryDirty))) { + deactivateWorld(worldDamage); + return; + } + + const QRect *childClipOuter = clipOuter; + const WPixmanRegion *childClipInner = clipInner; + QRect clipOuterStorage; + WPixmanRegion clipInnerStorage; + bool clipChanged = false; + if (m_type == Type::Clip) { + auto *clip = static_cast(this); + clipOuterStorage = mapOuter(m_worldTransform, clip->m_clipRect); + if (clipOuter) + clipOuterStorage = clipOuterStorage.intersected(*clipOuter); + if (clip->m_radius > 0) { + clipInnerStorage = mapRegionInner(m_worldTransform, + roundedRectInnerRegion(clip->m_clipRect, clip->m_radius)); + } else if (clip->m_rectangular) { + clipInnerStorage = WPixmanRegion(mapInner(m_worldTransform, clip->m_clipRect)); + } else { + clipInnerStorage = { }; + } + if (clipInner) + clipInnerStorage &= *clipInner; + else if (clipOuter) + clipInnerStorage &= *clipOuter; + childClipOuter = &clipOuterStorage; + childClipInner = &clipInnerStorage; + clipChanged = matrixChanged || (m_dirty & (DirtyGeometry | DirtyAdded | DirtyVisibility)); + } + + if (Q_UNLIKELY(childClipOuter && childClipOuter->isEmpty() && !hasContent())) { + deactivateWorld(worldDamage); + return; + } + + m_effectiveVisible = true; + const bool appearing = !m_committedVisible || (m_dirty & DirtyAdded); + if (hasContent()) { + auto *geo = static_cast(this); + QMatrix4x4 c2w; + if (localGeometryDirty) { + if (geo->m_fullyOpaque) + geo->syncFullyOpaqueRegion(); + + m_worldBounds = mapOuter(m_worldTransform, geo->m_boundingRect); + if (clipOuter) + m_worldBounds = m_worldBounds.intersected(*clipOuter); + c2w = contentToWorld(m_worldTransform, geo->m_boundingRect); + m_worldOpaque = mapRegionInner(c2w, geo->m_opaqueRegion); + if (clipInner) + m_worldOpaque &= *clipInner; + else if (clipOuter) + m_worldOpaque &= *clipOuter; + + if (appearing) { + m_ownDamage += m_worldBounds; + } else { + const bool visibilityFlipped = m_effectiveVisible != m_committedVisible; + const bool shapeChanged = + matrixChanged || (m_dirty & (DirtyGeometry | DirtyOpaque)) || visibilityFlipped; + if (shapeChanged) { + m_ownDamage += m_committedWorldBounds; + m_ownDamage += m_worldBounds; + } + } + } + if (!appearing && contentDirty) { + if (!localGeometryDirty) + c2w = contentToWorld(m_worldTransform, geo->m_boundingRect); + WPixmanRegion mapped = mapRegionOuter(c2w, geo->m_pendingContentDamage); + mapped &= m_worldBounds; + m_ownDamage += mapped; + } + if (localGeometryDirty || contentDirty) + geo->m_pendingContentDamage = { }; + } else if (localGeometryDirty) { + m_worldBounds = { }; + m_worldOpaque = { }; + if (auto *geo = toGeometry()) + geo->m_pendingContentDamage = { }; + } + + m_behindDamage = worldDamage; + WPixmanRegion extra; + if (m_needsBackdrop && hasContent()) { + if (auto *backdrop = toBackdrop()) { + QRect sample = expandedSampleBounds(backdrop); + if (sample.isEmpty()) + sample = m_worldBounds; + WPixmanRegion source(sample); + source &= m_behindDamage; + // Newly sampled pixels need recapture even when the background + // itself did not change. Pending debt survives until consumed by + // each output, independently of this frame's damage. + const QRect bounds = m_worldBounds; + if (appearing || (!bounds.isEmpty() && bounds != m_committedWorldBounds)) { + WPixmanRegion fresh(sample); + if (!appearing && !m_committedWorldBounds.isEmpty()) + fresh -= m_committedWorldBounds; + source += fresh; + } + if (!source.isEmpty()) { + backdrop->accumulateRecopy(source.native(), tracker); + extra = expandedBackdropDamage(backdrop, source, sample); + backdropDamage += source; + backdropDamage += extra; + } + } else if (!m_worldBounds.isEmpty()) { + extra = m_behindDamage & m_worldBounds; + backdropDamage += extra; + } + } + + if (hasContent() && !m_worldOpaque.isEmpty()) + worldDamage -= m_worldOpaque; + // A node's own pixels and grouping-node removal holes are behind its + // children, so a backdrop child samples them before later siblings. + worldDamage += m_ownDamage; + worldDamage += extra; + + QRect subtree = subtreeGeometryDirty && hasContent() ? m_worldBounds : QRect(); + const bool rebuildOpaqueCache = subtreeGeometryDirty && m_parent && m_subtreeBackdropCount == 0; + WPixmanRegion subtreeOpaque; + if (rebuildOpaqueCache && hasContent()) + subtreeOpaque += m_worldOpaque; + + const bool childWorldChanged = matrixChanged || appearing || clipChanged; + for (WSGDamageNode *child = m_firstChild; child; child = child->m_next) { + const bool skipChild = !childWorldChanged && !child->isDirty() + && (!child->m_effectiveVisible || child->m_subtreeBackdropCount == 0); + if (skipChild) { + child->m_ownDamage = { }; + child->m_behindDamage = worldDamage; + if (child->m_effectiveVisible) + worldDamage -= child->m_subtreeWorldOpaque; + } else { + child->updateWorld(m_worldTransform, + childWorldChanged, + worldDamage, + backdropDamage, + childClipOuter, + childClipInner, + tracker); + } + subtree = subtree.united(child->m_subtreeAABB); + if (rebuildOpaqueCache) + subtreeOpaque += child->m_subtreeWorldOpaque; + } + // Removed descendants can have been in front of an opaque remaining + // child. Restore their hole after processing this group's children. + if (!hasContent()) + worldDamage += m_ownDamage; + if (subtreeGeometryDirty) { + if (childClipOuter) + subtree = subtree.intersected(*childClipOuter); + m_subtreeAABB = subtree; + if (rebuildOpaqueCache) + m_subtreeWorldOpaque = subtreeOpaque; + else + m_subtreeWorldOpaque = { }; + } + + // A non-empty clip can still exclude an entire subtree. Cache that fact + // so later content changes stop at this node until geometry or clip state + // changes. + if (clipOuter && m_subtreeAABB.isEmpty()) { + m_effectiveVisible = false; + m_subtreeWorldOpaque = { }; + } +} + +void WSGDamageNode::deactivateWorld(WPixmanRegion &worldDamage) +{ + m_effectiveVisible = false; + m_deferDirtyWhileInactive = true; + if (m_committedVisible) + m_ownDamage += m_committedSubtreeAABB; + m_worldBounds = { }; + m_subtreeAABB = { }; + m_worldOpaque = { }; + m_subtreeWorldOpaque = { }; + m_behindDamage = worldDamage; + worldDamage += m_ownDamage; +} + +void WSGDamageNode::clearBehindDamageRecursive() +{ + m_behindDamage = { }; + for (WSGDamageNode *child = m_firstChild; child; child = child->m_next) + child->clearBehindDamageRecursive(); +} + +void WSGDamageNode::resetWorldVisibleRecursive() +{ + if (m_subtreeVisibilityEmpty) + return; + + m_worldValidRegion = { }; + m_worldFrontOpaque = { }; + m_worldVisibleRegion = { }; + m_worldFrontBackdrop = { }; + for (WSGDamageNode *child = m_firstChild; child; child = child->m_next) + child->resetWorldVisibleRecursive(); + m_subtreeVisibilityEmpty = true; +} + +void WSGDamageNode::computeWorldVisibility(WPixmanRegion &worldFrontOpaque, + WPixmanRegion &worldFrontBackdrop) +{ + if (Q_UNLIKELY(!m_effectiveVisible)) { + resetWorldVisibleRecursive(); + commitState(); + return; + } + + constexpr int kLocalizeRegionRectThreshold = 16; + const bool localize = m_parent && m_firstChild && !m_subtreeAABB.isEmpty() + && worldFrontOpaque.rectCount() + worldFrontBackdrop.rectCount() + > kLocalizeRegionRectThreshold; + if (!localize) { + computeWorldVisibilityImpl(worldFrontOpaque, worldFrontBackdrop); + return; + } + + // A subtree can only change front coverage inside its AABB. Restricting + // the working regions prevents every descendant from scanning unrelated + // rectangles accumulated by spatially distant front siblings. + WPixmanRegion localFrontOpaque; + localFrontOpaque.setIntersection(worldFrontOpaque.native(), m_subtreeAABB); + WPixmanRegion localFrontBackdrop; + localFrontBackdrop.setIntersection(worldFrontBackdrop.native(), m_subtreeAABB); + computeWorldVisibilityImpl(localFrontOpaque, localFrontBackdrop); + + worldFrontOpaque -= m_subtreeAABB; + worldFrontOpaque += localFrontOpaque; + worldFrontBackdrop -= m_subtreeAABB; + worldFrontBackdrop += localFrontBackdrop; +} + +void WSGDamageNode::computeWorldVisibilityImpl(WPixmanRegion &worldFrontOpaque, + WPixmanRegion &worldFrontBackdrop) +{ + m_subtreeVisibilityEmpty = false; + + for (WSGDamageNode *child = m_lastChild; child; child = child->m_prev) + child->computeWorldVisibility(worldFrontOpaque, worldFrontBackdrop); + + if (!hasContent()) { + m_worldFrontOpaque = { }; + m_worldValidRegion = { }; + m_worldFrontBackdrop = { }; + m_worldVisibleRegion = { }; + commitState(); + return; + } + + m_worldFrontOpaque.setIntersection(worldFrontOpaque.native(), m_worldBounds); + m_worldValidRegion = WPixmanRegion(m_worldBounds) - m_worldFrontOpaque; + m_worldFrontBackdrop.setIntersection(worldFrontBackdrop.native(), m_worldBounds); + m_worldVisibleRegion = WPixmanRegion(m_worldBounds) - m_worldFrontBackdrop; + + if (m_needsBackdrop) { + if (!m_worldBounds.isEmpty()) { + worldFrontOpaque -= m_worldBounds; + worldFrontBackdrop += m_worldBounds; + } + commitState(); + return; + } + + if (Q_UNLIKELY(!m_worldOpaque.isEmpty())) + worldFrontOpaque += m_worldOpaque; + commitState(); +} + +void WSGDamageNode::invalidateCommittedStateRecursive() +{ + m_committedWorldBounds = { }; + m_committedSubtreeAABB = { }; + m_worldBounds = { }; + m_subtreeAABB = { }; + m_worldOpaque = { }; + m_committedVisible = false; + m_effectiveVisible = false; + m_subtreeWorldOpaque = { }; + m_commitPending = false; + for (WSGDamageNode *child = m_firstChild; child; child = child->m_next) + child->invalidateCommittedStateRecursive(); +} + +void WSGDamageNode::commitState() +{ + if (!m_effectiveVisible && m_committedVisible) { + for (WSGDamageNode *child = m_firstChild; child; child = child->m_next) + child->invalidateCommittedStateRecursive(); + } + + if (m_effectiveVisible) { + m_committedWorldBounds = hasContent() ? m_worldBounds : QRect(); + m_committedSubtreeAABB = m_subtreeAABB; + } else { + m_committedWorldBounds = { }; + m_committedSubtreeAABB = { }; + if (m_deferDirtyWhileInactive) { + constexpr DirtyBits deferredBits = DirtyMatrix | DirtyGeometry | DirtyContent + | DirtyAdded | DirtyStructure | DirtyOpaque | DirtySubtree | DirtySubtreeGeometry; + m_deferredDirty |= m_dirty & deferredBits; + } + } + m_commitPending = false; + m_committedVisible = m_effectiveVisible; + m_deferDirtyWhileInactive = false; + m_dirty = { }; +} + +void WSGDamageNode::commitStateRecursive() +{ + if (m_effectiveVisible) { + for (WSGDamageNode *child = m_firstChild; child; child = child->m_next) { + if (child->m_commitPending) + child->commitStateRecursive(); + } + } + commitState(); +} + +WSGDamageTransformNode::WSGDamageTransformNode() + : WSGDamageNode(Type::Transform) +{ +} + +void WSGDamageTransformNode::setMatrix(const QTransform &matrix) +{ + setMatrix(QMatrix4x4(matrix)); +} + +void WSGDamageTransformNode::setMatrix(const QMatrix4x4 &matrix) +{ + if (m_matrix == matrix) + return; + m_matrix = matrix; + markDirty(DirtyMatrix); +} + +void WSGDamageTransformNode::setTranslation(qreal x, qreal y) +{ + QTransform t; + t.translate(x, y); + setMatrix(t); +} + +void WSGDamageTransformNode::setScale(qreal sx, qreal sy) +{ + QTransform t; + t.scale(sx, sy); + setMatrix(t); +} + +void WSGDamageTransformNode::setRotation(qreal degrees, Qt::Axis axis) +{ + QTransform t; + t.rotate(degrees, axis); + setMatrix(t); +} + +WSGDamageClipNode::WSGDamageClipNode() + : WSGDamageNode(Type::Clip) +{ +} + +void WSGDamageClipNode::setClipRect(const QRectF &rect) +{ + if (m_clipRect == rect) + return; + m_clipRect = rect; + markDirty(DirtyGeometry); +} + +void WSGDamageClipNode::setIsRectangular(bool rectangular) +{ + if (m_rectangular == rectangular) + return; + m_rectangular = rectangular; + markDirty(DirtyGeometry); +} + +void WSGDamageClipNode::setRadius(qreal radius) +{ + if (qFuzzyIsNull(m_radius - radius)) + return; + m_radius = radius; + markDirty(DirtyGeometry); +} + +WSGDamageGeometryNode::WSGDamageGeometryNode() + : WSGDamageNode(Type::Geometry) +{ + m_hasContent = true; +} + +WSGDamageGeometryNode::WSGDamageGeometryNode(Type type) + : WSGDamageNode(type) +{ + m_hasContent = true; +} + +void WSGDamageGeometryNode::syncFullyOpaqueRegion() +{ + m_opaqueRegion = + WPixmanRegion(innerAligned(QRectF(0, 0, m_boundingRect.width(), m_boundingRect.height()))); +} + +void WSGDamageGeometryNode::setBoundingRect(const QRectF &rect) +{ + if (m_boundingRect == rect) + return; + m_boundingRect = rect; + if (m_fullyOpaque) + syncFullyOpaqueRegion(); + markDirty(DirtyGeometry); +} + +void WSGDamageGeometryNode::setOpaqueRegion(const pixman_region32_t *localOpaque) +{ + const WPixmanRegion region(localOpaque); + if (m_opaqueRegion == region && !m_fullyOpaque) + return; + m_fullyOpaque = false; + m_opaqueRegion = region; + markDirty(DirtyOpaque); +} + +void WSGDamageGeometryNode::setFullyOpaque(bool fullyOpaque) +{ + if (m_fullyOpaque == fullyOpaque && fullyOpaque) + return; + if (!fullyOpaque && !m_fullyOpaque && m_opaqueRegion.isEmpty()) + return; + m_fullyOpaque = fullyOpaque; + if (fullyOpaque) + syncFullyOpaqueRegion(); + else + m_opaqueRegion = { }; + markDirty(DirtyOpaque); +} + +void WSGDamageGeometryNode::markContentDirty(const pixman_region32_t *localRegion) +{ + const WPixmanRegion region(localRegion); + if (region.isEmpty()) + return; + m_pendingContentDamage += region; + markDirty(DirtyContent); +} + +void WSGDamageGeometryNode::markContentDirty(const QRect &localRect) +{ + if (localRect.isEmpty()) + return; + m_pendingContentDamage += localRect; + markDirty(DirtyContent); +} + +void WSGDamageGeometryNode::setOpaqueRegion(const QRegion &localOpaque) +{ + setOpaqueRegion(WPixmanRegion::fromQRegion(localOpaque).native()); +} + +void WSGDamageGeometryNode::markContentDirty(const QRegion &localRegion) +{ + if (localRegion.isEmpty()) + return; + markContentDirty(WPixmanRegion::fromQRegion(localRegion).native()); +} + +WSGDamageBackdropNode::WSGDamageBackdropNode() + : WSGDamageGeometryNode(Type::Geometry) +{ + setNeedsBackdrop(true); +} + +WSGDamageBackdropNode::~WSGDamageBackdropNode() = default; + +void WSGDamageBackdropNode::accumulateRecopy(const pixman_region32_t *damage, WSGDamageTracker *tracker) +{ + m_recopySeed += damage; + if (tracker) + tracker->accumulateBackdropRecopy(this, damage); +} + +const WPixmanRegion &WSGDamageBackdropNode::pendingRecopy(WSGViewport *viewport) const +{ + if (!viewport) { + static const WPixmanRegion empty; + return empty; + } + auto *data = viewport->getAttachedData(); + if (!data) + return m_recopySeed; + return data->pending(this); +} + +void WSGDamageBackdropNode::consumeRecopy(const WPixmanRegion &applied, WSGViewport *viewport) +{ + if (applied.isEmpty() || !viewport) + return; + auto *data = viewport->getAttachedData(); + if (!data) + return; + data->consume(this, applied); +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgdamagenode_p.h b/waylib/src/server/qtquick/scenegraph/wsgdamagenode_p.h new file mode 100644 index 0000000000..799ed87d59 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgdamagenode_p.h @@ -0,0 +1,496 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include "wpixmanregion.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WSGDamageNode; +class WSGDamageTracker; +class WSGDamageTransformNode; +class WSGDamageClipNode; +class WSGDamageGeometryNode; +class WSGDamageBackdropNode; +class WSGViewport; +// Scene-graph node used for GUI damage precomputation. +// +// Tree shape matches QSG: first child is behind later siblings; a node's own +// geometry (if any) is behind its descendants. +// +// Parent owns children. Removing a child does not delete it. Destroying a +// parent deletes remaining children. Not thread-safe. +class WAYLIB_SERVER_EXPORT WSGDamageNode +{ +public: + enum class Type + { + Basic = 0, + Transform, + Clip, + Geometry, + }; + + enum DirtyBit + { + DirtyMatrix = 1 << 0, + DirtyGeometry = 1 << 1, + DirtyContent = 1 << 2, + DirtyAdded = 1 << 3, + DirtyStructure = 1 << 4, + DirtyVisibility = 1 << 5, + DirtyOpaque = 1 << 6, + DirtySubtree = 1 << 7, + DirtySubtreeGeometry = 1 << 8 + }; + Q_DECLARE_FLAGS(DirtyBits, DirtyBit) + + explicit WSGDamageNode(); + virtual ~WSGDamageNode(); + + WSGDamageNode(const WSGDamageNode &) = delete; + WSGDamageNode &operator=(const WSGDamageNode &) = delete; + + Type type() const + { + return m_type; + } + + bool hasContent() const + { + return m_hasContent; + } + + void setHasContent(bool hasContent); + + WSGDamageTransformNode *toTransform(); + WSGDamageClipNode *toClip(); + WSGDamageGeometryNode *toGeometry(); + const WSGDamageTransformNode *toTransform() const; + const WSGDamageClipNode *toClip() const; + const WSGDamageGeometryNode *toGeometry() const; + WSGDamageBackdropNode *toBackdrop(); + const WSGDamageBackdropNode *toBackdrop() const; + + void setName(const QString &name) + { + m_name = name; + } + + QString name() const + { + return m_name; + } + + quint64 id() const + { + return m_id; + } + + bool isVisible() const + { + return m_visible; + } + + void setVisible(bool visible); + void setNeedsBackdrop(bool needsBackdrop); + + bool needsBackdrop() const + { + return m_needsBackdrop; + } + + WSGDamageNode *parent() const + { + return m_parent; + } + + WSGDamageNode *firstChild() const + { + return m_firstChild; + } + + WSGDamageNode *lastChild() const + { + return m_lastChild; + } + + WSGDamageNode *nextSibling() const + { + return m_next; + } + + WSGDamageNode *previousSibling() const + { + return m_prev; + } + + int childCount() const + { + return m_childCount; + } + + void prependChild(WSGDamageNode *child); + void appendChild(WSGDamageNode *child); + void insertChildBefore(WSGDamageNode *child, WSGDamageNode *before); + void insertChildAfter(WSGDamageNode *child, WSGDamageNode *after); + void removeChild(WSGDamageNode *child); + void removeAllChildren(); // does not delete + + // After WSGDamageTracker::commit(). World fields are viewport-independent. + QMatrix4x4 worldTransform() const + { + return m_worldTransform; + } + + QRect worldBounds() const + { + return m_worldBounds; + } + + QRect subtreeBounds() const + { + return m_subtreeAABB; + } + + const pixman_region32_t *worldOpaqueRegion() const + { + return m_worldOpaque.native(); + } + + // Bounds minus front opaque. Backdrop punches this so sampled-behind + // stays valid for cache; not "on-screen visible". Ancestor clips are + // already applied to worldBounds, so this stays inside the clip. + const pixman_region32_t *worldValidRegion() const + { + return m_worldValidRegion.native(); + } + + // Bounds minus front needsBackdrop coverage. On-screen draw of this node. + // Clean nodes only redraw this; dirty nodes also draw the sampled part. + // Same clip inheritance as worldValidRegion. + const pixman_region32_t *worldVisibleRegion() const + { + return m_worldVisibleRegion.native(); + } + + const pixman_region32_t *behindDamageRegion() const + { + return m_behindDamage.native(); + } + + // Dirty bits from setters, not "content dirty for this viewport frame". + DirtyBits dirty() const + { + return m_dirty; + } + + bool isDirty() const + { + return m_dirty != 0; + } + +protected: + explicit WSGDamageNode(Type type); + void markDirty(DirtyBits bits); + bool m_hasContent = false; + friend class WSGDamageTracker; + friend class WSGDamageNodeTestAccess; + friend class WSGDamageClipNode; + friend class WSGDamageGeometryNode; + void attach(WSGDamageNode *child, WSGDamageNode *prev, WSGDamageNode *next); + void unlink(WSGDamageNode *child); + void adopt(WSGDamageNode *child); + // clipOuter is the world AABB of ancestor clips (damage/bounds; ignores + // radius). clipInner is the world opaque mask: rectangular clips use the + // inner AABB, rounded clips use the AABB minus corner squares. Null means + // unbounded. Empty (non-null) means nothing is visible / opaque. + void updateWorld(const QMatrix4x4 &parentWorld, + bool parentWorldChanged, + WPixmanRegion &worldDamage, + WPixmanRegion &backdropDamage, + const QRect *clipOuter = nullptr, + const WPixmanRegion *clipInner = nullptr, + WSGDamageTracker *tracker = nullptr); + void deactivateWorld(WPixmanRegion &worldDamage); + void clearBehindDamageRecursive(); + void computeWorldVisibility(WPixmanRegion &worldFrontOpaque, WPixmanRegion &worldFrontBackdrop); + void computeWorldVisibilityImpl(WPixmanRegion &worldFrontOpaque, + WPixmanRegion &worldFrontBackdrop); + void resetWorldVisibleRecursive(); + void invalidateCommittedStateRecursive(); + void commitState(); + void commitStateRecursive(); + Type m_type; + quint64 m_id = 0; + QString m_name; + + WSGDamageNode *m_parent = nullptr; + WSGDamageNode *m_firstChild = nullptr; + WSGDamageNode *m_lastChild = nullptr; + WSGDamageNode *m_prev = nullptr; + WSGDamageNode *m_next = nullptr; + int m_childCount = 0; + int m_subtreeBackdropCount = 0; + + bool m_visible = true; + bool m_needsBackdrop = false; + + DirtyBits m_dirty = DirtyAdded; + DirtyBits m_deferredDirty; + + QMatrix4x4 m_worldTransform; + QRect m_worldBounds; + QRect m_subtreeAABB; + WPixmanRegion m_worldOpaque; + WPixmanRegion m_subtreeWorldOpaque; + WPixmanRegion m_ownDamage; + WPixmanRegion m_behindDamage; + WPixmanRegion m_pendingRemovedDamage; + WPixmanRegion m_worldValidRegion; + WPixmanRegion m_worldFrontOpaque; + WPixmanRegion m_worldVisibleRegion; + WPixmanRegion m_worldFrontBackdrop; + QRect m_committedWorldBounds; + QRect m_committedSubtreeAABB; + bool m_committedVisible = false; + bool m_effectiveVisible = false; + bool m_subtreeVisibilityEmpty = true; + bool m_deferDirtyWhileInactive = false; + bool m_commitPending = false; + + static quint64 s_nextId; +}; + +class WAYLIB_SERVER_EXPORT WSGDamageTransformNode : public WSGDamageNode +{ +public: + WSGDamageTransformNode(); + + void setMatrix(const QTransform &matrix); + void setMatrix(const QMatrix4x4 &matrix); + + QMatrix4x4 matrix() const + { + return m_matrix; + } + + void setTranslation(qreal x, qreal y); + void setScale(qreal sx, qreal sy); + void setRotation(qreal degrees, Qt::Axis axis = Qt::ZAxis); + +private: + friend class WSGDamageNode; + friend class WSGDamageTracker; + QMatrix4x4 m_matrix; +}; + +class WAYLIB_SERVER_EXPORT WSGDamageClipNode : public WSGDamageNode +{ +public: + WSGDamageClipNode(); + + // Local clip rectangle. Same meaning as QSGClipNode::clipRect(): the + // scissor when rectangular, otherwise the AABB of the stencil geometry. + // Damage/bounds use this AABB even when radius() > 0. + void setClipRect(const QRectF &rect); + + QRectF clipRect() const + { + return m_clipRect; + } + + // Same as QSGClipNode::isRectangular(). Unknown stencil clips + // (!rectangular && radius == 0) contribute no descendant opaque. + void setIsRectangular(bool rectangular); + + bool isRectangular() const + { + return m_rectangular; + } + + // Corner radius from QQuickDefaultClipNode. Outer clip ignores it; + // opaque is the AABB minus ceil(radius) corner squares. + void setRadius(qreal radius); + + qreal radius() const + { + return m_radius; + } + +private: + friend class WSGDamageNode; + friend class WSGDamageTracker; + QRectF m_clipRect; + qreal m_radius = 0; + bool m_rectangular = true; +}; + +class WAYLIB_SERVER_EXPORT WSGDamageGeometryNode : public WSGDamageNode +{ +public: + WSGDamageGeometryNode(); + + void setBoundingRect(const QRectF &rect); + + QRectF boundingRect() const + { + return m_boundingRect; + } + + // Content-local opaque pixels. Origin is boundingRect top-left. + void setOpaqueRegion(const pixman_region32_t *localOpaque); + + void setOpaqueRegion(const WPixmanRegion &localOpaque) + { + setOpaqueRegion(localOpaque.native()); + } + + const pixman_region32_t *opaqueRegion() const + { + return m_opaqueRegion.native(); + } + + // Marks every pixel inside the (inner-aligned) content box as opaque. + // Recomputed when the bounding rect changes. + void setFullyOpaque(bool fullyOpaque); + + bool isFullyOpaque() const + { + return m_fullyOpaque; + } + + // Content-local dirty pixels. Origin is boundingRect top-left. + // Clipped to the content box on commit. + void setOpaqueRegion(const QRegion &localOpaque); + void markContentDirty(const pixman_region32_t *localRegion); + void markContentDirty(const QRect &localRect); + void markContentDirty(const QRegion &localRegion); + +protected: + explicit WSGDamageGeometryNode(Type type); + +private: + friend class WSGDamageNode; + friend class WSGDamageTracker; + void syncFullyOpaqueRegion(); + + QRectF m_boundingRect; + WPixmanRegion m_opaqueRegion; + WPixmanRegion m_pendingContentDamage; + bool m_fullyOpaque = false; +}; + +// Damage node for glass/blur content backed by a WRenderBufferNode cache. +// Viewport-independent bookkeeping of the node's own cache texture: the +// tracker accumulates the frame damage that invalidates the cached backdrop +// into per-output recopy debt (scene space). One damage tree is shared by +// all outputs of a window, but each output owns its blit cache, so debt is +// stored per output and a renderer consumes only its own slot; unconsumed +// damage persists to later frames. +struct WSGViewportBackdropData +{ + struct DebtEntry { + const WSGDamageBackdropNode *node = nullptr; + WPixmanRegion damage; + }; + QList debts; + + const WPixmanRegion &pending(const WSGDamageBackdropNode *node) const { + for (const auto &entry : debts) { + if (entry.node == node) + return entry.damage; + } + static const WPixmanRegion empty; + return empty; + } + + void accumulate(const WSGDamageBackdropNode *node, const pixman_region32_t *damage) { + for (auto &entry : debts) { + if (entry.node == node) { + entry.damage += damage; + return; + } + } + debts.append({node, WPixmanRegion(damage)}); + } + + void consume(const WSGDamageBackdropNode *node, const WPixmanRegion &applied) { + for (auto &entry : debts) { + if (entry.node == node) { + entry.damage -= applied; + return; + } + } + } +}; + +class WAYLIB_SERVER_EXPORT WSGDamageBackdropNode : public WSGDamageGeometryNode +{ +public: + WSGDamageBackdropNode(); + ~WSGDamageBackdropNode() override; + + const WPixmanRegion &pendingRecopy(WSGViewport *viewport) const; + void accumulateRecopy(const pixman_region32_t *damage, WSGDamageTracker *tracker = nullptr); + void consumeRecopy(const WPixmanRegion &applied, WSGViewport *viewport); + + // The cache samples beyond the drawn bounds by this margin (blur radius + // etc.). Mirrors WRenderBufferNode::damageExpansion(). + void setRecopyExpansion(const QMargins &expansion) + { + m_recopyExpansion = expansion; + } + + const QMargins &recopyExpansion() const + { + return m_recopyExpansion; + } + const WPixmanRegion &recopySeed() const + { + return m_recopySeed; + } + + +private: + friend class WSGDamageTracker; + friend class WSGDamageNodeTestAccess; + WPixmanRegion m_recopySeed; + + QMargins m_recopyExpansion; +}; + +class WSGDamageNodeTestAccess +{ +public: + static const pixman_region32_t *ownDamage(const WSGDamageNode *n) + { + return n->m_ownDamage.native(); + } + + static const WPixmanRegion *pendingRecopy(const WSGDamageNode *n, WSGViewport *viewport) + { + if (const auto *backdrop = n->toBackdrop()) + return &backdrop->pendingRecopy(viewport); + return nullptr; + } +}; + +WAYLIB_SERVER_END_NAMESPACE + +Q_DECLARE_OPERATORS_FOR_FLAGS(WAYLIB_SERVER_NAMESPACE::WSGDamageNode::DirtyBits) diff --git a/waylib/src/server/qtquick/scenegraph/wsgdamagetracker.cpp b/waylib/src/server/qtquick/scenegraph/wsgdamagetracker.cpp new file mode 100644 index 0000000000..a153282c8b --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgdamagetracker.cpp @@ -0,0 +1,134 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "wsgdamagetracker_p.h" + +WAYLIB_SERVER_BEGIN_NAMESPACE + +WSGDamageTracker::WSGDamageTracker(WSGDamageNode *root) + : m_root(root) +{ +} + +WSGDamageTracker::~WSGDamageTracker() = default; + +void WSGDamageTracker::setRoot(WSGDamageNode *root) +{ + if (m_root == root) + return; + m_root = root; + m_settled = false; + m_damage = { }; +} + +WSGDamageNode *WSGDamageTracker::root() const +{ + return m_root; +} + +void WSGDamageTracker::addViewport(const std::shared_ptr &viewport) +{ + if (!viewport) + return; + + bool exists = false; + for (auto it = m_viewports.begin(); it != m_viewports.end();) { + auto sp = it->lock(); + if (!sp) { + it = m_viewports.erase(it); + continue; + } + if (sp == viewport) + exists = true; + ++it; + } + + if (!exists) { + m_viewports.append(viewport); + } +} + +void WSGDamageTracker::removeViewport(WSGViewport *viewport) +{ + if (!viewport) + return; + + for (auto it = m_viewports.begin(); it != m_viewports.end();) { + auto sp = it->lock(); + if (!sp || sp.get() == viewport) { + it = m_viewports.erase(it); + } else { + ++it; + } + } +} +void WSGDamageTracker::accumulateBackdropRecopy(const WSGDamageBackdropNode *node, const pixman_region32_t *damage) +{ + for (auto it = m_viewports.begin(); it != m_viewports.end(); ++it) { + auto viewport = it->lock(); + if (!viewport) + continue; + + auto *data = viewport->getAttachedData(); + if (!data) { + data = new WSGViewportBackdropData; + viewport->setAttachedData(data, [](void *p) { + delete static_cast(p); + }); + data->accumulate(node, node->recopySeed().native()); + } else { + data->accumulate(node, damage); + } + } +} + + +void WSGDamageTracker::settle() +{ + if (m_settled) + return; + m_settled = true; + m_damage = { }; + if (m_root && m_root->isDirty()) { + WPixmanRegion worldDamage; + WPixmanRegion backdropDamage; + m_root->updateWorld(QMatrix4x4(), false, worldDamage, backdropDamage, nullptr, nullptr, this); + m_damage = worldDamage; + m_damage += backdropDamage; + + constexpr auto kShape = WSGDamageNode::DirtyMatrix | WSGDamageNode::DirtyGeometry + | WSGDamageNode::DirtyAdded | WSGDamageNode::DirtyStructure + | WSGDamageNode::DirtyVisibility | WSGDamageNode::DirtyOpaque + | WSGDamageNode::DirtySubtreeGeometry; + if (m_root->dirty() & kShape) { + WPixmanRegion worldFrontOpaque; + WPixmanRegion worldFrontBackdrop; + m_root->computeWorldVisibility(worldFrontOpaque, worldFrontBackdrop); + } else { + m_root->commitStateRecursive(); + } + } +} + +void WSGDamageTracker::commit() +{ + settle(); + + for (auto it = m_viewports.begin(); it != m_viewports.end();) { + auto viewport = it->lock(); + if (!viewport) { + it = m_viewports.erase(it); + continue; + } + + if (!viewport->isFull() && m_root && !m_damage.isEmpty()) + viewport->addDamage(WDamageRegion(m_damage, false)); + + ++it; + } + + m_settled = false; + m_damage = { }; +} + + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgdamagetracker_p.h b/waylib/src/server/qtquick/scenegraph/wsgdamagetracker_p.h new file mode 100644 index 0000000000..98a5edaf13 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgdamagetracker_p.h @@ -0,0 +1,43 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include "wsgdamagenode_p.h" +#include "wsgviewport_p.h" +#include +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WAYLIB_SERVER_EXPORT WSGDamageTracker +{ +public: + WSGDamageTracker() = default; + explicit WSGDamageTracker(WSGDamageNode *root); + ~WSGDamageTracker(); + + void setRoot(WSGDamageNode *root); + WSGDamageNode *root() const; + + void addViewport(const std::shared_ptr &viewport); + void removeViewport(WSGViewport *viewport); + void accumulateBackdropRecopy(const WSGDamageBackdropNode *node, const pixman_region32_t *damage); + + + // Walks the dirty tree, copies scene damage into every registered + // non-full viewport, then unlocks so the next commit() can settle + // new dirty. Mapping to a specific buffer happens at draw time. + void commit(); + +private: + void settle(); + + WSGDamageNode *m_root = nullptr; + WPixmanRegion m_damage; + QList> m_viewports; + bool m_settled = false; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgimagenode.cpp b/waylib/src/server/qtquick/scenegraph/wsgimagenode.cpp new file mode 100644 index 0000000000..8fb5b4c40f --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgimagenode.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgimagenode_p.h" + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +void WSGImageNode::setRect(const QRectF &rect) +{ + QSGDefaultImageNode::setRect(rect); + // rebuildGeometry is a no-op without a texture; keep vertex bounds in + // sync so the damage tracker can read the quad before setTexture. + if (!texture()) + QSGGeometry::updateTexturedRectGeometry(geometry(), rect, QRectF(0, 0, 1, 1)); +} + +void WSGImageNode::setDamageRegion(const WPixmanRegion ®ion) +{ + m_region = region; + m_explicit = true; + markDirty(QSGNode::DirtyMaterial); +} + +void WSGImageNode::clearDamageRegion() +{ + if (!m_explicit && m_region.isEmpty()) + return; + m_region.clear(); + m_explicit = false; + markDirty(QSGNode::DirtyMaterial); +} + +void WSGImageNode::setOpaqueRegion(const WPixmanRegion ®ion) +{ + if (m_opaqueExplicit && m_opaque == region) + return; + m_opaque = region; + m_opaqueExplicit = true; + markDirty(QSGNode::DirtyMaterial); +} + +WSGImageNode *WSGImageNode::enclosingNode(QSGNode *node) +{ + for (QSGNode *n = node; n; n = n->parent()) { + if (auto *image = dynamic_cast(n)) + return image; + } + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgimagenode_p.h b/waylib/src/server/qtquick/scenegraph/wsgimagenode_p.h new file mode 100644 index 0000000000..f8d69cd849 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgimagenode_p.h @@ -0,0 +1,58 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +// QSGDefaultImageNode plus optional content damage. WSGContext::createImageNode +// returns this so Wayland surfaces can report buffer damage on the node +// itself instead of wrapping it in an extra QSGNode. +class WAYLIB_SERVER_EXPORT WSGImageNode : public QSGDefaultImageNode +{ +public: + void setRect(const QRectF &rect) override; + + void setDamageRegion(const WPixmanRegion ®ion); + void clearDamageRegion(); + + bool hasExplicitDamage() const + { + return m_explicit; + } + + const WPixmanRegion &damageRegion() const + { + return m_region; + } + + // Surface-local opaque mapped into item coordinates (same space as + // damageRegion). Empty + hasExplicitOpaque means the client claimed + // nothing opaque; do not fall back to material blending. + void setOpaqueRegion(const WPixmanRegion ®ion); + + bool hasExplicitOpaque() const + { + return m_opaqueExplicit; + } + + const WPixmanRegion &opaqueRegion() const + { + return m_opaque; + } + + static WSGImageNode *enclosingNode(QSGNode *node); + +private: + WPixmanRegion m_region; + WPixmanRegion m_opaque; + bool m_explicit = false; + bool m_opaqueExplicit = false; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgrhivisualizer.cpp b/waylib/src/server/qtquick/scenegraph/wsgrhivisualizer.cpp new file mode 100644 index 0000000000..93fc016ac3 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgrhivisualizer.cpp @@ -0,0 +1,890 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// Copyright (C) 2016 Jolla Ltd, author: +// Copyright (C) 2016 Robin Burchell +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgrhivisualizer_p.h" +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +namespace WSGBatchRenderer +{ + +#define QSGNODE_TRAVERSE(NODE) for (QSGNode *child = NODE->firstChild(); child; child = child->nextSibling()) +#define SHADOWNODE_TRAVERSE(NODE) for (Node *child = NODE->firstChild(); child; child = child->sibling()) +#define QSGNODE_DIRTY_PARENT (QSGNode::DirtyNodeAdded \ + | QSGNode::DirtyOpacity \ + | QSGNode::DirtyMatrix \ + | QSGNode::DirtyNodeRemoved) + +QMatrix4x4 qsg_matrixForRoot(Node *node); +QRhiVertexInputAttribute::Format qsg_vertexInputFormat(const QSGGeometry::Attribute &a); +QRhiCommandBuffer::IndexFormat qsg_indexFormat(const QSGGeometry *geometry); +QRhiGraphicsPipeline::Topology qsg_topology(int geomDrawMode, QRhi *rhi); + +RhiVisualizer::RhiVisualizer(Renderer *renderer) + : Visualizer(renderer) +{ +} + +RhiVisualizer::~RhiVisualizer() +{ + releaseResources(); +} + +void RhiVisualizer::releaseResources() +{ + m_pipelines.releaseResources(); + + m_fade.releaseResources(); + + m_changeVis.releaseResources(); + m_batchVis.releaseResources(); + m_clipVis.releaseResources(); + m_overdrawVis.releaseResources(); +} + +void RhiVisualizer::prepareVisualize() +{ + // Called before the render pass has begun (but after preparing the + // batches). Now is the time to put resource updates to the renderer's + // current m_resourceUpdates instance. + + if (m_visualizeMode == VisualizeNothing) + return; + + if (!m_vs.isValid()) { + m_vs = QSGMaterialShaderPrivate::loadShader( + QLatin1String(":/qt-project.org/scenegraph/shaders_ng/visualization.vert.qsb")); + m_fs = QSGMaterialShaderPrivate::loadShader( + QLatin1String(":/qt-project.org/scenegraph/shaders_ng/visualization.frag.qsb")); + } + + m_fade.prepare(this, m_renderer->m_rhi, m_renderer->m_resourceUpdates, m_renderer->renderTarget().rpDesc); + + const bool forceUintIndex = m_renderer->m_uint32IndexForRhi; + + switch (m_visualizeMode) { + case VisualizeBatches: + m_batchVis.prepare(m_renderer->m_opaqueBatches, m_renderer->m_alphaBatches, + this, + m_renderer->m_rhi, m_renderer->m_resourceUpdates, + forceUintIndex); + break; + case VisualizeClipping: + m_clipVis.prepare(m_renderer->rootNode(), this, + m_renderer->m_rhi, m_renderer->m_resourceUpdates); + break; + case VisualizeChanges: + m_changeVis.prepare(m_renderer->m_nodes.value(m_renderer->rootNode()), + this, + m_renderer->m_rhi, m_renderer->m_resourceUpdates); + m_visualizeChangeSet.clear(); + break; + case VisualizeOverdraw: + m_overdrawVis.prepare(m_renderer->m_nodes.value(m_renderer->rootNode()), + this, + m_renderer->m_rhi, m_renderer->m_resourceUpdates); + break; + default: + Q_UNREACHABLE(); + break; + } +} + +void RhiVisualizer::visualize() +{ + if (m_visualizeMode == VisualizeNothing) + return; + + QRhiCommandBuffer *cb = m_renderer->renderTarget().cb; + m_fade.render(cb); + + switch (m_visualizeMode) { + case VisualizeBatches: + m_batchVis.render(cb); + break; + case VisualizeClipping: + m_clipVis.render(cb); + break; + case VisualizeChanges: + m_changeVis.render(cb); + break; + case VisualizeOverdraw: + m_overdrawVis.render(cb); + break; + default: + Q_UNREACHABLE(); + break; + } +} + +void RhiVisualizer::recordDrawCalls(const QList &drawCalls, + QRhiCommandBuffer *cb, + QRhiShaderResourceBindings *srb, + bool blendOneOne) +{ + for (const DrawCall &dc : drawCalls) { + QRhiGraphicsPipeline *ps = m_pipelines.pipeline(this, m_renderer->m_rhi, srb, m_renderer->renderTarget().rpDesc, + dc.vertex.topology, dc.vertex.format, dc.vertex.stride, + blendOneOne); + if (!ps) + continue; + cb->setGraphicsPipeline(ps); // no-op if same as the last one + QRhiCommandBuffer::DynamicOffset dynofs(0, dc.buf.ubufOffset); + cb->setShaderResources(srb, 1, &dynofs); + QRhiCommandBuffer::VertexInput vb(dc.buf.vbuf, dc.buf.vbufOffset); + if (dc.index.count) { + cb->setVertexInput(0, 1, &vb, dc.buf.ibuf, dc.buf.ibufOffset, dc.index.format); + cb->drawIndexed(dc.index.count); + } else { + cb->setVertexInput(0, 1, &vb); + cb->draw(dc.vertex.count); + } + } +} + +const QRhiShaderResourceBinding::StageFlags ubufVisibility = + QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; + +void RhiVisualizer::Fade::prepare(RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u, QRhiRenderPassDescriptor *rpDesc) +{ + this->visualizer = visualizer; + + if (!vbuf) { + float v[] = { -1, 1, 1, 1, -1, -1, 1, -1 }; + vbuf = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(v)); + if (!vbuf->create()) + return; + u->uploadStaticBuffer(vbuf, v); + } + + if (!ubuf) { + ubuf = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, DrawCall::UBUF_SIZE); + if (!ubuf->create()) + return; + float bgOpacity = 0.8f; + if (visualizer->m_visualizeMode == Visualizer::VisualizeBatches) + bgOpacity = 1.0; + QMatrix4x4 ident; + u->updateDynamicBuffer(ubuf, 0, 64, ident.constData()); // matrix + u->updateDynamicBuffer(ubuf, 64, 64, ident.constData()); // rotation + float color[4] = { 0.0f, 0.0f, 0.0f, bgOpacity }; + u->updateDynamicBuffer(ubuf, 128, 16, color); + float pattern = 0.0f; + u->updateDynamicBuffer(ubuf, 144, 4, &pattern); + qint32 projection = 0; + u->updateDynamicBuffer(ubuf, 148, 4, &projection); + } + + if (!srb) { + srb = rhi->newShaderResourceBindings(); + srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, ubufVisibility, ubuf) }); + if (!srb->create()) + return; + } + + if (!ps) { + ps = rhi->newGraphicsPipeline(); + ps->setTopology(QRhiGraphicsPipeline::TriangleStrip); + QRhiGraphicsPipeline::TargetBlend blend; // defaults to premul alpha, just what we need + blend.enable = true; + ps->setTargetBlends({ blend }); + ps->setShaderStages({ { QRhiShaderStage::Vertex, visualizer->m_vs }, + { QRhiShaderStage::Fragment, visualizer->m_fs } }); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ { 2 * sizeof(float) } }); + inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } }); + ps->setVertexInputLayout(inputLayout); + ps->setShaderResourceBindings(srb); + ps->setRenderPassDescriptor(rpDesc); + if (!ps->create()) + return; + } +} + +void RhiVisualizer::Fade::releaseResources() +{ + delete ps; + ps = nullptr; + + delete srb; + srb = nullptr; + + delete ubuf; + ubuf = nullptr; + + delete vbuf; + vbuf = nullptr; +} + +void RhiVisualizer::Fade::render(QRhiCommandBuffer *cb) +{ + cb->setGraphicsPipeline(ps); + cb->setViewport(visualizer->m_renderer->m_pstate.viewport); + cb->setShaderResources(); + QRhiCommandBuffer::VertexInput vb(vbuf, 0); + cb->setVertexInput(0, 1, &vb); + cb->draw(4); +} + +static void fillVertexIndex(RhiVisualizer::DrawCall *dc, QSGGeometry *g, bool withData, bool forceUintIndex, QRhi *rhi) +{ + dc->vertex.topology = qsg_topology(g->drawingMode(), rhi); + dc->vertex.format = qsg_vertexInputFormat(g->attributes()[0]); + dc->vertex.count = g->vertexCount(); + dc->vertex.stride = g->sizeOfVertex(); + if (withData) + dc->vertex.data = g->vertexData(); + + dc->index.format = forceUintIndex ? QRhiCommandBuffer::IndexUInt32 : qsg_indexFormat(g); + dc->index.count = g->indexCount(); + dc->index.stride = forceUintIndex ? sizeof(quint32) : g->sizeOfIndex(); + if (withData && g->indexCount()) + dc->index.data = g->indexData(); +} + +static inline uint aligned(uint v, uint byteAlign) +{ + return (v + byteAlign - 1) & ~(byteAlign - 1); +} + +static bool ensureBuffer(QRhi *rhi, QRhiBuffer **buf, QRhiBuffer::UsageFlags usage, quint32 newSize) +{ + if (!*buf) { + *buf = rhi->newBuffer(QRhiBuffer::Dynamic, usage, newSize); + if (!(*buf)->create()) + return false; + } else if ((*buf)->size() < newSize) { + (*buf)->setSize(newSize); + if (!(*buf)->create()) + return false; + } + return true; +} + +QRhiGraphicsPipeline *RhiVisualizer::PipelineCache::pipeline(RhiVisualizer *visualizer, + QRhi *rhi, + QRhiShaderResourceBindings *srb, + QRhiRenderPassDescriptor *rpDesc, + QRhiGraphicsPipeline::Topology topology, + QRhiVertexInputAttribute::Format vertexFormat, + quint32 vertexStride, + bool blendOneOne) +{ + for (int i = 0, ie = pipelines.size(); i != ie; ++i) { + const Pipeline &p(pipelines.at(i)); + if (p.topology == topology && p.format == vertexFormat && p.stride == vertexStride) + return p.ps; + } + + QRhiGraphicsPipeline *ps = rhi->newGraphicsPipeline(); + ps->setTopology(topology); + QRhiGraphicsPipeline::TargetBlend blend; // premul alpha + blend.enable = true; + if (blendOneOne) { + // only for visualizing overdraw, other modes use premul alpha + blend.srcColor = QRhiGraphicsPipeline::One; + blend.dstColor = QRhiGraphicsPipeline::One; + blend.srcAlpha = QRhiGraphicsPipeline::One; + blend.dstAlpha = QRhiGraphicsPipeline::One; + } + ps->setTargetBlends({ blend }); + ps->setShaderStages({ { QRhiShaderStage::Vertex, visualizer->m_vs }, + { QRhiShaderStage::Fragment, visualizer->m_fs } }); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ { vertexStride } }); + inputLayout.setAttributes({ { 0, 0, vertexFormat, 0 } }); + ps->setVertexInputLayout(inputLayout); + ps->setShaderResourceBindings(srb); + ps->setRenderPassDescriptor(rpDesc); + if (!ps->create()) + return nullptr; + + Pipeline p; + p.topology = topology; + p.format = vertexFormat; + p.stride = vertexStride; + p.ps = ps; + pipelines.append(p); + + return ps; +} + +void RhiVisualizer::PipelineCache::releaseResources() +{ + for (int i = 0, ie = pipelines.size(); i != ie; ++i) + delete pipelines.at(i).ps; + + pipelines.clear(); +} + +void RhiVisualizer::ChangeVis::gather(Node *n) +{ + if (n->type() == QSGNode::GeometryNodeType && n->element()->batch && visualizer->m_visualizeChangeSet.contains(n)) { + const uint dirty = visualizer->m_visualizeChangeSet.value(n); + const bool tinted = (dirty & QSGNODE_DIRTY_PARENT) != 0; + const QColor color = QColor::fromHsvF((visualizer->m_randomGenerator.generate() & 1023) / 1023.0f, 0.3f, 1.0f).toRgb(); + const float alpha = 0.5f; + + QMatrix4x4 matrix = visualizer->m_renderer->m_current_projection_matrix[0]; + if (n->element()->batch->root) + matrix = matrix * qsg_matrixForRoot(n->element()->batch->root); + + QSGGeometryNode *gn = static_cast(n->sgNode); + matrix = matrix * *gn->matrix(); + + QSGGeometry *g = gn->geometry(); + if (g->attributeCount() >= 1) { + DrawCall dc; + memcpy(dc.uniforms.data, matrix.constData(), 64); + QMatrix4x4 rotation; + memcpy(dc.uniforms.data + 64, rotation.constData(), 64); + float c[4] = { + float(color.redF()) * alpha, + float(color.greenF()) * alpha, + float(color.blueF()) * alpha, + alpha + }; + memcpy(dc.uniforms.data + 128, c, 16); + float pattern = tinted ? 0.5f : 0.0f; + memcpy(dc.uniforms.data + 144, &pattern, 4); + qint32 projection = 0; + memcpy(dc.uniforms.data + 148, &projection, 4); + + fillVertexIndex(&dc, g, true, false, visualizer->m_renderer->m_rhi); + drawCalls.append(dc); + } + + // This is because many changes don't propegate their dirty state to the + // parent so the node updater will not unset these states. They are + // not used for anything so, unsetting it should have no side effects. + n->dirtyState = { }; + } + + SHADOWNODE_TRAVERSE(n) { + gather(child); + } +} + +void RhiVisualizer::ChangeVis::prepare(Node *n, RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u) +{ + this->visualizer = visualizer; + + drawCalls.clear(); + gather(n); + + if (drawCalls.isEmpty()) + return; + + const int ubufAlign = rhi->ubufAlignment(); + int vbufOffset = 0; + int ibufOffset = 0; + int ubufOffset = 0; + for (RhiVisualizer::DrawCall &dc : drawCalls) { + dc.buf.vbufOffset = aligned(vbufOffset, 4); + vbufOffset = dc.buf.vbufOffset + dc.vertex.count * dc.vertex.stride; + + dc.buf.ibufOffset = aligned(ibufOffset, 4); + ibufOffset = dc.buf.ibufOffset + dc.index.count * dc.index.stride; + + dc.buf.ubufOffset = aligned(ubufOffset, ubufAlign); + ubufOffset = dc.buf.ubufOffset + DrawCall::UBUF_SIZE; + } + + ensureBuffer(rhi, &vbuf, QRhiBuffer::VertexBuffer, vbufOffset); + if (ibufOffset) + ensureBuffer(rhi, &ibuf, QRhiBuffer::IndexBuffer, ibufOffset); + const int ubufSize = ubufOffset; + ensureBuffer(rhi, &ubuf, QRhiBuffer::UniformBuffer, ubufSize); + + for (RhiVisualizer::DrawCall &dc : drawCalls) { + u->updateDynamicBuffer(vbuf, dc.buf.vbufOffset, dc.vertex.count * dc.vertex.stride, dc.vertex.data); + dc.buf.vbuf = vbuf; + if (dc.index.count) { + u->updateDynamicBuffer(ibuf, dc.buf.ibufOffset, dc.index.count * dc.index.stride, dc.index.data); + dc.buf.ibuf = ibuf; + } + u->updateDynamicBuffer(ubuf, dc.buf.ubufOffset, DrawCall::UBUF_SIZE, dc.uniforms.data); + } + + if (!srb) { + srb = rhi->newShaderResourceBindings(); + srb->setBindings({ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, ubufVisibility, ubuf, DrawCall::UBUF_SIZE) }); + if (!srb->create()) + return; + } +} + +void RhiVisualizer::ChangeVis::releaseResources() +{ + delete srb; + srb = nullptr; + + delete ubuf; + ubuf = nullptr; + + delete ibuf; + ibuf = nullptr; + + delete vbuf; + vbuf = nullptr; +} + +void RhiVisualizer::ChangeVis::render(QRhiCommandBuffer *cb) +{ + visualizer->recordDrawCalls(drawCalls, cb, srb); +} + +void RhiVisualizer::BatchVis::gather(Batch *b) +{ + if (b->positionAttribute != 0) + return; + + QMatrix4x4 matrix(visualizer->m_renderer->m_current_projection_matrix[0]); + if (b->root) + matrix = matrix * qsg_matrixForRoot(b->root); + + DrawCall dc; + + QMatrix4x4 rotation; + memcpy(dc.uniforms.data + 64, rotation.constData(), 64); + + const QColor color = QColor::fromHsvF((visualizer->m_randomGenerator.generate() & 1023) / 1023.0, 1.0, 1.0).toRgb(); + + float c[4] = { + float(color.redF()), + float(color.greenF()), + float(color.blueF()), + 1.0f + }; + memcpy(dc.uniforms.data + 128, c, 16); + + float pattern = b->merged ? 0.0f : 1.0f; + memcpy(dc.uniforms.data + 144, &pattern, 4); + + qint32 projection = 0; + memcpy(dc.uniforms.data + 148, &projection, 4); + + if (b->merged) { + memcpy(dc.uniforms.data, matrix.constData(), 64); + + QSGGeometryNode *gn = b->first->node; + QSGGeometry *g = gn->geometry(); + + fillVertexIndex(&dc, g, false, forceUintIndex, visualizer->m_renderer->m_rhi); + + for (int ds = 0; ds < b->drawSets.size(); ++ds) { + const DrawSet &set = b->drawSets.at(ds); + dc.buf.vbuf = b->vbo.buf; + dc.buf.vbufOffset = set.vertices; + dc.buf.ibuf = b->ibo.buf; + dc.buf.ibufOffset = set.indices; + dc.index.count = set.indexCount; + drawCalls.append(dc); + } + } else { + Element *e = b->first; + int vOffset = 0; + int iOffset = 0; + + while (e) { + QSGGeometryNode *gn = e->node; + QSGGeometry *g = gn->geometry(); + + QMatrix4x4 m = matrix * *gn->matrix(); + memcpy(dc.uniforms.data, m.constData(), 64); + + fillVertexIndex(&dc, g, false, forceUintIndex, visualizer->m_renderer->m_rhi); + + dc.buf.vbuf = b->vbo.buf; + dc.buf.vbufOffset = vOffset; + if (g->indexCount()) { + dc.buf.ibuf = b->ibo.buf; + dc.buf.ibufOffset = iOffset; + } + + drawCalls.append(dc); + + vOffset += dc.vertex.count * dc.vertex.stride; + iOffset += dc.index.count * dc.index.stride; + + e = e->nextInBatch; + } + } +} + +void RhiVisualizer::BatchVis::prepare(const QDataBuffer &opaqueBatches, const QDataBuffer &alphaBatches, + RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u, + bool forceUintIndex) +{ + this->visualizer = visualizer; + this->forceUintIndex = forceUintIndex; + + drawCalls.clear(); + + for (int i = 0; i < opaqueBatches.size(); ++i) + gather(opaqueBatches.at(i)); + for (int i = 0; i < alphaBatches.size(); ++i) + gather(alphaBatches.at(i)); + + if (drawCalls.isEmpty()) + return; + + const int ubufAlign = rhi->ubufAlignment(); + int ubufOffset = 0; + for (RhiVisualizer::DrawCall &dc : drawCalls) { + dc.buf.ubufOffset = aligned(ubufOffset, ubufAlign); + ubufOffset = dc.buf.ubufOffset + DrawCall::UBUF_SIZE; + } + + const int ubufSize = ubufOffset; + ensureBuffer(rhi, &ubuf, QRhiBuffer::UniformBuffer, ubufSize); + + for (RhiVisualizer::DrawCall &dc : drawCalls) + u->updateDynamicBuffer(ubuf, dc.buf.ubufOffset, DrawCall::UBUF_SIZE, dc.uniforms.data); + + if (!srb) { + srb = rhi->newShaderResourceBindings(); + srb->setBindings({ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, ubufVisibility, ubuf, DrawCall::UBUF_SIZE) }); + if (!srb->create()) + return; + } +} + +void RhiVisualizer::BatchVis::releaseResources() +{ + delete srb; + srb = nullptr; + + delete ubuf; + ubuf = nullptr; +} + +void RhiVisualizer::BatchVis::render(QRhiCommandBuffer *cb) +{ + visualizer->recordDrawCalls(drawCalls, cb, srb); +} + +void RhiVisualizer::ClipVis::gather(QSGNode *node) +{ + if (node->type() == QSGNode::ClipNodeType) { + QSGClipNode *clipNode = static_cast(node); + QMatrix4x4 matrix = visualizer->m_renderer->m_current_projection_matrix[0]; + if (clipNode->matrix()) + matrix = matrix * *clipNode->matrix(); + + QSGGeometry *g = clipNode->geometry(); + if (g->attributeCount() >= 1) { + DrawCall dc; + memcpy(dc.uniforms.data, matrix.constData(), 64); + QMatrix4x4 rotation; + memcpy(dc.uniforms.data + 64, rotation.constData(), 64); + float c[4] = { 0.2f, 0.0f, 0.0f, 0.2f }; + memcpy(dc.uniforms.data + 128, c, 16); + float pattern = 0.5f; + memcpy(dc.uniforms.data + 144, &pattern, 4); + qint32 projection = 0; + memcpy(dc.uniforms.data + 148, &projection, 4); + fillVertexIndex(&dc, g, true, false, visualizer->m_renderer->m_rhi); + drawCalls.append(dc); + } + } + + QSGNODE_TRAVERSE(node) { + gather(child); + } +} + +void RhiVisualizer::ClipVis::prepare(QSGNode *node, RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u) +{ + this->visualizer = visualizer; + + drawCalls.clear(); + gather(node); + + if (drawCalls.isEmpty()) + return; + + const int ubufAlign = rhi->ubufAlignment(); + int vbufOffset = 0; + int ibufOffset = 0; + int ubufOffset = 0; + for (RhiVisualizer::DrawCall &dc : drawCalls) { + dc.buf.vbufOffset = aligned(vbufOffset, 4); + vbufOffset = dc.buf.vbufOffset + dc.vertex.count * dc.vertex.stride; + + dc.buf.ibufOffset = aligned(ibufOffset, 4); + ibufOffset = dc.buf.ibufOffset + dc.index.count * dc.index.stride; + + dc.buf.ubufOffset = aligned(ubufOffset, ubufAlign); + ubufOffset = dc.buf.ubufOffset + DrawCall::UBUF_SIZE; + } + + ensureBuffer(rhi, &vbuf, QRhiBuffer::VertexBuffer, vbufOffset); + if (ibufOffset) + ensureBuffer(rhi, &ibuf, QRhiBuffer::IndexBuffer, ibufOffset); + const int ubufSize = ubufOffset; + ensureBuffer(rhi, &ubuf, QRhiBuffer::UniformBuffer, ubufSize); + + for (RhiVisualizer::DrawCall &dc : drawCalls) { + u->updateDynamicBuffer(vbuf, dc.buf.vbufOffset, dc.vertex.count * dc.vertex.stride, dc.vertex.data); + dc.buf.vbuf = vbuf; + if (dc.index.count) { + u->updateDynamicBuffer(ibuf, dc.buf.ibufOffset, dc.index.count * dc.index.stride, dc.index.data); + dc.buf.ibuf = ibuf; + } + u->updateDynamicBuffer(ubuf, dc.buf.ubufOffset, DrawCall::UBUF_SIZE, dc.uniforms.data); + } + + if (!srb) { + srb = rhi->newShaderResourceBindings(); + srb->setBindings({ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, ubufVisibility, ubuf, DrawCall::UBUF_SIZE) }); + if (!srb->create()) + return; + } +} + +void RhiVisualizer::ClipVis::releaseResources() +{ + delete srb; + srb = nullptr; + + delete ubuf; + ubuf = nullptr; + + delete ibuf; + ibuf = nullptr; + + delete vbuf; + vbuf = nullptr; +} + +void RhiVisualizer::ClipVis::render(QRhiCommandBuffer *cb) +{ + visualizer->recordDrawCalls(drawCalls, cb, srb); +} + +void RhiVisualizer::OverdrawVis::gather(Node *n) +{ + if (n->type() == QSGNode::GeometryNodeType && n->element()->batch) { + QMatrix4x4 matrix = visualizer->m_renderer->m_current_projection_matrix[0]; + matrix(2, 2) = visualizer->m_renderer->m_zRange; + matrix(2, 3) = 1.0f - n->element()->order * visualizer->m_renderer->m_zRange; + + if (n->element()->batch->root) + matrix = matrix * qsg_matrixForRoot(n->element()->batch->root); + + QSGGeometryNode *gn = static_cast(n->sgNode); + matrix = matrix * *gn->matrix(); + + QSGGeometry *g = gn->geometry(); + if (g->attributeCount() >= 1) { + DrawCall dc; + memcpy(dc.uniforms.data, matrix.constData(), 64); + memcpy(dc.uniforms.data + 64, rotation.constData(), 64); + + float c[4]; + const float ca = 0.33f; + if (n->element()->batch->isOpaque) { + c[0] = ca * 0.3f; c[1] = ca * 1.0f; c[2] = ca * 0.3f; c[3] = ca; + } else { + c[0] = ca * 1.0f; c[1] = ca * 0.3f; c[2] = ca * 0.3f; c[3] = ca; + } + memcpy(dc.uniforms.data + 128, c, 16); + float pattern = 0.0f; + memcpy(dc.uniforms.data + 144, &pattern, 4); + qint32 projection = 1; + memcpy(dc.uniforms.data + 148, &projection, 4); + + fillVertexIndex(&dc, g, true, false, visualizer->m_renderer->m_rhi); + drawCalls.append(dc); + } + } + + SHADOWNODE_TRAVERSE(n) { + gather(child); + } +} + +void RhiVisualizer::OverdrawVis::prepare(Node *n, RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u) +{ + this->visualizer = visualizer; + + step += float(M_PI * 2 / 1000.0); + if (step > float(M_PI * 2)) + step = 0.0f; + + const float yfix = rhi->isYUpInNDC() ? 1.0f : -1.0f; + rotation.setToIdentity(); + rotation.translate(0.0f, 0.5f * yfix, 4.0f); + rotation.scale(2.0f, 2.0f, 1.0f); + rotation.rotate(-30.0f * yfix, 1.0f, 0.0f, 0.0f); + rotation.rotate(80.0f * std::sin(step), 0.0f, 1.0f, 0.0f); + rotation.translate(0.0f, 0.0f, -1.0f); + + drawCalls.clear(); + gather(n); + + if (!box.vbuf) { + const float v[] = { + // lower + -1, 1, 0, 1, 1, 0, + -1, 1, 0, -1, -1, 0, + 1, 1, 0, 1, -1, 0, + -1, -1, 0, 1, -1, 0, + + // upper + -1, 1, 1, 1, 1, 1, + -1, 1, 1, -1, -1, 1, + 1, 1, 1, 1, -1, 1, + -1, -1, 1, 1, -1, 1, + + // sides + -1, -1, 0, -1, -1, 1, + 1, -1, 0, 1, -1, 1, + -1, 1, 0, -1, 1, 1, + 1, 1, 0, 1, 1, 1 + }; + box.vbuf = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(v)); + if (!box.vbuf->create()) + return; + u->uploadStaticBuffer(box.vbuf, v); + } + + if (!box.ubuf) { + box.ubuf = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, DrawCall::UBUF_SIZE); + if (!box.ubuf->create()) + return; + QMatrix4x4 ident; + u->updateDynamicBuffer(box.ubuf, 0, 64, ident.constData()); + float color[4] = { 0.5f, 0.5f, 1.0f, 1.0f }; + u->updateDynamicBuffer(box.ubuf, 128, 16, color); + float pattern = 0.0f; + u->updateDynamicBuffer(box.ubuf, 144, 4, &pattern); + qint32 projection = 1; + u->updateDynamicBuffer(box.ubuf, 148, 4, &projection); + } + + u->updateDynamicBuffer(box.ubuf, 64, 64, rotation.constData()); + + if (!box.srb) { + box.srb = rhi->newShaderResourceBindings(); + box.srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, ubufVisibility, box.ubuf) }); + if (!box.srb->create()) + return; + } + + if (!box.ps) { + box.ps = rhi->newGraphicsPipeline(); + box.ps->setTopology(QRhiGraphicsPipeline::Lines); + box.ps->setLineWidth(2); // may be be ignored (D3D, Metal), but may be used on GL and Vulkan + QRhiGraphicsPipeline::TargetBlend blend; + blend.enable = true; + blend.srcColor = QRhiGraphicsPipeline::One; + blend.dstColor = QRhiGraphicsPipeline::One; + blend.srcAlpha = QRhiGraphicsPipeline::One; + blend.dstAlpha = QRhiGraphicsPipeline::One; + box.ps->setTargetBlends({ blend }); + box.ps->setShaderStages({ { QRhiShaderStage::Vertex, visualizer->m_vs }, + { QRhiShaderStage::Fragment, visualizer->m_fs } }); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ { 3 * sizeof(float) } }); + inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } }); + box.ps->setVertexInputLayout(inputLayout); + box.ps->setShaderResourceBindings(box.srb); + box.ps->setRenderPassDescriptor(visualizer->m_renderer->renderTarget().rpDesc); + if (!box.ps->create()) + return; + } + + if (drawCalls.isEmpty()) + return; + + const int ubufAlign = rhi->ubufAlignment(); + int vbufOffset = 0; + int ibufOffset = 0; + int ubufOffset = 0; + for (RhiVisualizer::DrawCall &dc : drawCalls) { + dc.buf.vbufOffset = aligned(vbufOffset, 4); + vbufOffset = dc.buf.vbufOffset + dc.vertex.count * dc.vertex.stride; + + dc.buf.ibufOffset = aligned(ibufOffset, 4); + ibufOffset = dc.buf.ibufOffset + dc.index.count * dc.index.stride; + + dc.buf.ubufOffset = aligned(ubufOffset, ubufAlign); + ubufOffset = dc.buf.ubufOffset + DrawCall::UBUF_SIZE; + } + + ensureBuffer(rhi, &vbuf, QRhiBuffer::VertexBuffer, vbufOffset); + if (ibufOffset) + ensureBuffer(rhi, &ibuf, QRhiBuffer::IndexBuffer, ibufOffset); + const int ubufSize = ubufOffset; + ensureBuffer(rhi, &ubuf, QRhiBuffer::UniformBuffer, ubufSize); + + for (RhiVisualizer::DrawCall &dc : drawCalls) { + u->updateDynamicBuffer(vbuf, dc.buf.vbufOffset, dc.vertex.count * dc.vertex.stride, dc.vertex.data); + dc.buf.vbuf = vbuf; + if (dc.index.count) { + u->updateDynamicBuffer(ibuf, dc.buf.ibufOffset, dc.index.count * dc.index.stride, dc.index.data); + dc.buf.ibuf = ibuf; + } + u->updateDynamicBuffer(ubuf, dc.buf.ubufOffset, DrawCall::UBUF_SIZE, dc.uniforms.data); + } + + if (!srb) { + srb = rhi->newShaderResourceBindings(); + srb->setBindings({ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, ubufVisibility, ubuf, DrawCall::UBUF_SIZE) }); + if (!srb->create()) + return; + } +} + +void RhiVisualizer::OverdrawVis::releaseResources() +{ + delete srb; + srb = nullptr; + + delete ubuf; + ubuf = nullptr; + + delete ibuf; + ibuf = nullptr; + + delete vbuf; + vbuf = nullptr; + + delete box.ps; + box.ps = nullptr; + + delete box.srb; + box.srb = nullptr; + + delete box.ubuf; + box.ubuf = nullptr; + + delete box.vbuf; + box.vbuf = nullptr; +} + +void RhiVisualizer::OverdrawVis::render(QRhiCommandBuffer *cb) +{ + cb->setGraphicsPipeline(box.ps); + cb->setShaderResources(); + QRhiCommandBuffer::VertexInput vb(box.vbuf, 0); + cb->setVertexInput(0, 1, &vb); + cb->draw(24); + + visualizer->recordDrawCalls(drawCalls, cb, srb, true); +} + +} // namespace WSGBatchRenderer + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgrhivisualizer_p.h b/waylib/src/server/qtquick/scenegraph/wsgrhivisualizer_p.h new file mode 100644 index 0000000000..06197f71c4 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgrhivisualizer_p.h @@ -0,0 +1,201 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2016 Jolla Ltd, author: +// Copyright (C) 2016 Robin Burchell +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef WSGRHIVISUALIZER_P_H +#define WSGRHIVISUALIZER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "wsgbatchrenderer_p.h" + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +namespace WSGBatchRenderer +{ + +class RhiVisualizer : public Visualizer +{ +public: + RhiVisualizer(Renderer *renderer); + ~RhiVisualizer(); + + void prepareVisualize() override; + void visualize() override; + + void releaseResources() override; + + struct DrawCall + { + static const int UBUF_SIZE = 152; // visualization.vert/frag + struct { + char data[UBUF_SIZE]; // matrix, rotation, color, pattern, projection + } uniforms; + struct { + QRhiGraphicsPipeline::Topology topology; + QRhiVertexInputAttribute::Format format; + int count; + int stride; + const void *data; // only when using own vbuf + } vertex; + struct { + QRhiCommandBuffer::IndexFormat format; + int count; + int stride; + const void *data; // only when using own ibuf + } index; + struct { + QRhiBuffer *vbuf; // either same for all draw calls and owned by the *Vis, or points to a Batch.Buffer.vbo.buf + int vbufOffset; + QRhiBuffer *ibuf; // same, but for index + int ibufOffset; + int ubufOffset; + } buf; + }; + +private: + QShader m_vs; + QShader m_fs; + + void recordDrawCalls(const QList &drawCalls, + QRhiCommandBuffer *cb, + QRhiShaderResourceBindings *srb, + bool blendOneOne = false); + + class PipelineCache { + public: + QRhiGraphicsPipeline *pipeline(RhiVisualizer *visualizer, + QRhi *rhi, + QRhiShaderResourceBindings *srb, + QRhiRenderPassDescriptor *rpDesc, + QRhiGraphicsPipeline::Topology topology, + QRhiVertexInputAttribute::Format vertexFormat, + quint32 vertexStride, + bool blendOneOne); + void releaseResources(); + private: + struct Pipeline { + QRhiGraphicsPipeline::Topology topology; + QRhiVertexInputAttribute::Format format; + quint32 stride; + QRhiGraphicsPipeline *ps; + }; + QVarLengthArray pipelines; + }; + + PipelineCache m_pipelines; + + class Fade { + public: + void prepare(RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u, QRhiRenderPassDescriptor *rpDesc); + void releaseResources(); + void render(QRhiCommandBuffer *cb); + private: + RhiVisualizer *visualizer; + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ubuf = nullptr; + QRhiGraphicsPipeline *ps = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + } m_fade; + + class ChangeVis { + public: + void prepare(Node *n, RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u); + void releaseResources(); + void render(QRhiCommandBuffer *cb); + private: + void gather(Node *n); + RhiVisualizer *visualizer; + QList drawCalls; + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ibuf = nullptr; + QRhiBuffer *ubuf = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + } m_changeVis; + + class BatchVis { + public: + void prepare(const QDataBuffer &opaqueBatches, + const QDataBuffer &alphaBatches, + RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u, + bool forceUintIndex); + void releaseResources(); + void render(QRhiCommandBuffer *cb); + private: + void gather(Batch *b); + RhiVisualizer *visualizer; + bool forceUintIndex; + QList drawCalls; + QRhiBuffer *ubuf = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + } m_batchVis; + + class ClipVis { + public: + void prepare(QSGNode *node, RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u); + void releaseResources(); + void render(QRhiCommandBuffer *cb); + private: + void gather(QSGNode *node); + RhiVisualizer *visualizer; + QList drawCalls; + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ibuf = nullptr; + QRhiBuffer *ubuf = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + } m_clipVis; + + class OverdrawVis { + public: + void prepare(Node *n, RhiVisualizer *visualizer, + QRhi *rhi, QRhiResourceUpdateBatch *u); + void releaseResources(); + void render(QRhiCommandBuffer *cb); + private: + void gather(Node *n); + RhiVisualizer *visualizer; + QList drawCalls; + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ibuf = nullptr; + QRhiBuffer *ubuf = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + float step = 0.0f; + QMatrix4x4 rotation; + struct { + QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ubuf = nullptr; + QRhiShaderResourceBindings *srb = nullptr; + QRhiGraphicsPipeline *ps = nullptr; + } box; + } m_overdrawVis; + + QRandomGenerator m_randomGenerator; + + friend class Fade; + friend class PipelineCache; + friend class ChangeVis; + friend class ClipVis; + friend class OverdrawVis; +}; + +} // namespace WSGBatchRenderer + +WAYLIB_SERVER_END_NAMESPACE + +#endif // WSGRHIVISUALIZER_P_H diff --git a/waylib/src/server/qtquick/scenegraph/wsgviewport.cpp b/waylib/src/server/qtquick/scenegraph/wsgviewport.cpp new file mode 100644 index 0000000000..3e5d45708c --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgviewport.cpp @@ -0,0 +1,116 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "wsgviewport_p.h" + +WAYLIB_SERVER_BEGIN_NAMESPACE + +static bool sameReal(qreal a, qreal b) +{ + return qFuzzyCompare(a + 1.0, b + 1.0); +} + +static bool sameRectF(const QRectF &a, const QRectF &b) +{ + return a.isNull() == b.isNull() + && sameReal(a.x(), b.x()) && sameReal(a.y(), b.y()) + && sameReal(a.width(), b.width()) && sameReal(a.height(), b.height()); +} + +static bool sameMatrix(const QMatrix4x4 &a, const QMatrix4x4 &b) +{ + const float *da = a.constData(); + const float *db = b.constData(); + for (int i = 0; i < 16; ++i) { + if (!qFuzzyCompare(qreal(da[i]) + 1.0, qreal(db[i]) + 1.0)) + return false; + } + return true; +} + +WSGViewport::WSGViewport() = default; + +WSGViewport::~WSGViewport() +{ + for (const auto &entry : m_attachedDatas) { + if (entry.deleter) + entry.deleter(entry.data); + } +} + +int WSGViewport::indexOfAttachedData(const void *owner) const +{ + for (int i = 0; i < m_attachedDatas.count(); ++i) { + if (m_attachedDatas.at(i).owner == owner) + return i; + } + return -1; +} + +void WSGViewport::setRenderParameters(const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, + const QRectF &targetRect) +{ + if (sameMatrix(m_renderMatrix, renderMatrix) + && sameRectF(m_sourceRect, sourceRect) + && sameRectF(m_targetRect, targetRect)) + return; + m_renderMatrix = renderMatrix; + m_sourceRect = sourceRect; + m_targetRect = targetRect; + m_damageRegion = WDamageRegion(true); +} + +const QMatrix4x4 &WSGViewport::renderMatrix() const +{ + return m_renderMatrix; +} + +QRectF WSGViewport::sourceRect() const +{ + return m_sourceRect; +} + +QRectF WSGViewport::targetRect() const +{ + return m_targetRect; +} + +bool WSGViewport::isDirty() const +{ + if (m_damageRegion.isEmpty()) + return false; + if (isFull() || !m_sourceRect.isValid()) + return true; + + bool invertible = false; + const QMatrix4x4 fromSource = m_renderMatrix.inverted(&invertible); + if (!invertible) + return true; + for (int i = 0; i < 16; ++i) { + if (!qIsFinite(fromSource.constData()[i])) + return true; + } + + const QRectF sceneRectF = fromSource.mapRect(m_sourceRect); + if (!qIsFinite(sceneRectF.left()) || !qIsFinite(sceneRectF.top()) + || !qIsFinite(sceneRectF.right()) || !qIsFinite(sceneRectF.bottom())) + return true; + return !(m_damageRegion.region & outerAligned(sceneRectF)).isEmpty(); +} + +void WSGViewport::markFull() +{ + m_damageRegion = WDamageRegion(true); +} + +void WSGViewport::addDamage(const WDamageRegion &damage) +{ + m_damageRegion += damage; +} + +void WSGViewport::finishFrame() +{ + m_damageRegion.clear(); +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/scenegraph/wsgviewport_p.h b/waylib/src/server/qtquick/scenegraph/wsgviewport_p.h new file mode 100644 index 0000000000..ff93b0b624 --- /dev/null +++ b/waylib/src/server/qtquick/scenegraph/wsgviewport_p.h @@ -0,0 +1,123 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +WAYLIB_SERVER_BEGIN_NAMESPACE + +// Per-render-target mapping plus that target's accumulated scene damage. +// WSGDamageTracker::commit(WSGViewport&) copies this round's scene damage +// into every viewport; each viewport keeps it until it presents, so outputs +// sharing one scene at different refresh rates never lose damage. +// Mapping changes (matrix / source / target) mark full: the whole +// buffer contents are invalid. Size and DPR are WBufferRenderer's job. +// Damage stays in scene coordinates; whether it hits this output is decided +// with the current mapping and the buffer pixel size at draw time. +class WAYLIB_SERVER_EXPORT WSGViewport +{ +public: + WSGViewport(); + ~WSGViewport(); + + template + T *getAttachedData(const void *owner) const { + int index = indexOfAttachedData(owner); + if (index < 0) + return nullptr; + return reinterpret_cast(m_attachedDatas.at(index).data); + } + + template + T *getAttachedData() const { + const void *owner = typeid(T).name(); + return getAttachedData(owner); + } + + template + void setAttachedData(const void *owner, void *data, void (*deleter)(void*) = nullptr) { + int index = indexOfAttachedData(owner); + if (index >= 0) { + if (m_attachedDatas[index].deleter && m_attachedDatas[index].data != data) + m_attachedDatas[index].deleter(m_attachedDatas[index].data); + m_attachedDatas[index].data = data; + m_attachedDatas[index].deleter = deleter; + } else { + m_attachedDatas.append({owner, data, deleter}); + } + } + + template + void setAttachedData(void *data, void (*deleter)(void*) = nullptr) { + const void *owner = typeid(T).name(); + setAttachedData(owner, data, deleter); + } + + template + void removeAttachedData(const void *owner) { + int index = indexOfAttachedData(owner); + if (index >= 0) { + if (m_attachedDatas[index].deleter) + m_attachedDatas[index].deleter(m_attachedDatas[index].data); + m_attachedDatas.removeAt(index); + } + } + + template + void removeAttachedData() { + const void *owner = typeid(T).name(); + removeAttachedData(owner); + } + + int indexOfAttachedData(const void *owner) const; + + void setRenderParameters(const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect = {}, + const QRectF &targetRect = {}); + + const QMatrix4x4 &renderMatrix() const; + QRectF sourceRect() const; + QRectF targetRect() const; + + // True when accumulated scene damage intersects this viewport's source + // region. Unbounded or unmappable viewports conservatively report dirty. + bool isDirty() const; + + + const WDamageRegion &damageRegion() const + { + return m_damageRegion; + } + + bool isFull() const + { + return m_damageRegion.isFull; + } + + void markFull(); + void addDamage(const WDamageRegion &damage); + void finishFrame(); + +private: + QMatrix4x4 m_renderMatrix; + QRectF m_sourceRect; + QRectF m_targetRect; + WDamageRegion m_damageRegion; + + struct AttachedEntry { + const void *owner = nullptr; + void *data = nullptr; + void (*deleter)(void*) = nullptr; + }; + QList m_attachedDatas; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index da5eac2af7..78b87f5814 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -20,6 +20,13 @@ #include "winputdevice.h" #include "wseat.h" #include "wayliblogging.h" +#include "wsgcontext_p.h" +#include "wregionhighlightitem.h" +#include "wsgdamagenode_p.h" +#include "wsgdamagetracker_p.h" +#include "wrenderbuffernode_p.h" +#include "wsgbatchrenderer_p.h" +#include "private/wprivateaccessor_p.h" #include "platformplugin/qwlrootsintegration.h" #include "platformplugin/qwlrootscreen.h" @@ -40,19 +47,14 @@ #include #include #include -#include "private/wprivateaccessor_p.h" #include #include -#include #include #include #include #include #include -#include #include -#include -#include #include #include @@ -123,10 +125,15 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper : layer(l) , wlrLayer(layer) , contentsIsDirty(true) + , hasCommittedPresentation(false) + , lastCommittedEnabled(false) { } ~LayerData() { + if (debugRectangle) + delete debugRectangle; + if (renderer) { QObject::disconnect(rendererConnection); renderer->deleteLater(); @@ -145,14 +152,18 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper // dirty state uint contentsIsDirty:1; + uint hasCommittedPresentation:1; + uint lastCommittedEnabled:1; // end QRectF mapRect; QRectF noClipMapRect; QRect mapToOutput; + QRect lastCommittedMapToOutput; QSize pixelSize; QMatrix4x4 renderMatrix; - + std::shared_ptr viewport = std::make_shared(); + QPointer debugRectangle; // for proxy LayerData *mapFromLayer = nullptr; // check mapFrom before use QPointer mapFrom; @@ -168,7 +179,7 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper ~OutputHelper() { - if (!m_layerProxys.isEmpty() || m_output || m_output2 || m_layerPorxyContainer + if (!m_layerProxys.isEmpty() || m_output || m_layerCompositor || m_layerPorxyContainer || m_cursorRenderer || m_cursorLayerProxy) qFatal("Before destroying OutputHelper, ensure call invalidate method."); } @@ -195,9 +206,9 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper return WOutputViewportPrivate::get(m_output)->bufferRenderer; } - inline WBufferRenderer *bufferRenderer2() const { - Q_ASSERT(m_output2); - return WOutputViewportPrivate::get(m_output2)->bufferRenderer; + inline bool damageDebugNeedsFrame() const { + return m_highlight && m_highlight->overlay() + && m_highlight->overlay()->needsAnotherFrame(); } inline const QList &layers() const { @@ -205,6 +216,8 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper } inline void invalidate() { + delete m_highlight; + m_highlight = nullptr; m_output = nullptr; cleanLayerCompositor(); cleanCursorRender(); @@ -224,27 +237,44 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper void sortLayers(); void cleanLayerCompositor(); void cleanCursorRender(); - - inline wlr_buffer *beginRender(WBufferRenderer *renderer, - const QSize &pixelSize, uint32_t format, - WBufferRenderer::RenderFlags flags, - WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); - inline void render(WBufferRenderer *renderer, int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect, const QRectF &viewportRect); - + void ensureHighlight(); + + wlr_buffer *render(WBufferRenderer *renderer, + const QSize &pixelSize, uint32_t format, + WBufferRenderer::RenderFlags flags, + WGlobal::ColorContentsMode mode, + const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect = {}, + const QRectF &targetRect = {}, + qreal dpr = 0); + wlr_buffer *render(WBufferRenderer *renderer, uint32_t format, + WBufferRenderer::RenderFlags flags = {}, + WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); + // Feeds the settled cycle flush to the damage highlight overlay. Call + // after the output renderer's endRender(). + inline void addHighlightFrame(WBufferRenderer *renderer) { + if (m_highlight) + m_highlight->addFrame(renderer->lastRenderRegion()); + } static bool visualizeLayers() { static bool on = qEnvironmentVariableIsSet("WAYLIB_VISUALIZE_LAYERS"); return on; } wlr_buffer *renderLayer(LayerData *layer, bool *dontEndRenderAndReturnNeedsEndRender); + bool ensureLayerRenderer(LayerData *layer); + bool updateLayerGeometry(LayerData *layer, qreal *dpr); + bool layerPresentationChanged(const LayerData *layer) const; + void snapshotLayerPresentation(); + bool inspectNeedsFrame(); WBufferRenderer *afterRender(); - WBufferRenderer *compositeLayers(const QList layers, bool forceShadowRenderer); + WBufferRenderer *compositeLayers(const QList layers); bool commit(WBufferRenderer *buffer); bool tryToHardwareCursor(const LayerData *layer); -private: WOutputViewport *m_output = nullptr; + QPointer m_highlight; + std::shared_ptr m_cursorViewport = std::make_shared(); QList m_layers; WBufferRenderer *m_lastCommitBuffer = nullptr; // only for render cursor @@ -253,8 +283,9 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper bool m_cursorDirty = false; bool m_hardwareCursorRenderComplete = false; - // for compositeLayers - QPointer m_output2; + // for compositeLayers: own WSGViewport, not shared with the output renderer + QPointer m_layerCompositor; + std::shared_ptr m_layerCompositorViewport = std::make_shared(); QPointer m_layerPorxyContainer; QList> m_layerProxys; }; @@ -349,8 +380,7 @@ class Q_DECL_HIDDEN OutputLayer Rejected, }; - State state; - + State state = Normal; QList m_outputs; }; @@ -389,6 +419,14 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate return nullptr; return outputs.at(index); } + OutputHelper *helperForWlrOutput(wlr_output *native) const { + for (auto *helper : std::as_const(outputs)) { + if (helper->output() == native) + return helper; + } + return nullptr; + } + int indexOfOutputLayer(const WOutputLayer *layer) const; inline OutputLayer *getOutputLayer(WOutputLayer *layer) const { @@ -430,11 +468,9 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate void updateSceneDPR(); void sortOutputs(); - QVector> - doRenderOutputs(wlr_output *needsFrameOutput, const QList &outputs, - bool forceRender); - void doRender(wlr_output *needsFrameOutput, const QList &outputs, - bool forceRender, bool doCommit); + void doRender(OutputHelper *helper, bool forceRender, bool doCommit); + void requestSceneInspect(); + void inspectSceneAndScheduleFrames(); inline void pushRenderer(WBufferRenderer *renderer) { rendererList.push(renderer); @@ -456,6 +492,9 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate bool componentCompleted = true; bool inRendering = false; + bool inInspect = false; + bool inspectPosted = false; + bool sceneInspectNeeded = false; bool renderEnabled = true; WPointer m_renderer; @@ -464,6 +503,7 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate QList outputs; QList layers; bool disableLayers = false; + WOutputRenderWindow::DamageVisual damageVisual = WOutputRenderWindow::DamageVisual::Off; QOpenGLContext *glContext = nullptr; #ifdef ENABLE_VULKAN_RENDER @@ -488,6 +528,25 @@ void OutputHelper::updateSceneDPR() WOutputRenderWindowPrivate::get(renderWindow())->updateSceneDPR(); } +void OutputHelper::ensureHighlight() +{ + if (!m_output) + return; + if (renderWindow()->damageVisual() != WOutputRenderWindow::DamageVisual::Highlight) + return; + if (WOutputViewportPrivate::get(m_output)->offscreen) + return; + QQuickItem *host = m_output->parentItem(); + if (!host) + return; + if (m_highlight) { + if (m_highlight->parentItem() != host) + m_highlight->setParentItem(host); + return; + } + m_highlight = new WRegionHighlightItem(m_output, host); +} + int OutputHelper::indexOfLayer(OutputLayer *layer) const { for (int i = 0; i < m_layers.count(); ++i) @@ -559,15 +618,9 @@ void OutputHelper::cleanLayerCompositor() proxy->setRenderer(nullptr); } - if (m_output2) { - m_output2->deleteLater(); - m_output2 = nullptr; - } - - if (m_output) { - auto d = WOutputViewportPrivate::get(m_output); - if (!d->inDestructor) - d->setExtraRenderSource(nullptr); + if (m_layerCompositor) { + m_layerCompositor->deleteLater(); + m_layerCompositor = nullptr; } if (m_layerPorxyContainer) { @@ -585,19 +638,30 @@ void OutputHelper::cleanCursorRender() } } -wlr_buffer *OutputHelper::beginRender(WBufferRenderer *renderer, - const QSize &pixelSize, uint32_t format, - WBufferRenderer::RenderFlags flags, - WGlobal::ColorContentsMode mode) +wlr_buffer *OutputHelper::render(WBufferRenderer *renderer, + const QSize &pixelSize, uint32_t format, + WBufferRenderer::RenderFlags flags, + WGlobal::ColorContentsMode mode, + const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, + const QRectF &targetRect, + qreal dpr) { - return renderer->beginRender(pixelSize, devicePixelRatio(), format, flags, mode); + renderer->setPixelSize(pixelSize); + renderer->setDevicePixelRatio(dpr > 0 ? dpr : devicePixelRatio()); + if (auto viewport = renderer->viewport().lock()) + viewport->setRenderParameters(renderMatrix, sourceRect, targetRect); + renderWindowD()->pushRenderer(renderer); + return renderer->render(format, flags, mode); } - -void OutputHelper::render(WBufferRenderer *renderer, int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect, const QRectF &targetRect) +wlr_buffer *OutputHelper::render(WBufferRenderer *renderer, uint32_t format, + WBufferRenderer::RenderFlags flags, + WGlobal::ColorContentsMode mode) { - renderWindowD()->pushRenderer(renderer); - renderer->render(sourceIndex, renderMatrix, sourceRect, targetRect); + return render(renderer, outputViewport()->output()->size(), format, flags, mode, + m_output->renderMatrix(), + m_output->effectiveSourceRect(), + m_output->targetRect()); } static QQuickItem *createVisualRectangle(QQuickItem *target, const QColor &color) { @@ -606,7 +670,6 @@ static QQuickItem *createVisualRectangle(QQuickItem *target, const QColor &color rectangle->border()->setWidth(1); rectangle->setColor(Qt::transparent); QQuickItemPrivate::get(rectangle)->anchors()->setFill(target); - return rectangle; } @@ -616,138 +679,222 @@ static inline QRectF scaleRect(const QRectF &r, qreal xScale, qreal yScale) { return QRectF(r.x() * xScale, r.y() * yScale, r.width() * xScale, r.height() * yScale); } -wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndReturnNeedsEndRender) +bool OutputHelper::ensureLayerRenderer(LayerData *layer) { auto source = layer->layer->layer->parent(); if (!source->parentItem() || source->window() != renderWindow()) - return nullptr; + return false; if (!layer->renderer) { layer->renderer = new WBufferRenderer(source); - if (visualizeLayers()) - layer->renderer->setClearColor(Qt::yellow); - - QList sourceList {source}; if (visualizeLayers()) { - auto rectangle = createVisualRectangle(source, Qt::green); - QQuickItemPrivate::get(rectangle)->refFromEffectItem(true); - sourceList << rectangle; + layer->renderer->setClearColor(Qt::yellow); + layer->debugRectangle = createVisualRectangle(source, Qt::green); } - layer->renderer->setSourceList(sourceList, false); + layer->renderer->setViewport(layer->viewport, source, false); layer->renderer->setOutput(outputViewport()->output()); - // for the new WBufferRenderer and createVisualRectangle renderWindowD()->updateDirtyNodes(); layer->rendererConnection = connect(layer->renderer, &WBufferRenderer::sceneGraphChanged, this, [layer] { layer->contentsIsDirty = true; }); } + return true; +} + +bool OutputHelper::updateLayerGeometry(LayerData *layer, qreal *dprOut) +{ + auto source = layer->layer->layer->parent(); + if (!source->parentItem() || source->window() != renderWindow()) { + layer->mapToOutput = {}; + return false; + } qreal dpr = devicePixelRatio(); - { - QRectF mapRect, noClipMapRect; - // matrix function: map source to WOutputViewport - QMatrix4x4 viewportMatrix; - - const auto layerFlags = layer->layer->layer->flags(); - const bool sizeSensitive = layerFlags & WOutputLayer::SizeSensitive; - const bool isRef = layer->mapFrom && layer->mapTo; - if (isRef) { - viewportMatrix = outputViewport()->mapToViewport(layer->mapTo); - const auto xScale = layer->mapTo->width() / layer->mapFrom->outputViewport()->width(); - const auto yScale = layer->mapTo->height() / layer->mapFrom->outputViewport()->height(); - - // geometry relative the other output buffer - noClipMapRect = scaleRect(layer->mapFromLayer->noClipMapRect, xScale, yScale); - if (sizeSensitive) { - mapRect = scaleRect(layer->mapFromLayer->mapRect, xScale, yScale); - } else { - mapRect = noClipMapRect; - } - } else { - viewportMatrix = outputViewport()->mapToViewport(source->parentItem()); + QRectF mapRect, noClipMapRect; + QMatrix4x4 viewportMatrix; + + const auto layerFlags = layer->layer->layer->flags(); + const bool sizeSensitive = layerFlags & WOutputLayer::SizeSensitive; + const bool isRef = layer->mapFrom && layer->mapTo; + if (isRef) { + viewportMatrix = outputViewport()->mapToViewport(layer->mapTo); + const auto xScale = layer->mapTo->width() / layer->mapFrom->outputViewport()->width(); + const auto yScale = layer->mapTo->height() / layer->mapFrom->outputViewport()->height(); - // geometry relative source's parent - noClipMapRect = QRectF(source->position(), source->size()); + noClipMapRect = scaleRect(layer->mapFromLayer->noClipMapRect, xScale, yScale); + if (sizeSensitive) { + mapRect = scaleRect(layer->mapFromLayer->mapRect, xScale, yScale); + } else { mapRect = noClipMapRect; } + } else { + viewportMatrix = outputViewport()->mapToViewport(source->parentItem()); - // matrix function: map source to output buffer - const auto outputMatrix = viewportMatrix * outputViewport()->sourceRectToTargetRectTransfrom(); - noClipMapRect = outputMatrix.mapRect(noClipMapRect); - mapRect = outputMatrix.mapRect(mapRect); - - QTransform revertScaleTransform; - if (!sizeSensitive) { - const auto scaledPoint1 = viewportMatrix.map(QPointF(0, 0)); - const auto scaledPoint2 = viewportMatrix.map(QPointF(1, 1)) - scaledPoint1; - const auto xScale = 1.0 / std::abs(scaledPoint2.x()); - const auto yScale = 1.0 / std::abs(scaledPoint2.y()); - - if (xScale != 1 || yScale != 1) { - revertScaleTransform.scale(xScale, yScale); - noClipMapRect.setSize(revertScaleTransform.mapRect(noClipMapRect).size()); - mapRect.setSize(revertScaleTransform.mapRect(mapRect).size()); - } - } else if (layer->mapFrom) { - // This layer's size is strict mode, needs follow the map source's DPR. - dpr = layer->mapFrom->devicePixelRatio(); - } + noClipMapRect = QRectF(source->position(), source->size()); + mapRect = noClipMapRect; + } - // clip to WOutputViewport - mapRect = mapRect & QRectF(QPointF(0, 0), outputViewport()->size()); + const auto outputMatrix = viewportMatrix * outputViewport()->sourceRectToTargetRectTransfrom(); + noClipMapRect = outputMatrix.mapRect(noClipMapRect); + mapRect = outputMatrix.mapRect(mapRect); - QSize pixelSize; - const auto tmpSize = mapRect.size() * dpr; + QTransform revertScaleTransform; + if (!sizeSensitive) { + const auto scaledPoint1 = viewportMatrix.map(QPointF(0, 0)); + const auto scaledPoint2 = viewportMatrix.map(QPointF(1, 1)) - scaledPoint1; + const auto xScale = 1.0 / std::abs(scaledPoint2.x()); + const auto yScale = 1.0 / std::abs(scaledPoint2.y()); - if (layerFlags & WOutputLayer::DontClip) { - pixelSize.rwidth() = qCeil(tmpSize.width()); - pixelSize.rheight() = qCeil(tmpSize.height()); - } else { - // Limitation max buffer - const auto maxSize = qMax(source->width(), source->height()) * dpr; - pixelSize.rwidth() = qCeil(qMin(tmpSize.width(), maxSize)); - pixelSize.rheight() = qCeil(qMin(tmpSize.height(), maxSize)); + if (xScale != 1 || yScale != 1) { + revertScaleTransform.scale(xScale, yScale); + noClipMapRect.setSize(revertScaleTransform.mapRect(noClipMapRect).size()); + mapRect.setSize(revertScaleTransform.mapRect(mapRect).size()); } + } else if (layer->mapFrom) { + dpr = layer->mapFrom->devicePixelRatio(); + } - if (mapRect.isEmpty()) { - return nullptr; - } - Q_ASSERT(!pixelSize.isEmpty()); + mapRect = mapRect & QRectF(QPointF(0, 0), outputViewport()->size()); - QMatrix4x4 renderMatrix = revertScaleTransform * viewportMatrix; - if (isRef) { - renderMatrix = layer->mapFromLayer->renderMatrix * renderMatrix; - } + QSize pixelSize; + const auto tmpSize = mapRect.size() * dpr; - // viewportMatrix is relative of the output buffer, but the layer - // render buffer's pixelSize is not same as the output buffer, so - // needs reset the x,y translate relative the render buffer of the layer. - if (!renderMatrix.isIdentity()) { - const auto tmp = renderMatrix.mapRect(QRectF(QPointF(0, 0), source->size())); - renderMatrix(0, 3) -= tmp.x(); - renderMatrix(1, 3) -= tmp.y(); - } + if (layerFlags & WOutputLayer::DontClip) { + pixelSize.rwidth() = qCeil(tmpSize.width()); + pixelSize.rheight() = qCeil(tmpSize.height()); + } else { + const auto maxSize = qMax(source->width(), source->height()) * dpr; + pixelSize.rwidth() = qCeil(qMin(tmpSize.width(), maxSize)); + pixelSize.rheight() = qCeil(qMin(tmpSize.height(), maxSize)); + } - std::swap(mapRect, layer->mapRect); - std::swap(noClipMapRect, layer->noClipMapRect); - std::swap(pixelSize, layer->pixelSize); - std::swap(renderMatrix, layer->renderMatrix); + if (mapRect.isEmpty()) { + layer->mapRect = {}; + layer->mapToOutput = {}; + if (dprOut) + *dprOut = dpr; + return false; + } + Q_ASSERT(!pixelSize.isEmpty()); - if (layer->pixelSize != pixelSize - || layer->mapRect.size() != mapRect.size() - || layer->renderMatrix != renderMatrix) { - layer->contentsIsDirty = true; - } + QMatrix4x4 renderMatrix = revertScaleTransform * viewportMatrix; + if (isRef) { + renderMatrix = layer->mapFromLayer->renderMatrix * renderMatrix; + } + + if (!renderMatrix.isIdentity()) { + const auto tmp = renderMatrix.mapRect(QRectF(QPointF(0, 0), source->size())); + renderMatrix(0, 3) -= tmp.x(); + renderMatrix(1, 3) -= tmp.y(); + } + + std::swap(mapRect, layer->mapRect); + std::swap(noClipMapRect, layer->noClipMapRect); + std::swap(pixelSize, layer->pixelSize); + std::swap(renderMatrix, layer->renderMatrix); + + if (layer->pixelSize != pixelSize + || layer->mapRect.size() != mapRect.size() + || layer->renderMatrix != renderMatrix) { + layer->contentsIsDirty = true; } layer->mapToOutput = QRect((layer->mapRect.topLeft() * dpr).toPoint(), layer->pixelSize); + if (dprOut) + *dprOut = dpr; + return true; +} + +bool OutputHelper::layerPresentationChanged(const LayerData *layer) const +{ + if (!layer->hasCommittedPresentation) + return true; + if (bool(layer->lastCommittedEnabled) != layer->layer->isEnabled()) + return true; + return layer->lastCommittedMapToOutput != layer->mapToOutput; +} + +void OutputHelper::snapshotLayerPresentation() +{ + for (auto *layer : std::as_const(m_layers)) { + layer->hasCommittedPresentation = true; + layer->lastCommittedEnabled = layer->layer->isEnabled(); + layer->lastCommittedMapToOutput = layer->mapToOutput; + } +} + +bool OutputHelper::inspectNeedsFrame() +{ + if (extraState() || output()->attach_render_locks > 0) + return true; + + auto *primary = bufferRenderer(); + primary->setPixelSize(outputViewport()->output()->size()); + primary->setDevicePixelRatio(devicePixelRatio()); + if (auto viewport = primary->viewport().lock()) { + viewport->setRenderParameters(m_output->renderMatrix(), + m_output->effectiveSourceRect(), + m_output->targetRect()); + } + if (primary->settleDamage()) + return true; + + for (auto *layer : std::as_const(m_layers)) { + if (!layer->layer->isEnabled()) { + if (layerPresentationChanged(layer)) + return true; + continue; + } + if (!ensureLayerRenderer(layer)) { + if (layerPresentationChanged(layer)) + return true; + continue; + } + qreal dpr = devicePixelRatio(); + const bool mapped = updateLayerGeometry(layer, &dpr); + if (layerPresentationChanged(layer) || layer->contentsIsDirty) + return true; + if (!mapped) + continue; + + const QRectF sr(layer->mapRect.topLeft() - layer->noClipMapRect.topLeft(), + layer->mapRect.size()); + const QRectF tr(QPointF(0, 0), layer->mapRect.size()); + layer->renderer->setPixelSize(layer->pixelSize); + layer->renderer->setDevicePixelRatio(dpr); + if (auto viewport = layer->renderer->viewport().lock()) + viewport->setRenderParameters(layer->renderMatrix, sr, tr); + if (layer->renderer->settleDamage()) + return true; + } + + return false; +} + +wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndReturnNeedsEndRender) +{ + if (!ensureLayerRenderer(layer)) + return nullptr; + + qreal dpr = devicePixelRatio(); + if (!updateLayerGeometry(layer, &dpr)) + return nullptr; + auto buffer = layer->renderer->lastBuffer(); - if (!buffer || layer->contentsIsDirty) { + const QRectF sr = QRectF(layer->mapRect.topLeft() - layer->noClipMapRect.topLeft(), layer->mapRect.size()); + const QRectF tr(QPointF(0, 0), layer->mapRect.size()); + layer->renderer->setPixelSize(layer->pixelSize); + layer->renderer->setDevicePixelRatio(dpr); + if (auto viewport = layer->renderer->viewport().lock()) + viewport->setRenderParameters(layer->renderMatrix, sr, tr); + + const bool redrawLayer = !buffer || layer->contentsIsDirty || layer->renderer->settleDamage(); + if (redrawLayer) { layer->renderer->setSize(layer->pixelSize / dpr); const bool alpha = !layer->layer->layer->flags().testFlag(WOutputLayer::NoAlpha); @@ -759,21 +906,11 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe } mode = WGlobal::ColorContentsMode::Preserve; } - // Don't use OutputHelper::beginRender, because the dpr maybe is from LayerData::mapFrom - buffer = layer->renderer->beginRender(layer->pixelSize, dpr, - // TODO: Allows control format by WOutputLayer - alpha ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888, - WBufferRenderer::DontConfigureSwapchain, - mode); + buffer = render(layer->renderer, layer->pixelSize, + alpha ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888, + WBufferRenderer::DontConfigureSwapchain, mode, + layer->renderMatrix, sr, tr, dpr); if (buffer) { - const QRectF sr = QRectF(layer->mapRect.topLeft() - layer->noClipMapRect.topLeft(), layer->mapRect.size()); - const QRectF tr(QPointF(0, 0), layer->mapRect.size()); - - render(layer->renderer, 0, layer->renderMatrix, sr, tr); - - if (visualizeLayers()) - render(layer->renderer, 1, layer->renderMatrix, sr, tr); - if (dontEndRenderAndReturnNeedsEndRender) { *dontEndRenderAndReturnNeedsEndRender = true; } else { @@ -800,6 +937,9 @@ typedef QScopedPointer wl_array_pointer; WBufferRenderer *OutputHelper::afterRender() { + ensureHighlight(); + if (m_highlight) + m_highlight->sync(); if (m_layers.isEmpty()) { cleanLayerCompositor(); return bufferRenderer(); @@ -963,59 +1103,42 @@ WBufferRenderer *OutputHelper::afterRender() return bufferRenderer(); } - return compositeLayers(needsCompositeLayers, forceShadowRender); + return compositeLayers(needsCompositeLayers); } -#define PRIVATE_WOutputViewport "__private_WOutputViewport" -WBufferRenderer *OutputHelper::compositeLayers(const QList layers, bool forceShadowRenderer) +WBufferRenderer *OutputHelper::compositeLayers(const QList layers) { Q_ASSERT(!layers.isEmpty()); - const bool usingShadowRenderer = forceShadowRenderer - || !bufferRenderer()->isColorPreserved(); - if (!m_layerPorxyContainer) { m_layerPorxyContainer = new QQuickItem(renderWindow()->contentItem()); } - WOutputViewport *output; - - if (usingShadowRenderer) { - if (!m_output2) { - m_output2 = new WOutputViewport(m_output); - m_output2->setObjectName(PRIVATE_WOutputViewport); - m_output2->setOutput(m_output->output()); - bufferRenderer2()->setSourceList({m_layerPorxyContainer.get()}, true); - } - - m_output2->setSize(m_output->size()); - m_output2->setDevicePixelRatio(m_output->devicePixelRatio()); - output = m_output2; - - if (m_layerProxys.size() <= layers.size()) - m_layerProxys.reserve(layers.size() + 1); - - if (m_layerProxys.isEmpty()) - m_layerProxys.append(new BufferRendererProxy(m_layerPorxyContainer)); + if (!m_layerCompositor) { + m_layerCompositor = new WBufferRenderer(renderWindow()->contentItem()); + m_layerCompositor->setViewport(m_layerCompositorViewport, + m_layerPorxyContainer.get(), true); + m_layerCompositor->setOutput(m_output->output()); + m_layerCompositor->setVisible(false); + } - auto outputProxy = m_layerProxys.first(); - outputProxy->setRenderer(bufferRenderer()); - outputProxy->setSize(output->size()); - outputProxy->setPosition({0, 0}); - outputProxy->setZ(0); - } else { - output = m_output; + const QSizeF outputSize = m_output->size(); + if (m_layerProxys.size() <= layers.size()) + m_layerProxys.reserve(layers.size() + 1); - if (m_layerProxys.size() < layers.size()) - m_layerProxys.reserve(layers.size()); + if (m_layerProxys.isEmpty()) + m_layerProxys.append(new BufferRendererProxy(m_layerPorxyContainer)); - WOutputViewportPrivate::get(output)->setExtraRenderSource(m_layerPorxyContainer); - } + auto outputProxy = m_layerProxys.first(); + outputProxy->setRenderer(bufferRenderer()); + outputProxy->setSize(outputSize); + outputProxy->setPosition({0, 0}); + outputProxy->setZ(0); - m_layerPorxyContainer->setSize(output->size()); + m_layerPorxyContainer->setSize(outputSize); for (int i = 0; i < layers.count(); ++i) { - const int j = i + (usingShadowRenderer ? 1 : 0); + const int j = i + 1; BufferRendererProxy *proxy = nullptr; if (j < m_layerProxys.size()) { proxy = m_layerProxys.at(j); @@ -1034,7 +1157,7 @@ WBufferRenderer *OutputHelper::compositeLayers(const QList layers, b } // Clean - for (int i = layers.count() + (usingShadowRenderer ? 1 : 0); i < m_layerProxys.count(); ++i) { + for (int i = layers.count() + 1; i < m_layerProxys.count(); ++i) { auto proxy = m_layerProxys.takeAt(i); proxy->setVisible(false); proxy->deleteLater(); @@ -1043,29 +1166,18 @@ WBufferRenderer *OutputHelper::compositeLayers(const QList layers, b // for the new QQuickItem renderWindowD()->updateDirtyNodes(); - if (usingShadowRenderer) { - const bool ok = beginRender(bufferRenderer2(), m_output->output()->size(), - this->output()->render_format, - WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject, - WGlobal::ColorContentsMode::Preserve); - - if (ok) { - // stop primary render - if (bufferRenderer()->currentBuffer()) - bufferRenderer()->endRender(); - render(bufferRenderer2(), 0, {}, m_output->effectiveSourceRect(), m_output->targetRect()); + const bool ok = render(m_layerCompositor, m_output->output()->size(), + this->output()->render_format, + WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject + | WBufferRenderer::ForceRender, + WGlobal::ColorContentsMode::Preserve, + {}, m_output->effectiveSourceRect(), + m_output->targetRect()); - return bufferRenderer2(); - } - } else { - if (bufferRenderer()->currentBuffer()) { - render(bufferRenderer(), 1, {}, m_output->effectiveSourceRect(), m_output->targetRect()); - } else { - // ###(zccrs): Maybe because contents is not dirty, so not do render - // in WOutputRenderWindowPrivate::doRenderOutputs, force mark the - // contents to dirty here to ensure can render layers in the next frame. - update(); - } + if (ok) { + if (bufferRenderer()->currentBuffer()) + bufferRenderer()->endRender(); + return m_layerCompositor; } return bufferRenderer(); @@ -1076,21 +1188,25 @@ bool OutputHelper::commit(WBufferRenderer *buffer) if (outputViewport()->offscreen()) return true; + bool ok = false; if (!buffer || !buffer->currentBuffer()) { Q_ASSERT(!this->buffer()); - return WOutputHelper::commit(); - } + ok = WOutputHelper::commit(); + } else { + setBuffer(buffer->currentBuffer()); - setBuffer(buffer->currentBuffer()); + if (m_lastCommitBuffer == buffer) { + const WDamageRegion &damageRegion = buffer->lastFlushRegion(); + if (!damageRegion.isFull) + setDamage(damageRegion.region); + } - if (m_lastCommitBuffer == buffer) { - if (pixman_region32_not_empty(&buffer->damageRing()->current)) - setDamage(&buffer->damageRing()->current); + m_lastCommitBuffer = buffer; + ok = WOutputHelper::commit(); } - - m_lastCommitBuffer = buffer; - - return WOutputHelper::commit(); + if (ok) + snapshotLayerPresentation(); + return ok; } bool OutputHelper::tryToHardwareCursor(const LayerData *layer) @@ -1180,7 +1296,7 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) if (visualizeLayers()) m_cursorRenderer->setClearColor(Qt::cyan); m_cursorLayerProxy = new BufferRendererProxy(m_cursorRenderer); - m_cursorRenderer->setSourceList({m_cursorLayerProxy}, false); + m_cursorRenderer->setViewport(m_cursorViewport, m_cursorLayerProxy, false); m_cursorRenderer->setOutput(m_output->output()); m_cursorRenderer->setVisible(false); // for the new WBufferRenderer and WQuickTextureProxy @@ -1203,12 +1319,13 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) if (m_cursorDirty || !newBuffer || (pixelSize.width() != newBuffer->width) || (pixelSize.height() != newBuffer->height)) { - newBuffer = m_cursorRenderer->beginRender(pixelSize, 1.0, DRM_FORMAT_ARGB8888, - WBufferRenderer::UseCursorFormats); - if (newBuffer) { - m_cursorRenderer->render(0, {}); + newBuffer = render(m_cursorRenderer, pixelSize, DRM_FORMAT_ARGB8888, + WBufferRenderer::UseCursorFormats, + WGlobal::ColorContentsMode::DontCare, + {}, {}, {}, 1.0); + if (newBuffer) m_cursorRenderer->endRender(); - } + m_cursorDirty = false; } @@ -1311,15 +1428,13 @@ void WOutputRenderWindowPrivate::init() 6. QQuickRenderControlPrivate::maybeUpdate 7. QQuickRenderControl::sceneChanged */ - // TODO: Get damage regions from the Qt, and use WOutputDamage::add instead of WOutput::update. + // Qt scene notifications request an inspect, not an output commit. + // Inspect settles damage and layer presentation, then schedule_frame + // only for outputs that actually need a commit. QObject::connect(rc(), &QQuickRenderControl::renderRequested, - q, qOverload<>(&WOutputRenderWindow::update)); + q, [this] { requestSceneInspect(); }); QObject::connect(rc(), &QQuickRenderControl::sceneChanged, - q, [q, this] { - if (inRendering) - return; - q->update(); - }); + q, [this] { requestSceneInspect(); }); // for WSeat::filterUnacceptedEvent auto eventJunkman = new WEventJunkman(contentItem); @@ -1446,82 +1561,6 @@ void WOutputRenderWindowPrivate::sortOutputs() }); } -QVector> -WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const QList &outputs, - bool forceRender) -{ - QVector renderResults; - renderResults.reserve(outputs.size()); - for (OutputHelper *helper : std::as_const(outputs)) { - if (Q_LIKELY(needsFrameOutput)) { - if (helper->output() != needsFrameOutput) - continue; - else - Q_ASSERT(!helper->framePending()); - } - - if (Q_LIKELY(!forceRender)) { - if (helper->framePending()) - continue; - - // Render if output will be enabled OR has extraState to commit - // Note: Even when disabling, we need to render once to commit the disabled state - // (extraState will contain the ENABLED=false change) - bool shouldRender = helper->willBeEnabled() || helper->extraState(); - if (Q_UNLIKELY(!WOutputViewportPrivate::get(helper->outputViewport())->renderable()) - || !shouldRender) - continue; - - if (!(helper->needsFrame() || helper->contentIsDirty())) - continue; - - // Capture sessions (ext-image-copy-capture etc.) lock the output - // via wlr_output_lock_attach_render() and need a buffer commit to - // complete, even if the content didn't change. - bool captureLocked = helper->output()->attach_render_locks > 0; - if (!helper->contentIsDirty() && !captureLocked) { - renderResults.append(helper); - continue; - } - } - - Q_ASSERT(helper->outputViewport()->output()->scale() <= helper->outputViewport()->devicePixelRatio()); - - const auto &format = helper->output()->render_format; - const auto renderMatrix = helper->outputViewport()->renderMatrix(); - - // maybe using the other WOutputViewport's QSGTextureProvider - if (!helper->outputViewport()->depends().isEmpty()) - updateDirtyNodes(); - - wlr_buffer *buffer = helper->beginRender(helper->bufferRenderer(), helper->outputViewport()->output()->size(), format, - WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject, - helper->outputViewport()->colorContentsMode()); - Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); - if (buffer) { - helper->render(helper->bufferRenderer(), 0, renderMatrix, - helper->outputViewport()->effectiveSourceRect(), - helper->outputViewport()->targetRect()); - } - renderResults.append(helper); - } - - QVector> needsCommit; - needsCommit.reserve(renderResults.size()); - for (auto helper : std::as_const(renderResults)) { - auto bufferRenderer = helper->afterRender(); - // A forced render may not acquire a new buffer, while an external - // output state transaction is still pending. WOutputHelper::commit() - // handles a null renderer by committing that state-only update. - if (bufferRenderer || helper->extraState()) - needsCommit.append({helper, bufferRenderer}); - } - - rendererList.clear(); - - return needsCommit; -} - // ###: QQuickAnimatorController::advance symbol not export static void QQuickAnimatorController_advance(QQuickAnimatorController *ac) { @@ -1540,33 +1579,120 @@ static void QQuickAnimatorController_advance(QQuickAnimatorController *ac) W_PRIVATE_MEMBER(*ac, QQuickAnimCtrl_m_window_tag{})->update(); } -void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, - const QList &outputs, - bool forceRender, bool doCommit) +void WOutputRenderWindowPrivate::requestSceneInspect() { + sceneInspectNeeded = true; + if (inRendering || inInspect) + return; + if (inspectPosted) + return; + inspectPosted = true; + QMetaObject::invokeMethod(q_func(), [this] { + inspectPosted = false; + if (sceneInspectNeeded) + inspectSceneAndScheduleFrames(); + }, Qt::QueuedConnection); +} + +void WOutputRenderWindowPrivate::inspectSceneAndScheduleFrames() +{ + if (!isInitialized() || inRendering || !renderEnabled) + return; + + sceneInspectNeeded = false; + inInspect = true; + + W_Q(WOutputRenderWindow); + for (OutputLayer *layer : std::as_const(layers)) + layer->beforeRender(q); + + rc()->polishItems(); + const bool rhi = QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi()); + if (rhi) + rc()->beginFrame(); + rc()->sync(); + QQuickAnimatorController_advance(animationController.get()); + + for (auto *helper : std::as_const(outputs)) { + auto *outputPrivate = WOutputViewportPrivate::get(helper->outputViewport()); + if (!helper->extraState()) { + if (Q_UNLIKELY(!outputPrivate->renderable()) || !helper->willBeEnabled()) + continue; + } + if (helper->inspectNeedsFrame()) + helper->scheduleFrame(); + } + + if (rhi) + rc()->endFrame(); + resetGlState(); + if (glContext) + glContext->doneCurrent(); + + inInspect = false; + if (sceneInspectNeeded) + requestSceneInspect(); +} + + +void WOutputRenderWindowPrivate::doRender(OutputHelper *helper, bool forceRender, bool doCommit) +{ + Q_ASSERT(helper); Q_ASSERT(rendererList.isEmpty()); Q_ASSERT(!inRendering); if (!renderEnabled) return; + if (sceneInspectNeeded) + inspectSceneAndScheduleFrames(); + + auto *outputPrivate = WOutputViewportPrivate::get(helper->outputViewport()); + const bool captureLocked = helper->output()->attach_render_locks > 0; + + if (Q_LIKELY(!forceRender)) { + if (helper->framePending()) + return; + + const bool shouldRender = helper->willBeEnabled() || helper->extraState(); + if (Q_UNLIKELY(!outputPrivate->renderable()) || !shouldRender) + return; + + if (!helper->needsFrame() && !helper->extraState() && !captureLocked) + return; + } + inRendering = true; W_Q(WOutputRenderWindow); - for (OutputLayer *layer : std::as_const(layers)) { + for (OutputLayer *layer : std::as_const(layers)) layer->beforeRender(q); - } - rc()->polishItems(); - if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) rc()->beginFrame(); rc()->sync(); - QQuickAnimatorController_advance(animationController.get()); Q_EMIT q->beforeRendering(); runAndClearJobs(&beforeRenderingJobs); - auto needsCommit = doRenderOutputs(needsFrameOutput, outputs, forceRender); + WBufferRenderer::RenderFlags flags = WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject; + if (forceRender || captureLocked) + flags |= WBufferRenderer::ForceRender; + + Q_ASSERT(helper->outputViewport()->output()->scale() <= helper->outputViewport()->devicePixelRatio()); + const auto &format = helper->output()->render_format; + if (!helper->outputViewport()->depends().isEmpty()) + updateDirtyNodes(); + + auto *primary = helper->bufferRenderer(); + const bool redrawPrimary = forceRender || captureLocked || primary->settleDamage(); + if (redrawPrimary) { + wlr_buffer *buffer = helper->render(primary, format, flags, + helper->outputViewport()->colorContentsMode()); + Q_ASSERT(buffer == primary->currentBuffer()); + } + + WBufferRenderer *bufferRenderer = helper->afterRender(); + rendererList.clear(); Q_EMIT q->afterRendering(); runAndClearJobs(&afterRenderingJobs); @@ -1574,58 +1700,51 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) rc()->endFrame(); - // prevent gles2-render exception in wlroots. - // wlroots may have render operations after commit, so do - // not move the location during the reset operation. - // eg: screencopy ext-image-capture resetGlState(); QList> committedOutputs; - if (doCommit) { - committedOutputs.reserve(needsCommit.size()); - for (auto i : std::as_const(needsCommit)) { - // Explicit render(viewport, true) is used for state-only output - // transactions. It must not be suppressed merely because the - // transaction itself scheduled the next frame. - if (forceRender || Q_UNLIKELY(!i.first->framePending())) { - if (Q_LIKELY(i.first->commit(i.second))) { - // Make sure the output is still valid after commit - auto output = i.first->outputViewport()->output(); - if (Q_LIKELY(needsFrameOutput)) { - Q_ASSERT(output->handle() == needsFrameOutput); - if (committedOutputs.isEmpty()) - committedOutputs.append(output); - } else if (!committedOutputs.contains(output)) { - committedOutputs.append(output); - } - } + if (bufferRenderer || helper->extraState()) { + if (doCommit) { + if (forceRender || Q_UNLIKELY(!helper->framePending())) { + if (Q_LIKELY(helper->commit(bufferRenderer))) + committedOutputs.append(helper->outputViewport()->output()); } - - if (i.second->currentBuffer()) { - i.second->endRender(); + const bool didRender = bufferRenderer && bufferRenderer->currentBuffer() != nullptr; + if (bufferRenderer) + bufferRenderer->endRender(); + if (didRender + && q->damageVisual() == WOutputRenderWindow::DamageVisual::Highlight) { + helper->addHighlightFrame(bufferRenderer); } - - i.first->resetState(); + helper->resetState(); + } else { + if (bufferRenderer) + bufferRenderer->endRender(); + helper->resetState(); } } resetGlState(); - // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all - // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb, - // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current - // OpenGL context here in order to ensure QtQuick always make EGL context to Intel - // renderer before next frame. if (glContext) glContext->doneCurrent(); inRendering = false; + if (sceneInspectNeeded) + requestSceneInspect(); Q_EMIT q->renderEnd(committedOutputs); } + +static QQuickRenderControl *createOutputRenderControl() +{ + WSGContext::ensureInstalled(); + return new RenderControl(); +} + // TODO: Support QWindow::setCursor WOutputRenderWindow::WOutputRenderWindow(QObject *parent) - : QQuickWindow(*new WOutputRenderWindowPrivate(this), new RenderControl()) + : QQuickWindow(*new WOutputRenderWindowPrivate(this), createOutputRenderControl()) { setObjectName(QW::RenderWindow::id()); @@ -1641,6 +1760,16 @@ WOutputRenderWindow::WOutputRenderWindow(QObject *parent) contentItem()->setFlag(QQuickItem::ItemIsFocusScope); contentItem()->setFocus(true); + { + const QByteArray raw = qgetenv("WAYLIB_DEBUG_DAMAGE").trimmed().toLower(); + DamageVisual visual = DamageVisual::Off; + if (raw == "highlight" || raw == "1" || raw == "true" || raw == "on") + visual = DamageVisual::Highlight; + if (visual != DamageVisual::Off) + qCInfo(lcWlRenderer) << "WAYLIB_DEBUG_DAMAGE=" << raw.constData(); + d_func()->damageVisual = visual; + } + qGuiApp->installEventFilter(this); } @@ -1655,6 +1784,11 @@ WOutputRenderWindow::~WOutputRenderWindow() renderControl()->disconnect(this); renderControl()->invalidate(); + // Blit RHI managers are QObject children of this window and import the + // RenderControl-owned GL context. Destroy them here, before + // delete renderControl() frees that context. Otherwise QRhi::~QRhi() + // makeCurrent() SIGSEGVs during QObject::deleteChildren(). + WRenderBufferNode::destroyWindowDataManagers(this); delete renderControl(); } @@ -1668,9 +1802,6 @@ void WOutputRenderWindow::attach(WOutputViewport *output) { Q_D(WOutputRenderWindow); - if (output->objectName() == PRIVATE_WOutputViewport) - return; - Q_ASSERT(output->output()); const bool containsOutput = d->containsOutput(output->output()); auto newOutput = new OutputHelper(output, this, true); @@ -1691,10 +1822,10 @@ void WOutputRenderWindow::attach(WOutputViewport *output) // On hot-unplug WBackend deletes WOutput from the native destroy // callback; ~WOutput::teardown() drops this owner group before // wlr_output_finish asserts empty frame/needs_frame lists. - // Only render the output whose frame event fired. Explicit render() - // calls still scan all outputs. + // Only render the output whose frame event fired. woutput->listeners(owner)->add(&wlrOut->events.frame, this, [d, wlrOut] { - d->doRender(wlrOut, d->outputs, false, true); + if (auto *helper = d->helperForWlrOutput(wlrOut)) + d->doRender(helper, false, true); }); woutput->listeners(owner)->add(&wlrOut->events.needs_frame, woutput, &WOutput::scheduleFrame); @@ -1717,9 +1848,6 @@ void WOutputRenderWindow::attach(WOutputViewport *output) void WOutputRenderWindow::detach(WOutputViewport *output) { - if (output->objectName() == PRIVATE_WOutputViewport) - return; - Q_D(WOutputRenderWindow); int index = d->indexOfOutputHelper(output); @@ -1974,10 +2102,20 @@ void WOutputRenderWindow::setDisableLayers(bool newDisableLayers) Q_EMIT disableLayersChanged(); } -void WOutputRenderWindow::render() +WOutputRenderWindow::DamageVisual WOutputRenderWindow::damageVisual() const +{ + Q_D(const WOutputRenderWindow); + return d->damageVisual; +} + +void WOutputRenderWindow::setDamageVisual(DamageVisual visual) { Q_D(WOutputRenderWindow); - d->doRender(nullptr, d->outputs, false, true); + if (d->damageVisual == visual) + return; + d->damageVisual = visual; + d->scheduleDoRender(); + Q_EMIT damageVisualChanged(); } void WOutputRenderWindow::render(WOutputViewport *output, bool doCommit) @@ -1986,7 +2124,7 @@ void WOutputRenderWindow::render(WOutputViewport *output, bool doCommit) int index = d->indexOfOutputHelper(output); Q_ASSERT(index >= 0); - d->doRender(nullptr, {d->outputs.at(index)}, true, doCommit); + d->doRender(d->outputs.at(index), true, doCommit); } void WOutputRenderWindow::update() diff --git a/waylib/src/server/qtquick/woutputrenderwindow.h b/waylib/src/server/qtquick/woutputrenderwindow.h index 1e20ef10d2..6ecd9c65f3 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.h +++ b/waylib/src/server/qtquick/woutputrenderwindow.h @@ -25,10 +25,17 @@ class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQm Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged) Q_PROPERTY(qreal height READ height WRITE setHeight NOTIFY heightChanged) Q_PROPERTY(bool disableLayers READ disableLayers WRITE setDisableLayers NOTIFY disableLayersChanged FINAL) + Q_PROPERTY(DamageVisual damageVisual READ damageVisual WRITE setDamageVisual NOTIFY damageVisualChanged FINAL) QML_NAMED_ELEMENT(OutputRenderWindow) Q_INTERFACES(QQmlParserStatus) public: + enum class DamageVisual { + Off, + Highlight, + }; + Q_ENUM(DamageVisual) + explicit WOutputRenderWindow(QObject *parent = nullptr); ~WOutputRenderWindow(); @@ -64,9 +71,11 @@ class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQm bool disableLayers() const; void setDisableLayers(bool newDisableLayers); + DamageVisual damageVisual() const; + void setDamageVisual(DamageVisual visual); + public Q_SLOTS: - void render(); void render(WOutputViewport *output, bool doCommit); void update(); void update(WOutputViewport *output); @@ -80,6 +89,7 @@ public Q_SLOTS: void outputViewportInitialized(WAYLIB_SERVER_NAMESPACE::WOutputViewport *output); void initialized(); void disableLayersChanged(); + void damageVisualChanged(); void renderEnd(QList> committedOutputs); void effectiveDevicePixelRatioChanged(qreal scale); diff --git a/waylib/src/server/qtquick/woutputviewport.cpp b/waylib/src/server/qtquick/woutputviewport.cpp index a1f8554b99..80547a82a3 100644 --- a/waylib/src/server/qtquick/woutputviewport.cpp +++ b/waylib/src/server/qtquick/woutputviewport.cpp @@ -75,28 +75,8 @@ void WOutputViewportPrivate::updateImplicitSize() void WOutputViewportPrivate::updateRenderBufferSource() { - QList sources; - - if (input) { - sources.append(input); - } else { - // the "nullptr" is on behalf of the window's contentItem - sources.append(nullptr); - } - - if (extraRenderSource) - sources.append(extraRenderSource); - + bufferRenderer->setViewport(viewport, input, true); forceRender = true; - bufferRenderer->setSourceList(sources, true); -} - -void WOutputViewportPrivate::setExtraRenderSource(QQuickItem *source) -{ - if (extraRenderSource == source) - return; - extraRenderSource = source; - updateRenderBufferSource(); } WOutputViewport::WOutputViewport(QQuickItem *parent) @@ -386,9 +366,9 @@ void WOutputViewport::resetTargetRect() QTransform WOutputViewport::sourceRectToTargetRectTransfrom() const { return WBufferRenderer::inputMapToOutput(effectiveSourceRect(), - targetRect(), - output()->size(), - devicePixelRatio()); + targetRect(), + output()->size(), + devicePixelRatio()); } QMatrix4x4 WOutputViewport::renderMatrix() const diff --git a/waylib/src/server/qtquick/wregionhighlightitem.cpp b/waylib/src/server/qtquick/wregionhighlightitem.cpp new file mode 100644 index 0000000000..577d3d0229 --- /dev/null +++ b/waylib/src/server/qtquick/wregionhighlightitem.cpp @@ -0,0 +1,356 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wregionhighlightitem.h" +#include "woutputlayer.h" +#include "woutputviewport.h" +#include "woutputrenderwindow.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +namespace { + +constexpr int kBorderWidth = 2; +constexpr float kFillRgb = 0.45f; +constexpr float kBorderRgb = 0.9f; + +QRect toDeviceRect(const QRect &sceneRect, const QTransform &sceneToDevice) +{ + return sceneToDevice.mapRect(QRectF(sceneRect)).toAlignedRect(); +} + +} // namespace + +qint64 WRegionOverlay::nowMs() +{ + static const qint64 origin = [] { + QElapsedTimer t; + t.start(); + return t.msecsSinceReference(); + }(); + QElapsedTimer t; + t.start(); + return t.msecsSinceReference() - origin; +} + +float WRegionOverlay::alphaFor(const Entry &e, qint64 now) +{ + const qint64 age = qMax(qint64(0), now - e.whenMs); + if (age >= fadeOutMs) + return 0.0f; + return 1.0f - float(age) / float(fadeOutMs); +} + +void WRegionOverlay::rgbForFrame(int index, int liveCount, float *r, float *g, float *b) +{ + const float t = (liveCount <= 1) + ? 0.0f + : float(qBound(0, index, liveCount - 1)) / float(liveCount - 1); + *r = 1.0f - t; + *g = t; + *b = 0.0f; +} + +int WRegionOverlay::liveEntryCount(const QList &entries, qint64 now) +{ + int n = 0; + for (const Entry &e : entries) { + if (alphaFor(e, now) > 0.0f) + ++n; + } + return n; +} + +QVector WRegionOverlay::borderRects(const QRect &rect, int width) +{ + QVector out; + if (rect.isEmpty() || width <= 0) + return out; + out.append(QRect(rect.x(), rect.y(), rect.width(), width)); + out.append(QRect(rect.x(), rect.y() + rect.height() - width, rect.width(), width)); + out.append(QRect(rect.x(), rect.y(), width, rect.height())); + out.append(QRect(rect.x() + rect.width() - width, rect.y(), width, rect.height())); + return out; +} + +void WRegionOverlay::rebuildEntries(qint64 now) +{ + WPixmanRegion acc; + bool accFull = false; + QList kept; + kept.reserve(m_entries.size()); + + for (Entry &e : m_entries) { + if (!accFull) { + if (e.full) { + accFull = true; + acc.clear(); + } else { + e.region -= acc; + acc += e.region; + } + } else { + e.region.clear(); + e.full = false; + } + + const bool expired = (now - e.whenMs) >= fadeOutMs + || (!e.full && e.region.isEmpty()); + if (!expired) + kept.append(std::move(e)); + } + + m_entries = std::move(kept); + m_nowMs = now; +} + +void WRegionOverlay::addFrame(const WDamageRegion &damage, qint64 now) +{ + if (damage.isFull || !damage.region.isEmpty()) { + Entry e; + e.region = damage.isFull ? WPixmanRegion() : damage.region; + e.whenMs = now; + e.full = damage.isFull; + m_entries.prepend(e); + } + rebuildEntries(now); +} + +void WRegionOverlay::expire(qint64 now) +{ + rebuildEntries(now); +} + +QVector WRegionOverlay::overlayRects(const QRect &outputRect, qint64 now) const +{ + QVector out; + const int liveCount = liveEntryCount(m_entries, now); + int index = 0; + for (const Entry &e : m_entries) { + const float alpha = alphaFor(e, now); + if (alpha <= 0.0f) + continue; + float red = 1.0f; + float green = 0.0f; + float blue = 0.0f; + rgbForFrame(index, liveCount, &red, &green, &blue); + ++index; + OverlayRect quad; + quad.fill = QColor(qRound(255.0f * red), qRound(255.0f * green), qRound(255.0f * blue), + qRound(255.0f * kFillRgb * alpha)); + quad.border = QColor(qRound(255.0f * red), qRound(255.0f * green), qRound(255.0f * blue), + qRound(255.0f * kBorderRgb * alpha)); + if (e.full) { + if (outputRect.isEmpty()) + continue; + quad.rect = outputRect; + out.append(quad); + continue; + } + int count = 0; + const pixman_box32_t *boxes = e.region.rectangles(&count); + for (int i = 0; i < count && boxes; ++i) { + const QRect rect(boxes[i].x1, boxes[i].y1, + boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); + if (rect.isEmpty()) + continue; + quad.rect = rect; + out.append(quad); + } + } + std::reverse(out.begin(), out.end()); + return out; +} + +void WRegionOverlay::paint(QPainter *painter, const QTransform &sceneToDevice, + const QList &entries, qint64 now) +{ + if (!painter || entries.isEmpty()) + return; + + painter->save(); + painter->setPen(Qt::NoPen); + painter->setRenderHint(QPainter::Antialiasing, false); + + const QRect clip = painter->device() + ? QRect(0, 0, painter->device()->width(), painter->device()->height()) + : QRect(); + + const int liveCount = liveEntryCount(entries, now); + QVector live; + live.reserve(liveCount); + for (const Entry &e : entries) { + if (alphaFor(e, now) > 0.0f) + live.append(&e); + } + + for (int i = live.size() - 1; i >= 0; --i) { + const Entry &e = *live.at(i); + const float alpha = alphaFor(e, now); + float red = 1.0f; + float green = 0.0f; + float blue = 0.0f; + rgbForFrame(i, live.size(), &red, &green, &blue); + + QColor fill(qRound(255.0f * red), qRound(255.0f * green), qRound(255.0f * blue), + qRound(255.0f * kFillRgb * alpha)); + QColor border(qRound(255.0f * red), qRound(255.0f * green), qRound(255.0f * blue), + qRound(255.0f * kBorderRgb * alpha)); + + QVector rects; + if (e.full) { + rects.append(clip.isNull() ? QRect(0, 0, 1, 1) : clip); + } else { + int count = 0; + const pixman_box32_t *boxes = e.region.rectangles(&count); + for (int i = 0; i < count && boxes; ++i) { + const QRect r(boxes[i].x1, boxes[i].y1, + boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); + rects.append(toDeviceRect(r, sceneToDevice)); + } + } + painter->setBrush(fill); + for (const QRect &r : rects) { + const QRect clipped = clip.isNull() ? r : r.intersected(clip); + if (!clipped.isEmpty()) + painter->fillRect(clipped, fill); + } + painter->setBrush(border); + for (const QRect &r : rects) { + for (const QRect &b : borderRects(r, kBorderWidth)) { + const QRect clipped = clip.isNull() ? b : b.intersected(clip); + if (!clipped.isEmpty()) + painter->fillRect(clipped, border); + } + } + } + painter->restore(); +} + +WRegionHighlightItem::WRegionHighlightItem(WOutputViewport *viewport, QQuickItem *host) + : QQuickItem(host) + , m_viewport(viewport) +{ + Q_ASSERT(viewport); + Q_ASSERT(host); + setObjectName(QStringLiteral("damageHighlight")); + setZ(1000000); + setVisible(false); + setAcceptedMouseButtons(Qt::NoButton); + setAcceptTouchEvents(false); + setAcceptHoverEvents(false); + if (auto *ctx = qmlContext(host)) + QQmlEngine::setContextForObject(this, ctx); + + m_layer = WOutputLayer::qmlAttachedProperties(this); + // keepLayer: do not drop the overlay if the DRM plane is rejected. + // force: software fallback must use the shadow copy, never extraRenderSource + // onto the preserved primary buffer (that would dirty its damage ring). + m_layer->setKeepLayer(true); + m_layer->setForce(true); + m_layer->setZ(1000000); + m_layer->setOutputs({ viewport }); +} + +void WRegionHighlightItem::addFrame(const WDamageRegion &damage) +{ + m_overlay.addFrame(damage); +} + +QQuickRectangle *WRegionHighlightItem::ensureRect(int index) +{ + while (m_rects.size() <= index) { + auto *rect = new QQuickRectangle(this); + if (auto *ctx = qmlContext(this)) + QQmlEngine::setContextForObject(rect, ctx); + rect->setVisible(false); + rect->border()->setWidth(WRegionOverlay::overlayBorderWidth); + m_rects.append(rect); + } + return m_rects.at(index); +} + +void WRegionHighlightItem::setLayerEnabled(bool on) +{ + if (!m_layer || m_wantLayer == on) + return; + m_wantLayer = on; + if (m_layer->enabled() != on) + m_layer->setEnabled(on); +} + +void WRegionHighlightItem::hide() +{ + for (auto *rect : std::as_const(m_rects)) + rect->setVisible(false); + if (isVisible()) + setVisible(false); + setLayerEnabled(false); +} + +void WRegionHighlightItem::sync() +{ + auto *win = qobject_cast(window()); + if (!m_viewport || !win + || win->damageVisual() != WOutputRenderWindow::DamageVisual::Highlight) { + m_overlay.expire(); + m_overlay.clear(); + hide(); + return; + } + + // Entries and lastRenderRegion() are scene-logical; keep the quads in + // the same space. Device conversion happens only at damage-ring write. + const QRect output(0, 0, + qMax(1, qRound(m_viewport->width())), + qMax(1, qRound(m_viewport->height()))); + const auto quads = m_overlay.overlayRects(output); + if (quads.isEmpty()) { + hide(); + return; + } + + QRect aabb; + for (const auto &quad : quads) + aabb |= quad.rect; + if (aabb.isEmpty()) { + hide(); + return; + } + + const QPointF origin = m_viewport->position(); + setPosition(origin + QPointF(aabb.x(), aabb.y())); + setSize(QSizeF(aabb.width(), aabb.height())); + + for (int i = 0; i < quads.size(); ++i) { + auto *rect = ensureRect(i); + const QRect &scene = quads.at(i).rect; + rect->setX(scene.x() - aabb.x()); + rect->setY(scene.y() - aabb.y()); + rect->setWidth(scene.width()); + rect->setHeight(scene.height()); + rect->setColor(quads.at(i).fill); + rect->border()->setColor(quads.at(i).border); + rect->setVisible(true); + } + for (int i = quads.size(); i < m_rects.size(); ++i) + m_rects.at(i)->setVisible(false); + + // Extract from the parent scene before becoming visible so the primary + // pass cannot paint these rectangles into the main buffer. + setLayerEnabled(true); + if (!isVisible()) + setVisible(true); +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/wregionhighlightitem.h b/waylib/src/server/qtquick/wregionhighlightitem.h new file mode 100644 index 0000000000..e45eef4566 --- /dev/null +++ b/waylib/src/server/qtquick/wregionhighlightitem.h @@ -0,0 +1,104 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include +#include +#include +#include "wpixmanregion.h" +#include + +QT_BEGIN_NAMESPACE +class QPainter; +class QQuickRectangle; +class QTransform; +QT_END_NAMESPACE + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WOutputViewport; +class WOutputLayer; + +// Fade history for the highlight layer. Not part of damage tracking. +class WRegionOverlay +{ +public: + static constexpr int fadeOutMs = 250; + static constexpr int overlayBorderWidth = 2; + + struct Entry { + WPixmanRegion region; + qint64 whenMs = 0; + bool full = false; + }; + + struct OverlayRect { + QRect rect; + QColor fill; + QColor border; + }; + + static qint64 nowMs(); + + void addFrame(const WDamageRegion &damage, qint64 now = nowMs()); + void addFrame(const WPixmanRegion &content, bool full, qint64 now = nowMs()) + { + addFrame(WDamageRegion(content, full), now); + } + void expire(qint64 now = nowMs()); + void clear() { m_entries.clear(); } + bool needsAnotherFrame() const { return !m_entries.isEmpty(); } + + const QList &entries() const { return m_entries; } + qint64 currentTimeMs() const { return m_nowMs; } + QVector overlayRects(const QRect &outputRect, qint64 now = nowMs()) const; + + static void paint(QPainter *painter, const QTransform &sceneToDevice, + const QList &entries, qint64 now); + +private: + void rebuildEntries(qint64 now); + static float alphaFor(const Entry &e, qint64 now); + static int liveEntryCount(const QList &entries, qint64 now); + static void rgbForFrame(int index, int liveCount, float *r, float *g, float *b); + static QVector borderRects(const QRect &rect, int width); + + QList m_entries; + qint64 m_nowMs = 0; +}; + +// OutputLayer host. Damage quads are pooled QQuickRectangle children, +// sized to the overlay AABB so the layer buffer is not fullscreen. +// force+keepLayer: DRM overlay when possible; otherwise a shadow copy. +// Never extraRenderSource onto the preserved primary buffer. +class WRegionHighlightItem : public QQuickItem +{ + Q_OBJECT + +public: + explicit WRegionHighlightItem(WOutputViewport *viewport, QQuickItem *host); + void addFrame(const WDamageRegion &damage); + void addFrame(const WPixmanRegion &content, bool full) + { + addFrame(WDamageRegion(content, full)); + } + void sync(); + WRegionOverlay *overlay() { return &m_overlay; } + const WRegionOverlay *overlay() const { return &m_overlay; } + +private: + QQuickRectangle *ensureRect(int index); + void hide(); + void setLayerEnabled(bool on); + + WOutputViewport *m_viewport = nullptr; + WOutputLayer *m_layer = nullptr; + WRegionOverlay m_overlay; + QVector m_rects; + bool m_wantLayer = false; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/wrenderbufferblitter.cpp b/waylib/src/server/qtquick/wrenderbufferblitter.cpp index 9443bd6469..331ec087cf 100644 --- a/waylib/src/server/qtquick/wrenderbufferblitter.cpp +++ b/waylib/src/server/qtquick/wrenderbufferblitter.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "wayliblogging.h" WAYLIB_SERVER_BEGIN_NAMESPACE @@ -95,6 +96,8 @@ class Q_DECL_HIDDEN WRenderBufferBlitterPrivate : public WObjectPrivate Content *content = nullptr; QQuickItem *container = nullptr; mutable BlitTextureProvider *tp = nullptr; + int damageExpansion = 0; + bool clipDamageExpansion = true; }; class Q_DECL_HIDDEN Content : public QQuickItem @@ -242,6 +245,39 @@ void WRenderBufferBlitter::setOffscreen(bool newOffscreen) Q_EMIT offscreenChanged(); } +int WRenderBufferBlitter::damageExpansion() const +{ + W_DC(WRenderBufferBlitter); + return d->damageExpansion; +} + +void WRenderBufferBlitter::setDamageExpansion(int px) +{ + W_D(WRenderBufferBlitter); + px = qMax(0, px); + if (d->damageExpansion == px) + return; + d->damageExpansion = px; + update(); + Q_EMIT damageExpansionChanged(); +} + +bool WRenderBufferBlitter::clipDamageExpansion() const +{ + W_DC(WRenderBufferBlitter); + return d->clipDamageExpansion; +} + +void WRenderBufferBlitter::setClipDamageExpansion(bool clip) +{ + W_D(WRenderBufferBlitter); + if (d->clipDamageExpansion == clip) + return; + d->clipDamageExpansion = clip; + update(); + Q_EMIT clipDamageExpansionChanged(); +} + void WRenderBufferBlitter::invalidateSceneGraph() { W_D(WRenderBufferBlitter); @@ -261,13 +297,17 @@ static void onTextureChanged(WRenderBufferNode *node, void *data) { QSGNode *WRenderBufferBlitter::updatePaintNode(QSGNode *oldNode, [[maybe_unused]] QQuickItem::UpdatePaintNodeData *oldData) { + W_D(WRenderBufferBlitter); auto node = static_cast(oldNode); + qCDebug(lcWlRenderBuffer) << "updatePaintNode" << objectName() << size() + << "visible" << isVisible() << "existing" << bool(node); if (Q_LIKELY(node)) { node->resize(size()); + node->setDamageExpansion(d->damageExpansion); + node->setClipDamageExpansion(d->clipDamageExpansion); return node; } - W_D(WRenderBufferBlitter); if (window()->graphicsApi() == QSGRendererInterface::Software) { node = WRenderBufferNode::createSoftwareNode(this); } else { @@ -277,6 +317,8 @@ QSGNode *WRenderBufferBlitter::updatePaintNode(QSGNode *oldNode, [[maybe_unused] node->setContentItem(d->container); node->setTextureChangedCallback(onTextureChanged, d); node->resize(size()); + node->setDamageExpansion(d->damageExpansion); + node->setClipDamageExpansion(d->clipDamageExpansion); onTextureChanged(node, d); return node; diff --git a/waylib/src/server/qtquick/wrenderbufferblitter.h b/waylib/src/server/qtquick/wrenderbufferblitter.h index 5157c39604..679a1ebefc 100644 --- a/waylib/src/server/qtquick/wrenderbufferblitter.h +++ b/waylib/src/server/qtquick/wrenderbufferblitter.h @@ -17,6 +17,8 @@ class Q_DECL_EXPORT WRenderBufferBlitter : public QQuickItem, public WObject Q_PRIVATE_PROPERTY(WRenderBufferBlitter::d_func(), QQmlListProperty data READ data DESIGNABLE false) Q_PROPERTY(QQuickItem* content READ content CONSTANT) Q_PROPERTY(bool offscreen READ offscreen WRITE setOffscreen NOTIFY offscreenChanged FINAL) + Q_PROPERTY(int damageExpansion READ damageExpansion WRITE setDamageExpansion NOTIFY damageExpansionChanged FINAL) + Q_PROPERTY(bool clipDamageExpansion READ clipDamageExpansion WRITE setClipDamageExpansion NOTIFY clipDamageExpansionChanged FINAL) QML_NAMED_ELEMENT(RenderBufferBlitter) public: @@ -28,8 +30,15 @@ class Q_DECL_EXPORT WRenderBufferBlitter : public QQuickItem, public WObject bool offscreen() const; void setOffscreen(bool newOffscreen); + int damageExpansion() const; + void setDamageExpansion(int px); + bool clipDamageExpansion() const; + void setClipDamageExpansion(bool clip); + Q_SIGNALS: void offscreenChanged(); + void damageExpansionChanged(); + void clipDamageExpansionChanged(); private Q_SLOTS: void invalidateSceneGraph(); diff --git a/waylib/src/server/qtquick/wrenderhelper.cpp b/waylib/src/server/qtquick/wrenderhelper.cpp index 131631a62b..b37408ddaf 100644 --- a/waylib/src/server/qtquick/wrenderhelper.cpp +++ b/waylib/src/server/qtquick/wrenderhelper.cpp @@ -8,6 +8,7 @@ #include "wayliblogging.h" #include "private/wqmlhelper_p.h" #include "private/wglobal_p.h" +#include "wsgcontext_p.h" #include #include @@ -136,13 +137,13 @@ bool WRenderHelper::RenderTarget::colorPreserved() const } static constexpr WGlobal::ColorContentsMode resolveColorContentsMode( - WGlobal::ColorContentsMode requested, bool softwareRenderer) noexcept + WGlobal::ColorContentsMode requested) noexcept { if (requested != WGlobal::ColorContentsMode::DontCare) return requested; - // Software clear is expensive; default to preserve. - return softwareRenderer ? WGlobal::ColorContentsMode::Preserve - : WGlobal::ColorContentsMode::Clear; + // Recycled swapchain images keep last-use pixels (wlroots LOAD). An + // explicit Clear is the only path that discards them. + return WGlobal::ColorContentsMode::Preserve; } static QRhiTextureRenderTarget::Flags rhiRenderTargetFlags(WGlobal::ColorContentsMode mode) @@ -345,7 +346,9 @@ QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi(QQuickRenderCont QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi() { auto getApi = [] () { - // Only for get GraphicsApi + // Only for get GraphicsApi. Install first so this temporary + // QQuickRenderControl does not create Qt's default sg. + WSGContext::ensureInstalled(); QQuickRenderControl rc; return getGraphicsApi(&rc); }; @@ -667,7 +670,7 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr return {}; const bool isSoftware = wlr_renderer_is_pixman(d->renderer); - const auto resolvedMode = resolveColorContentsMode(mode, isSoftware); + const auto resolvedMode = resolveColorContentsMode(mode); const bool needPreserve = resolvedMode == WGlobal::ColorContentsMode::Preserve; const auto flags = rhiRenderTargetFlags(resolvedMode); @@ -677,7 +680,7 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr if (needPreserve != data->colorPreserved) { #ifdef ENABLE_VULKAN_RENDER if (wlr_renderer_is_vk(d->renderer)) { - qCWarning(lcWlRenderHelper) + qCDebug(lcWlRenderHelper) << "Recreating Vulkan render target for buffer" << buffer << "to change color preserved from" << data->colorPreserved << "to" << needPreserve; diff --git a/waylib/src/server/qtquick/wrenderhelper.h b/waylib/src/server/qtquick/wrenderhelper.h index 0ea45a3ba4..d44872e1de 100644 --- a/waylib/src/server/qtquick/wrenderhelper.h +++ b/waylib/src/server/qtquick/wrenderhelper.h @@ -9,12 +9,14 @@ #include #include #include +#include QT_BEGIN_NAMESPACE class QQuickRenderControl; class QSGTexture; class QSGPlainTexture; class QRhi; +class QRhiTexture; class QRhiCommandBuffer; QT_END_NAMESPACE diff --git a/waylib/src/server/qtquick/wsurfaceitem.cpp b/waylib/src/server/qtquick/wsurfaceitem.cpp index 953a032a45..d2de694125 100644 --- a/waylib/src/server/qtquick/wsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wsurfaceitem.cpp @@ -24,6 +24,10 @@ #include #include #include +#include "wpixmanregion.h" +#include "wsgimagenode_p.h" +#include "wtools.h" +#include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -223,6 +227,10 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate Q_ASSERT(!updateTextureConnection); + // Surface-local; drop it on detach so a later setSurface() cannot + // map the previous client's damage onto a different buffer. + pendingDamage = { }; + if (dontCacheLastBuffer) { buffer.reset(); cleanTextureProvider(); @@ -244,6 +252,12 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate updateTextureConnection = QObject::connect(surface, &WSurface::commit, q, [q, this] (quint32 committedState) { const bool bufferChanged = committedState & WLR_SURFACE_STATE_BUFFER; + const bool opaqueChanged = committedState & WLR_SURFACE_STATE_OPAQUE_REGION; + if (surface && surface->handle()) { + WPixmanRegion pixDamage; + wlr_surface_get_effective_damage(surface->handle(), pixDamage); + pendingDamage += pixDamage; + } if (bufferChanged) { // Get the new buffer pointer from surface @@ -257,6 +271,8 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate buffer.reset(newBuffer); q->update(); } + } else if (live && (!pendingDamage.isEmpty() || opaqueChanged)) { + q->update(); } if (Q_LIKELY((q->isVisible() || lastRendered) && live)) @@ -367,6 +383,7 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate bool ignoreBufferOffset = false; bool lastRendered = false; QAtomicInteger rendered = false; + WPixmanRegion pendingDamage; }; WSurfaceItemContent::WSurfaceItemContent(QQuickItem *parent) @@ -405,6 +422,7 @@ void WSurfaceItemContent::setSurface(WSurface *surface) if (surface && d->surface == surface) return; + d->pendingDamage = { }; auto oldSurface = d->surface; d->surface = surface; if (isComponentComplete()) { @@ -582,6 +600,63 @@ class Q_DECL_HIDDEN WSGRenderFootprintNode: public QSGRenderNode QPointer m_owner; }; +static WPixmanRegion mapSurfaceDamageToItem(const WPixmanRegion &surfaceDamage, + const QSizeF &surfaceSize, + const QRectF &targetGeometry) +{ + if (surfaceDamage.isEmpty()) + return { }; + QTransform xf; + xf.translate(targetGeometry.x(), targetGeometry.y()); + if (surfaceSize.width() > 0 && surfaceSize.height() > 0 && targetGeometry.width() > 0 + && targetGeometry.height() > 0) { + xf.scale(targetGeometry.width() / surfaceSize.width(), + targetGeometry.height() / surfaceSize.height()); + } + return surfaceDamage.mappedOuter(xf); +} + +static WPixmanRegion mapSurfaceOpaqueToItem(const pixman_region32_t *opaque, + const QSizeF &surfaceSize, + const QRectF &targetGeometry) +{ + if (!opaque) + return { }; + WPixmanRegion local(opaque); + if (local.isEmpty()) + return { }; + QTransform xf; + xf.translate(targetGeometry.x(), targetGeometry.y()); + if (surfaceSize.width() > 0 && surfaceSize.height() > 0 && targetGeometry.width() > 0 + && targetGeometry.height() > 0) { + xf.scale(targetGeometry.width() / surfaceSize.width(), + targetGeometry.height() / surfaceSize.height()); + } + WPixmanRegion mapped = local.mappedInner(xf); + mapped &= innerAligned(targetGeometry); + return mapped; +} + +static WPixmanRegion surfaceDamageInItem(WSurface *surface, const QRectF &targetGeometry) +{ + if (!surface) + return { }; + WPixmanRegion damage; + wlr_surface_get_effective_damage(surface->handle(), damage); + return mapSurfaceDamageToItem(damage, surface->size(), targetGeometry); +} + +static WPixmanRegion surfaceOpaqueInItem(WSurface *surface, + const QRectF &targetGeometry, + qreal alphaModifier) +{ + if (!surface || !surface->handle() || alphaModifier <= 0.999) + return { }; + return mapSurfaceOpaqueToItem(&surface->handle()->opaque_region, + surface->size(), + targetGeometry); +} + QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { W_D(WSurfaceItemContent); @@ -609,11 +684,28 @@ QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeD node->appendChildNode(fpnode); } + const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size()); + // Set explicit damage before touching the texture so DirtyMaterial uses + // the surface damage instead of the full quad. + WPixmanRegion mapped = d->pendingDamage.isEmpty() + ? surfaceDamageInItem(d->surface, targetGeometry) + : mapSurfaceDamageToItem(d->pendingDamage, + d->surface ? d->surface->size() : QSizeF(), + targetGeometry); + d->pendingDamage.clear(); + if (smooth()) + mapped = dilateRegion(mapped, QMargins(1, 1, 1, 1)); + if (auto *image = dynamic_cast(node)) { + image->setDamageRegion(mapped); + if (d->surface) + image->setOpaqueRegion( + surfaceOpaqueInItem(d->surface, targetGeometry, d->alphaModifier)); + } + auto texture = tp->texture(); node->setTexture(texture); const QRectF textureGeometry = d->bufferSourceBox; node->setSourceRect(textureGeometry); - const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size()); node->setRect(targetGeometry); node->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); diff --git a/waylib/src/server/utils/wpixmanregion.cpp b/waylib/src/server/utils/wpixmanregion.cpp new file mode 100644 index 0000000000..4a7a5d03a7 --- /dev/null +++ b/waylib/src/server/utils/wpixmanregion.cpp @@ -0,0 +1,473 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wpixmanregion.h" + +#include + +#include +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +static void requireRegion(pixman_bool_t ok) +{ + if (Q_UNLIKELY(!ok)) + std::abort(); +} + +WPixmanRegion::WPixmanRegion() +{ + pixman_region32_init(&m_region); +} + +WPixmanRegion::WPixmanRegion(int x, int y, int width, int height) +{ + if (width > 0 && height > 0) + pixman_region32_init_rect(&m_region, x, y, unsigned(width), unsigned(height)); + else + pixman_region32_init(&m_region); +} + +WPixmanRegion::WPixmanRegion(const QRect &rect) + : WPixmanRegion(rect.x(), rect.y(), rect.width(), rect.height()) +{ +} + +WPixmanRegion::WPixmanRegion(const pixman_region32_t *region) +{ + pixman_region32_init(&m_region); + if (region) + requireRegion(pixman_region32_copy(&m_region, region)); +} + +WPixmanRegion::WPixmanRegion(const WPixmanRegion &other) + : WPixmanRegion(other.native()) +{ +} + +WPixmanRegion::WPixmanRegion(WPixmanRegion &&other) noexcept + : m_region(other.m_region) +{ + pixman_region32_init(&other.m_region); +} + +WPixmanRegion::~WPixmanRegion() +{ + pixman_region32_fini(&m_region); +} + +WPixmanRegion &WPixmanRegion::operator=(const WPixmanRegion &other) +{ + if (this != &other) + requireRegion(pixman_region32_copy(&m_region, &other.m_region)); + return *this; +} + +WPixmanRegion &WPixmanRegion::operator=(WPixmanRegion &&other) noexcept +{ + if (this == &other) + return *this; + pixman_region32_fini(&m_region); + m_region = other.m_region; + pixman_region32_init(&other.m_region); + return *this; +} + +bool WPixmanRegion::isEmpty() const +{ + return !pixman_region32_not_empty(&m_region); +} + +// No allocation: pixman answers overlap with a rectangle containment query. +bool WPixmanRegion::intersects(const QRect &rect) const +{ + if (isEmpty() || rect.isEmpty()) + return false; + pixman_box32_t box = { rect.x(), rect.y(), rect.x() + rect.width(), + rect.y() + rect.height() }; + return pixman_region32_contains_rectangle(&m_region, &box) != PIXMAN_REGION_OUT; +} + +int WPixmanRegion::rectCount() const +{ + return pixman_region32_n_rects(&m_region); +} + +QRect WPixmanRegion::boundingRect() const +{ + if (isEmpty()) + return { }; + const pixman_box32_t *box = pixman_region32_extents(&m_region); + return QRect(box->x1, box->y1, box->x2 - box->x1, box->y2 - box->y1); +} + +const pixman_box32_t *WPixmanRegion::rectangles(int *count) const +{ + return pixman_region32_rectangles(&m_region, count); +} + +void WPixmanRegion::clear() +{ + pixman_region32_clear(&m_region); +} + +void WPixmanRegion::translate(int dx, int dy) +{ + pixman_region32_translate(&m_region, dx, dy); +} + +WPixmanRegion WPixmanRegion::translated(int dx, int dy) const +{ + WPixmanRegion result(*this); + result.translate(dx, dy); + return result; +} + +void WPixmanRegion::setRects(const pixman_box32_t *rects, int count) +{ + pixman_region32_fini(&m_region); + if (count > 0) + requireRegion(pixman_region32_init_rects(&m_region, rects, count)); + else + pixman_region32_init(&m_region); +} + +WPixmanRegion WPixmanRegion::fromRects(const pixman_box32_t *rects, int count) +{ + WPixmanRegion result; + result.setRects(rects, count); + return result; +} + +void WPixmanRegion::setIntersection(const pixman_region32_t *source, const QRect &rect) +{ + if (!source || rect.isEmpty()) { + clear(); + return; + } + requireRegion(pixman_region32_intersect_rect(&m_region, + source, + rect.x(), + rect.y(), + unsigned(rect.width()), + unsigned(rect.height()))); +} + +void WPixmanRegion::setIntersection(const pixman_region32_t *lhs, const pixman_region32_t *rhs) +{ + if (!lhs || !rhs) { + clear(); + return; + } + requireRegion(pixman_region32_intersect(&m_region, lhs, rhs)); +} + +WPixmanRegion &WPixmanRegion::operator+=(const WPixmanRegion &other) +{ + return *this += other.native(); +} + +WPixmanRegion &WPixmanRegion::operator+=(const pixman_region32_t *other) +{ + if (other && pixman_region32_not_empty(other)) + requireRegion(pixman_region32_union(&m_region, &m_region, other)); + return *this; +} + +WPixmanRegion &WPixmanRegion::operator+=(const QRect &rect) +{ + if (!rect.isEmpty()) { + requireRegion(pixman_region32_union_rect(&m_region, + &m_region, + rect.x(), + rect.y(), + unsigned(rect.width()), + unsigned(rect.height()))); + } + return *this; +} + +WPixmanRegion &WPixmanRegion::operator-=(const WPixmanRegion &other) +{ + return *this -= other.native(); +} + +WPixmanRegion &WPixmanRegion::operator-=(const pixman_region32_t *other) +{ + if (other && pixman_region32_not_empty(other)) + requireRegion(pixman_region32_subtract(&m_region, &m_region, other)); + return *this; +} + +WPixmanRegion &WPixmanRegion::operator-=(const QRect &rect) +{ + return *this -= WPixmanRegion(rect); +} + +WPixmanRegion &WPixmanRegion::operator&=(const WPixmanRegion &other) +{ + requireRegion(pixman_region32_intersect(&m_region, &m_region, &other.m_region)); + return *this; +} + +WPixmanRegion &WPixmanRegion::operator&=(const QRect &rect) +{ + if (rect.isEmpty()) { + clear(); + } else { + requireRegion(pixman_region32_intersect_rect(&m_region, + &m_region, + rect.x(), + rect.y(), + unsigned(rect.width()), + unsigned(rect.height()))); + } + return *this; +} + +bool WPixmanRegion::operator==(const WPixmanRegion &other) const +{ + return pixman_region32_equal(&m_region, &other.m_region); +} + +WPixmanRegion operator+(WPixmanRegion lhs, const WPixmanRegion &rhs) +{ + lhs += rhs; + return lhs; +} + +WPixmanRegion operator+(WPixmanRegion lhs, const QRect &rhs) +{ + lhs += rhs; + return lhs; +} + +WPixmanRegion operator-(WPixmanRegion lhs, const WPixmanRegion &rhs) +{ + lhs -= rhs; + return lhs; +} + +WPixmanRegion operator-(WPixmanRegion lhs, const QRect &rhs) +{ + lhs -= rhs; + return lhs; +} + +WPixmanRegion operator&(WPixmanRegion lhs, const WPixmanRegion &rhs) +{ + lhs &= rhs; + return lhs; +} + +WPixmanRegion operator&(WPixmanRegion lhs, const QRect &rhs) +{ + lhs &= rhs; + return lhs; +} + +static pixman_box32_t boxFromRect(const QRect &rect) +{ + return { + rect.x(), + rect.y(), + rect.x() + rect.width(), + rect.y() + rect.height(), + }; +} + +struct ActiveBox +{ + int x1; + int x2; + int resultIndex; +}; + +static QVarLengthArray maximalRectangles(const WPixmanRegion ®ion) +{ + int count = 0; + const pixman_box32_t *boxes = region.rectangles(&count); + QVarLengthArray result; + result.reserve(count); + QVarLengthArray active; + + for (int bandStart = 0; bandStart < count;) { + int bandEnd = bandStart + 1; + while (bandEnd < count && boxes[bandEnd].y1 == boxes[bandStart].y1 + && boxes[bandEnd].y2 == boxes[bandStart].y2) { + ++bandEnd; + } + + QVarLengthArray next; + next.reserve(bandEnd - bandStart); + int activeIndex = 0; + for (int i = bandStart; i < bandEnd; ++i) { + const pixman_box32_t &box = boxes[i]; + while (activeIndex < active.size() + && (active[activeIndex].x1 < box.x1 + || (active[activeIndex].x1 == box.x1 && active[activeIndex].x2 < box.x2))) { + ++activeIndex; + } + + int resultIndex = -1; + if (activeIndex < active.size() && active[activeIndex].x1 == box.x1 + && active[activeIndex].x2 == box.x2 + && result[active[activeIndex].resultIndex].y2 == box.y1) { + resultIndex = active[activeIndex].resultIndex; + result[resultIndex].y2 = box.y2; + ++activeIndex; + } else { + resultIndex = result.size(); + result.append(box); + } + next.append({ box.x1, box.x2, resultIndex }); + } + + active = std::move(next); + bandStart = bandEnd; + } + + return result; +} + +WPixmanRegion WPixmanRegion::mappedOuter(const QTransform &transform) const +{ + if (isEmpty()) + return { }; + if (transform.isIdentity()) + return *this; + if (transform.type() <= QTransform::TxTranslate) { + const qreal dx = transform.dx(); + const qreal dy = transform.dy(); + if (std::floor(dx) == dx && std::floor(dy) == dy) + return translated(int(dx), int(dy)); + } + if (!isAxisAligned(transform)) { + const QVarLengthArray boxes = maximalRectangles(*this); + QVarLengthArray mapped; + mapped.reserve(boxes.size()); + for (const pixman_box32_t &box : boxes) { + const QRect rect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1); + const QRect result = mapOuter(transform, QRectF(rect)); + if (!result.isEmpty()) + mapped.append(boxFromRect(result)); + } + return fromRects(mapped.constData(), mapped.size()); + } + + int count = 0; + const pixman_box32_t *boxes = rectangles(&count); + QVarLengthArray mapped; + mapped.reserve(count); + for (int i = 0; i < count; ++i) { + const QRect rect(boxes[i].x1, + boxes[i].y1, + boxes[i].x2 - boxes[i].x1, + boxes[i].y2 - boxes[i].y1); + const QRect result = mapOuter(transform, QRectF(rect)); + if (!result.isEmpty()) + mapped.append(boxFromRect(result)); + } + + WPixmanRegion output; + output.setRects(mapped.constData(), mapped.size()); + return output; +} + +WPixmanRegion WPixmanRegion::mappedInner(const QTransform &transform) const +{ + if (isEmpty() || !isAxisAligned(transform)) + return { }; + if (transform.isIdentity()) + return *this; + if (transform.type() <= QTransform::TxTranslate) { + const qreal dx = transform.dx(); + const qreal dy = transform.dy(); + if (std::floor(dx) == dx && std::floor(dy) == dy) + return translated(int(dx), int(dy)); + } + const QVarLengthArray boxes = maximalRectangles(*this); + WPixmanRegion output; + for (const pixman_box32_t &box : boxes) { + const QRect rect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1); + output += mapInner(transform, QRectF(rect)); + } + return output; +} + +WPixmanRegion WPixmanRegion::mappedOuter(const QMatrix4x4 &matrix) const +{ + return mappedOuter(matrix.toTransform()); +} + +WPixmanRegion WPixmanRegion::mappedInner(const QMatrix4x4 &matrix) const +{ + return mappedInner(matrix.toTransform()); +} + +WPixmanRegion dilateRegion(const WPixmanRegion ®ion, const QMargins &margins) +{ + if (region.isEmpty()) + return { }; + if (margins.isNull()) + return region; + + int count = 0; + const pixman_box32_t *boxes = region.rectangles(&count); + QVarLengthArray expanded; + expanded.reserve(count); + for (int i = 0; i < count; ++i) { + const qint64 x = qint64(boxes[i].x1) - margins.left(); + const qint64 y = qint64(boxes[i].y1) - margins.top(); + const qint64 w = qint64(boxes[i].x2) - boxes[i].x1 + margins.left() + margins.right(); + const qint64 h = qint64(boxes[i].y2) - boxes[i].y1 + margins.top() + margins.bottom(); + const int x1 = int(qBound(qint64(std::numeric_limits::min()), + x, + qint64(std::numeric_limits::max()))); + const int y1 = int(qBound(qint64(std::numeric_limits::min()), + y, + qint64(std::numeric_limits::max()))); + const int width = int(qBound(qint64(0), w, qint64(std::numeric_limits::max()))); + const int height = int(qBound(qint64(0), h, qint64(std::numeric_limits::max()))); + if (width > 0 && height > 0) + expanded.append(boxFromRect(QRect(x1, y1, width, height))); + } + return WPixmanRegion::fromRects(expanded.constData(), expanded.size()); +} + +QRegion WPixmanRegion::toQRegion() const +{ + return toQRegion(&m_region); +} + +QRegion WPixmanRegion::toQRegion(const pixman_region32_t *region) +{ + QRegion result; + if (!region) + return result; + int count = 0; + const pixman_box32_t *boxes = pixman_region32_rectangles(region, &count); + QVarLengthArray rects; + rects.reserve(count); + for (int i = 0; i < count; ++i) { + rects.append( + QRect(boxes[i].x1, boxes[i].y1, boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1)); + } + result.setRects(rects.constData(), rects.size()); + return result; +} + +WPixmanRegion WPixmanRegion::fromQRegion(const QRegion ®ion) +{ + QVarLengthArray boxes; + boxes.reserve(region.rectCount()); + for (const QRect &rect : region) + boxes.append(boxFromRect(rect)); + WPixmanRegion result; + result.setRects(boxes.constData(), boxes.size()); + return result; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/utils/wpixmanregion.h b/waylib/src/server/utils/wpixmanregion.h new file mode 100644 index 0000000000..66faf3b17a --- /dev/null +++ b/waylib/src/server/utils/wpixmanregion.h @@ -0,0 +1,321 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WAYLIB_SERVER_EXPORT WPixmanRegion +{ +public: + WPixmanRegion(); + WPixmanRegion(int x, int y, int width, int height); + explicit WPixmanRegion(const QRect &rect); + explicit WPixmanRegion(const pixman_region32_t *region); + WPixmanRegion(const WPixmanRegion &other); + WPixmanRegion(WPixmanRegion &&other) noexcept; + ~WPixmanRegion(); + + WPixmanRegion &operator=(const WPixmanRegion &other); + WPixmanRegion &operator=(WPixmanRegion &&other) noexcept; + + const pixman_region32_t *native() const + { + return &m_region; + } + + pixman_region32_t *native() + { + return &m_region; + } + + pixman_region32_t *get() + { + return native(); + } + + operator pixman_region32_t *() + { + return native(); + } + + operator const pixman_region32_t *() const + { + return native(); + } + + bool isEmpty() const; + bool intersects(const QRect &rect) const; + int rectCount() const; + QRect boundingRect() const; + const pixman_box32_t *rectangles(int *count) const; + + void clear(); + void translate(int dx, int dy); + WPixmanRegion translated(int dx, int dy) const; + // Regions contain integer, axis-aligned rectangles. Outer mapping + // conservatively covers every transformed pixel (damage); inner mapping + // contains only guaranteed covered pixels (opaque). + WPixmanRegion mappedOuter(const QTransform &transform) const; + WPixmanRegion mappedInner(const QTransform &transform) const; + WPixmanRegion mappedOuter(const QMatrix4x4 &matrix) const; + WPixmanRegion mappedInner(const QMatrix4x4 &matrix) const; + static WPixmanRegion fromRects(const pixman_box32_t *rects, int count); + void setIntersection(const pixman_region32_t *source, const QRect &rect); + void setIntersection(const pixman_region32_t *lhs, const pixman_region32_t *rhs); + + WPixmanRegion &operator+=(const WPixmanRegion &other); + WPixmanRegion &operator+=(const pixman_region32_t *other); + WPixmanRegion &operator+=(const QRect &rect); + WPixmanRegion &operator-=(const WPixmanRegion &other); + WPixmanRegion &operator-=(const pixman_region32_t *other); + WPixmanRegion &operator-=(const QRect &rect); + WPixmanRegion &operator&=(const WPixmanRegion &other); + WPixmanRegion &operator&=(const QRect &rect); + + bool operator==(const WPixmanRegion &other) const; + + bool operator!=(const WPixmanRegion &other) const + { + return !(*this == other); + } + + QRegion toQRegion() const; + static QRegion toQRegion(const pixman_region32_t *region); + static WPixmanRegion fromQRegion(const QRegion ®ion); + +private: + void setRects(const pixman_box32_t *rects, int count); + pixman_region32_t m_region; +}; + +struct WAYLIB_SERVER_EXPORT WDamageRegion +{ + WPixmanRegion region; + bool isFull = false; + + WDamageRegion() = default; + WDamageRegion(const WPixmanRegion &r, bool full = false) + : region(r), isFull(full) {} + WDamageRegion(WPixmanRegion &&r, bool full = false) + : region(std::move(r)), isFull(full) {} + explicit WDamageRegion(bool full) + : isFull(full) {} + + bool isEmpty() const { return !isFull && region.isEmpty(); } + void clear() { region.clear(); isFull = false; } + + WDamageRegion &operator+=(const WDamageRegion &other) + { + if (other.isFull) { + isFull = true; + region.clear(); + } else if (!isFull) { + region += other.region; + } + return *this; + } + + bool operator==(const WDamageRegion &other) const + { + return isFull == other.isFull && region == other.region; + } + + bool operator!=(const WDamageRegion &other) const + { + return !(*this == other); + } +}; + +WPixmanRegion operator+(WPixmanRegion lhs, const WPixmanRegion &rhs); +WPixmanRegion operator+(WPixmanRegion lhs, const QRect &rhs); +WPixmanRegion operator-(WPixmanRegion lhs, const WPixmanRegion &rhs); +WPixmanRegion operator-(WPixmanRegion lhs, const QRect &rhs); +WPixmanRegion operator&(WPixmanRegion lhs, const WPixmanRegion &rhs); + +#ifndef QT_NO_DEBUG_STREAM +inline QDebug operator<<(QDebug debug, const WPixmanRegion ®ion) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "WPixmanRegion("; + int count = 0; + const pixman_box32_t *rects = region.rectangles(&count); + if (!rects || count == 0) { + debug << "empty)"; + return debug; + } + debug << count << " rects:"; + for (int i = 0; i < count; ++i) { + debug << ' ' << (rects[i].x2 - rects[i].x1) << 'x' << (rects[i].y2 - rects[i].y1) + << '+' << rects[i].x1 << '+' << rects[i].y1; + } + debug << ')'; + return debug; +} +#endif +WPixmanRegion operator&(WPixmanRegion lhs, const QRect &rhs); + +inline int clampToIntCoord(double v) +{ + constexpr double lo = double(std::numeric_limits::min()); + constexpr double hi = double(std::numeric_limits::max()); + if (v <= lo) + return std::numeric_limits::min(); + if (v >= hi) + return std::numeric_limits::max(); + return int(v); +} + +// Pixel coverage of a continuous rect: over-estimate (damage) and under-estimate (opaque). +inline QRect outerAligned(const QRectF &r) +{ + if (Q_UNLIKELY(!r.isValid() || r.width() <= 0.0 || r.height() <= 0.0)) + return { }; + const int x1 = clampToIntCoord(std::floor(r.left())); + const int y1 = clampToIntCoord(std::floor(r.top())); + const int x2 = clampToIntCoord(std::ceil(r.right())); + const int y2 = clampToIntCoord(std::ceil(r.bottom())); + if (Q_UNLIKELY(x2 <= x1 || y2 <= y1)) + return { }; + return QRect(x1, y1, x2 - x1, y2 - y1); +} + +inline QRect innerAligned(const QRectF &r) +{ + if (Q_UNLIKELY(!r.isValid() || r.width() <= 0.0 || r.height() <= 0.0)) + return { }; + const int x1 = clampToIntCoord(std::ceil(r.left())); + const int y1 = clampToIntCoord(std::ceil(r.top())); + const int x2 = clampToIntCoord(std::floor(r.right())); + const int y2 = clampToIntCoord(std::floor(r.bottom())); + if (Q_UNLIKELY(x2 <= x1 || y2 <= y1)) + return { }; + return QRect(x1, y1, x2 - x1, y2 - y1); +} + +// Axis-aligned = translation / scale / 90-degree rotation. No shear/arbitrary rotation. +inline bool isAxisAligned(const QTransform &t) +{ + if (Q_UNLIKELY(!t.isAffine())) + return false; + const bool noOffAxis = qFuzzyIsNull(t.m12()) && qFuzzyIsNull(t.m21()); + const bool rot90 = qFuzzyIsNull(t.m11()) && qFuzzyIsNull(t.m22()); + return noOffAxis || rot90; +} + +inline QRect mapOuter(const QTransform &t, const QRectF &local) +{ + if (Q_UNLIKELY(!local.isValid() || local.width() <= 0.0 || local.height() <= 0.0)) + return { }; + if (Q_LIKELY(t.isIdentity())) + return outerAligned(local); + return outerAligned(t.mapRect(local)); +} + +inline QRect mapOuter(const QMatrix4x4 &matrix, const QRectF &local) +{ + if (Q_UNLIKELY(!local.isValid() || local.width() <= 0.0 || local.height() <= 0.0)) + return { }; + if (matrix.isIdentity()) + return outerAligned(local); + const float xs[2] = { float(local.left()), float(local.right()) }; + const float ys[2] = { float(local.top()), float(local.bottom()) }; + float x0 = 0, y0 = 0, x1 = 0, y1 = 0; + bool first = true; + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + const QVector3D p = matrix.map(QVector3D(xs[i], ys[j], 0.0f)); + if (first) { + x0 = x1 = p.x(); + y0 = y1 = p.y(); + first = false; + } else { + x0 = qMin(x0, p.x()); + y0 = qMin(y0, p.y()); + x1 = qMax(x1, p.x()); + y1 = qMax(y1, p.y()); + } + } + } + return outerAligned(QRectF(x0, y0, x1 - x0, y1 - y0)); +} + +inline QRect mapInner(const QTransform &t, const QRectF &local) +{ + if (Q_UNLIKELY(!local.isValid() || local.width() <= 0.0 || local.height() <= 0.0)) + return { }; + if (Q_UNLIKELY(!isAxisAligned(t))) + return { }; + if (Q_LIKELY(t.isIdentity())) + return innerAligned(local); + return innerAligned(t.mapRect(local)); +} + +// Pixels guaranteed inside a rounded rect, as axis-aligned boxes. The four +// corner squares of size ceil(radius) are omitted so quarter-circles are +// never claimed as opaque. Damage/clip bounds should still use the AABB. +inline WPixmanRegion roundedRectInnerRegion(const QRectF &rect, qreal radius) +{ + const QRect box = innerAligned(rect); + if (box.isEmpty()) + return { }; + qreal clamped = radius; + if (clamped < 0) + clamped = 0; + const qreal halfW = rect.width() * 0.5; + const qreal halfH = rect.height() * 0.5; + if (clamped > halfW) + clamped = halfW; + if (clamped > halfH) + clamped = halfH; + if (clamped <= 0) + return WPixmanRegion(box); + const int r = int(std::ceil(clamped)); + if (r <= 0) + return WPixmanRegion(box); + WPixmanRegion region(box); + region -= QRect(box.x(), box.y(), r, r); + region -= QRect(box.x() + box.width() - r, box.y(), r, r); + region -= QRect(box.x(), box.y() + box.height() - r, r, r); + region -= QRect(box.x() + box.width() - r, box.y() + box.height() - r, r, r); + return region; +} + +WPixmanRegion dilateRegion(const WPixmanRegion ®ion, const QMargins &margins); + +namespace WPixman { + +// QRegion overloads for Qt / wlr edges. +inline QRegion dilateRegion(const QRegion &r, const QMargins &m) +{ + if (r.isEmpty()) + return { }; + if (m.isNull()) + return r; + if (r.rectCount() == 1) + return QRegion(r.boundingRect().marginsAdded(m)); + QRegion out; + for (const QRect &rect : r) + out += rect.marginsAdded(m); + return out; +} + +} // namespace WPixman + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/utils/wtools.cpp b/waylib/src/server/utils/wtools.cpp index 7d5fd119d9..1fb3255f2f 100644 --- a/waylib/src/server/utils/wtools.cpp +++ b/waylib/src/server/utils/wtools.cpp @@ -115,9 +115,9 @@ QImage::Format WTools::toImageFormat(uint32_t drmFormat) case DRM_FORMAT_RGB565: return QImage::Format_RGB16; case DRM_FORMAT_RGB888: - return QImage::Format_RGB888; - case DRM_FORMAT_BGR888: return QImage::Format_BGR888; + case DRM_FORMAT_BGR888: + return QImage::Format_RGB888; case DRM_FORMAT_XRGB8888: return QImage::Format_RGB32; case DRM_FORMAT_RGBX8888: @@ -128,6 +128,8 @@ QImage::Format WTools::toImageFormat(uint32_t drmFormat) return QImage::Format_RGBA8888; case DRM_FORMAT_ABGR8888: return QImage::Format_RGBA8888_Premultiplied; + case DRM_FORMAT_XBGR8888: + return QImage::Format_RGBX8888; case DRM_FORMAT_XRGB2101010: return QImage::Format_RGB30; case DRM_FORMAT_BGRX1010102: @@ -158,9 +160,9 @@ uint32_t WTools::toDrmFormat(QImage::Format format) case QImage::Format_RGB16: return DRM_FORMAT_RGB565; case QImage::Format_RGB888: - return DRM_FORMAT_RGB888; - case QImage::Format_BGR888: return DRM_FORMAT_BGR888; + case QImage::Format_BGR888: + return DRM_FORMAT_RGB888; case QImage::Format_RGB32: return DRM_FORMAT_XRGB8888; case QImage::Format_RGBX8888: @@ -312,17 +314,4 @@ Qt::Edges WTools::toQtEdge(uint32_t edges) return qedges; } -// WPixmanRegion implementation -WPixmanRegion::WPixmanRegion() { - pixman_region32_init(&r); -} - -WPixmanRegion::WPixmanRegion(int x, int y, int w, int h) { - pixman_region32_init_rect(&r, x, y, w, h); -} - -WPixmanRegion::~WPixmanRegion() { - pixman_region32_fini(&r); -} - WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/utils/wtools.h b/waylib/src/server/utils/wtools.h index 4c3675eb85..628fd1371a 100644 --- a/waylib/src/server/utils/wtools.h +++ b/waylib/src/server/utils/wtools.h @@ -8,14 +8,11 @@ #include #include #include +#include "wpixmanregion.h" struct pixman_region32; typedef int wl_shm_format_t; -extern "C" { -#include -} - QT_BEGIN_NAMESPACE class QQuickItem; QT_END_NAMESPACE @@ -38,27 +35,6 @@ class WAYLIB_SERVER_EXPORT WTools static Qt::Edges toQtEdge(uint32_t edges); }; -// RAII wrapper for pixman region -struct WAYLIB_SERVER_EXPORT WPixmanRegion { - pixman_region32_t r; - - // Default constructor - initializes empty region - WPixmanRegion(); - // Constructor with rectangle - initializes with specific rect - WPixmanRegion(int x, int y, int w, int h); - ~WPixmanRegion(); - - pixman_region32_t* get() { return &r; } - - // Conversion operator for compatibility - inline operator pixman_region32_t*() { return &r; } - - // Utility method to check if region is empty - inline bool isEmpty() const { - return !pixman_region32_not_empty(&r); - } -}; - WAYLIB_SERVER_END_NAMESPACE #endif // WTOOLS_H diff --git a/waylib/src/server/wayliblogging.cpp b/waylib/src/server/wayliblogging.cpp index 89ebe5de43..b195b3bb0c 100644 --- a/waylib/src/server/wayliblogging.cpp +++ b/waylib/src/server/wayliblogging.cpp @@ -40,6 +40,7 @@ Q_LOGGING_CATEGORY(lcWlRenderer, "waylib.renderer", QtDebugMsg) #else Q_LOGGING_CATEGORY(lcWlRenderer, "waylib.renderer", QtWarningMsg) #endif +Q_LOGGING_CATEGORY(lcWlDamage, "waylib.renderer.damage", QtWarningMsg) // Scene damage tracker, blitter recopy, flush Q_LOGGING_CATEGORY(lcWlBufferItem, "waylib.qtquick.bufferitem", QtInfoMsg) // Buffer item commit and damage // Utils diff --git a/waylib/src/server/wayliblogging.h b/waylib/src/server/wayliblogging.h index 31d553316c..770bec00bb 100644 --- a/waylib/src/server/wayliblogging.h +++ b/waylib/src/server/wayliblogging.h @@ -31,6 +31,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcWlTextureProvider) Q_DECLARE_LOGGING_CATEGORY(lcWlSurface) Q_DECLARE_LOGGING_CATEGORY(lcWlQtQuickTexture) Q_DECLARE_LOGGING_CATEGORY(lcWlRenderer) +Q_DECLARE_LOGGING_CATEGORY(lcWlDamage) Q_DECLARE_LOGGING_CATEGORY(lcWlBufferItem) // Utils diff --git a/waylib/tests/manual/CMakeLists.txt b/waylib/tests/manual/CMakeLists.txt index 8662fa5ac4..db3d0548ab 100644 --- a/waylib/tests/manual/CMakeLists.txt +++ b/waylib/tests/manual/CMakeLists.txt @@ -2,3 +2,5 @@ add_subdirectory(subsurface) add_subdirectory(cursor) add_subdirectory(pinchhandler) add_subdirectory(live) +add_subdirectory(damage) +add_subdirectory(damagegraph-demo) diff --git a/waylib/tests/manual/damage/CMakeLists.txt b/waylib/tests/manual/damage/CMakeLists.txt new file mode 100644 index 0000000000..266ae75df2 --- /dev/null +++ b/waylib/tests/manual/damage/CMakeLists.txt @@ -0,0 +1,52 @@ +find_package(Qt6 COMPONENTS Quick QuickControls2 Qml Gui REQUIRED) +qt_standard_project_setup(REQUIRES 6.4) + +if(QT_KNOWN_POLICY_QTP0001) # this policy was introduced in Qt 6.5 + qt_policy(SET QTP0001 NEW) + # the RESOURCE_PREFIX argument for qt_add_qml_module() defaults to ":/qt/qml/" +endif() +if(POLICY CMP0071) + # https://cmake.org/cmake/help/latest/policy/CMP0071.html + cmake_policy(SET CMP0071 NEW) +endif() + + +find_package(PkgConfig REQUIRED) +pkg_search_module(PIXMAN REQUIRED IMPORTED_TARGET pixman-1) +pkg_search_module(WAYLAND REQUIRED IMPORTED_TARGET wayland-server) + +add_executable(damage + main.cpp +) + +qt_add_qml_module(damage + URI DamagePlayground + VERSION "1.0" + QML_FILES + Main.qml + DamageView.qml + SOURCES + helper.h + demoscene.h + demoscene.cpp + damageoverlay.h + damageoverlay.cpp + visualnodemodel.h + visualnodemodel.cpp +) + +target_compile_definitions(damage + PRIVATE + WLR_USE_UNSTABLE +) + +target_link_libraries(damage + PRIVATE + Qt6::Quick + Qt6::QuickControls2 + Qt6::Qml + Qt6::Gui + waylibserver + PkgConfig::PIXMAN + PkgConfig::WAYLAND +) diff --git a/waylib/tests/manual/damage/DamageView.qml b/waylib/tests/manual/damage/DamageView.qml new file mode 100644 index 0000000000..1d91e33aef --- /dev/null +++ b/waylib/tests/manual/damage/DamageView.qml @@ -0,0 +1,1424 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import DamagePlayground + +Page { + id: win + anchors.fill: parent + padding: 0 + background: Rectangle { color: "#0a0f1a" } + readonly property color color: "#0a0f1a" + + readonly property color panelColor: "#111827" + readonly property color raisedColor: "#172033" + readonly property color borderColor: "#263247" + readonly property color textColor: "#f3f6fb" + readonly property color mutedColor: "#8996aa" + readonly property color accentColor: "#6d7cff" + property var sel: scene.selectedProps + property int damageHistoryDuration: 200 + readonly property int detectedRefreshRate: 60 + property int sceneRefreshRate: detectedRefreshRate + property bool treeDragging: false + property int treeDragId: 0 + property string treeDragName: "" + property int treeDropTargetId: 0 + property int treeDropParentId: 0 + property int treeDropBeforeId: 0 + property int treeDropMode: 0 + property real treePressX: 0 + property real treePressY: 0 + property real treeGhostX: 0 + property real treeGhostY: 0 + property string treeDropHint: "" + property bool demoPausedByInteraction: false + + function beginUserInteraction() { + if (scene.demoRunning) { + demoPausedByInteraction = true + scene.demoRunning = false + } + } + + function endUserInteraction() { + if (demoPausedByInteraction) { + demoPausedByInteraction = false + scene.demoRunning = true + } + } + + DemoScene { + id: scene + refreshRate: win.sceneRefreshRate + Component.onCompleted: loadDemoScene("occlusion") + } + + Connections { + target: scene + function onSceneChanged() { + if (!win.treeDragging) + win.resetTreeDrag() + } + } + + function num(v, fallback) { + const n = Number(v) + return Number.isFinite(n) ? n : (fallback ?? 0) + } + + function typeLabel(type) { + if (type === "Geometry") + return "几何节点" + if (type === "Backdrop") + return "背景采样节点" + if (type === "Transform") + return "变换节点" + return "分组节点" + } + + function revealSelectedNode() { + for (let i = 0; i < scene.treeNodes.length; ++i) { + if (scene.treeNodes[i].id === scene.selectedId) { + tree.positionViewAtIndex(i, ListView.Contain) + return + } + } + } + function isNodeDescendantOf(targetId, ancestorId) { + if (!targetId || !ancestorId) + return false + if (targetId === ancestorId) + return true + let curr = targetId + while (curr !== 0) { + let found = false + for (let i = 0; i < scene.treeNodes.length; ++i) { + const item = scene.treeNodes[i] + if (item.id === curr) { + if (item.parentId === ancestorId) + return true + curr = item.parentId || 0 + found = true + break + } + } + if (!found || curr === 0) + break + } + return false + } + + function calculateTreeDrop(mouseXInTree, mouseYInTree) { + const count = scene.treeNodes.length + if (!count || !treeDragId) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "" + return + } + + let index = tree.indexAt(mouseXInTree, mouseYInTree) + if (index < 0) { + if (mouseYInTree <= 0) + index = 0 + else + index = count - 1 + } + index = Math.max(0, Math.min(count - 1, index)) + const target = scene.treeNodes[index] + if (!target) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "" + return + } + + if (target.id === treeDragId) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "保持原位置" + return + } + if (isNodeDescendantOf(target.id, treeDragId)) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "无法移动至子孙节点" + return + } + + // Derive the stride from delegate geometry instead of hard-coding 38. + let rowStride = 36 + tree.spacing + if (tree.contentItem.children.length > 0) + rowStride = tree.contentItem.children[0].height + tree.spacing + const rowTop = index * rowStride - tree.contentY + const relY = mouseYInTree - rowTop + let dragParentId = 0 + let dragIndex = -1 + for (let i = 0; i < count; ++i) { + if (scene.treeNodes[i].id === treeDragId) { + dragParentId = scene.treeNodes[i].parentId || 0 + dragIndex = i + break + } + } + + if (target.id === scene.treeNodes[0].id) { + treeDropTargetId = target.id + treeDropMode = 2 + treeDropParentId = target.id + treeDropBeforeId = 0 + treeDropHint = "作为 " + target.name + " 的子节点" + return + } + + if (relY < 10) { + const targetParentId = target.parentId || 0 + if (targetParentId === dragParentId && dragIndex >= 0 && index === dragIndex + 1) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "保持原位置" + return + } + treeDropTargetId = target.id + treeDropMode = 1 + treeDropParentId = targetParentId + treeDropBeforeId = target.id + treeDropHint = "插入到 " + target.name + " 之前" + } else if (relY > 26) { + const targetParentId = target.parentId || 0 + if (targetParentId === dragParentId && dragIndex >= 0 && index === dragIndex - 1) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "保持原位置" + return + } + treeDropTargetId = target.id + treeDropMode = 3 + treeDropParentId = targetParentId + // treeNodes is a pre-order flattening: the target's descendants + // sit right behind it, so skip them to find the next sibling. + const targetDepth = target.depth || 0 + let nextId = 0 + for (let j = index + 1; j < count; ++j) { + const following = scene.treeNodes[j] + if ((following.depth || 0) > targetDepth) + continue + if ((following.parentId || 0) === targetParentId) + nextId = following.id + break + } + treeDropBeforeId = nextId + treeDropHint = "插入到 " + target.name + " 之后" + } else { + treeDropTargetId = target.id + treeDropMode = 2 + treeDropParentId = target.id + treeDropBeforeId = 0 + treeDropHint = "作为 " + target.name + " 的子节点" + } + } + function resetTreeDrag() { + treeDragging = false + treeDragId = 0 + treeDropTargetId = 0 + treeDropParentId = 0 + treeDropBeforeId = 0 + treeDropMode = 0 + treeDropHint = "" + treeDragName = "" + } + Action { + id: addGeometryAction + text: "几何节点" + shortcut: "Ctrl+Shift+G" + onTriggered: scene.addGeometry() + } + Action { + id: addTransformAction + text: "变换节点" + shortcut: "Ctrl+Shift+T" + onTriggered: scene.addTransform() + } + Action { + id: addClipAction + text: "裁剪节点" + onTriggered: scene.addClip() + } + Action { + id: addBackdropAction + text: "背景采样节点" + onTriggered: scene.addBackdrop() + } + Action { + id: addRendererAction + text: "自定义渲染节点" + onTriggered: scene.addRenderer() + } + Action { + id: addGroupAction + text: "分组节点" + onTriggered: scene.addBasic() + shortcut: "Ctrl+Shift+N" + } + Action { + id: moveUpAction + text: "上移一层" + enabled: !!win.sel.canRaise + onTriggered: scene.raiseSelected() + } + Action { + id: moveDownAction + text: "下移一层" + enabled: !!win.sel.canLower + onTriggered: scene.lowerSelected() + } + Action { + id: dirtyAction + text: "标记内容损伤" + shortcut: "Ctrl+D" + enabled: !!win.sel.canDirty + onTriggered: scene.markSelectedContentDirty() + } + Action { + id: deleteAction + text: "删除节点" + shortcut: "Delete" + enabled: !!win.sel.canDelete + onTriggered: scene.removeSelected() + } + + Menu { + id: addNodeMenu + y: addNodeButton.height + 6 + MenuItem { action: addGeometryAction } + MenuItem { action: addTransformAction } + MenuItem { action: addClipAction } + MenuItem { action: addBackdropAction } + MenuItem { action: addRendererAction } + MenuSeparator {} + MenuItem { action: addGroupAction } + } + + Menu { + id: nodeMenu + title: win.sel.name || "节点" + Menu { + title: "新增子节点" + MenuItem { action: addGeometryAction } + MenuItem { action: addTransformAction } + MenuItem { action: addClipAction } + MenuItem { action: addBackdropAction } + MenuItem { action: addRendererAction } + MenuItem { action: addGroupAction } + } + MenuSeparator {} + MenuItem { action: moveUpAction } + MenuItem { action: moveDownAction } + MenuItem { action: visibilityAction } + MenuItem { action: dirtyAction } + MenuSeparator {} + MenuItem { action: deleteAction } + } + + Menu { + id: treeOptionsMenu + y: treeOptionsButton.height + 6 + MenuItem { + text: "选择根节点" + onTriggered: scene.selectedId = scene.treeNodes.length ? scene.treeNodes[0].id : 0 + } + MenuItem { + text: "立即提交" + onTriggered: scene.commit() + } + MenuSeparator {} + MenuItem { + text: "清空节点树" + onTriggered: scene.clearTree() + } + } + + header: Rectangle { + height: 72 + color: "#0f1625" + border.color: win.borderColor + border.width: 1 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 22 + anchors.rightMargin: 18 + spacing: 18 + + ColumnLayout { + spacing: 1 + Layout.minimumWidth: 250 + Label { + text: "损伤跟踪器" + color: win.textColor + font.pixelSize: 20 + font.weight: Font.DemiBold + } + Label { + text: "场景树、遮挡剔除与多输出损伤" + color: win.mutedColor + font.pixelSize: 11 + } + } + + Item { Layout.fillWidth: true } + + RowLayout { + spacing: 10 + Label { text: "图例"; color: win.mutedColor; font.pixelSize: 10 } + Rectangle { + width: 40; height: 16; radius: 4 + border.color: "#8794ff" + gradient: Gradient { + GradientStop { position: 0; color: "#a6b0ff" } + GradientStop { position: 1; color: "#4e5ee8" } + } + } + Label { text: "节点"; color: win.mutedColor; font.pixelSize: 10 } + Rectangle { width: 40; height: 16; radius: 3; color: "#2dc470" } + Label { text: "较旧损伤"; color: win.mutedColor; font.pixelSize: 10 } + Rectangle { width: 40; height: 16; radius: 3; color: "#e83e4e" } + Label { text: "最新损伤"; color: win.mutedColor; font.pixelSize: 10 } + + Rectangle { width: 1; height: 34; color: win.borderColor } + + Label { text: "损伤高亮"; color: win.mutedColor; font.pixelSize: 10 } + ComboBox { + id: damageModeBox + Layout.preferredWidth: 120 + model: ["highlight", "off"] + Component.onCompleted: currentIndex = Math.max(0, model.indexOf(Helper.damageVisual)) + onActivated: Helper.damageVisual = currentText + ToolTip.visible: hovered + ToolTip.text: "OutputRenderWindow highlight/off(WAYLIB_DEBUG_DAMAGE)" + Connections { + target: Helper + function onDamageDebugModeChanged() { + damageModeBox.currentIndex = Math.max(0, damageModeBox.model.indexOf(Helper.damageDebugMode)) + } + } + } + + Rectangle { width: 1; height: 34; color: win.borderColor } + + ComboBox { + id: demoSceneCombo + Layout.preferredWidth: 150 + model: scene.demoScenes + textRole: "text" + valueRole: "value" + onActivated: scene.loadDemoScene(currentValue) + ToolTip.visible: hovered + ToolTip.text: "选择只读自动演示场景" + } + Button { + text: scene.demoRunning ? "暂停演示" : "继续演示" + onClicked: scene.demoRunning = !scene.demoRunning + } + Label { + text: scene.demoRunning ? "自动演示中" : "演示已暂停" + color: scene.demoRunning ? "#8dc8e8" : "#ffcf70" + font.pixelSize: 10 + } + Switch { + text: "自动提交" + checked: scene.autoCommit + onToggled: scene.autoCommit = checked + ToolTip.visible: hovered + ToolTip.text: "每次编辑后自动提交" + } + Button { + text: "提交" + highlighted: true + onClicked: scene.commit() + } + } + } + + } + SplitView { + anchors.fill: parent + orientation: Qt.Horizontal + + Rectangle { + SplitView.preferredWidth: 330 + SplitView.minimumWidth: 285 + color: win.panelColor + border.color: win.borderColor + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 10 + + RowLayout { + Layout.fillWidth: true + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Label { + text: "节点树" + color: win.mutedColor + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + } + Label { + text: "共 " + scene.treeNodes.length + " 个节点" + color: win.textColor + font.pixelSize: 15 + font.weight: Font.DemiBold + } + } + ToolButton { + id: treeOptionsButton + text: "•••" + onClicked: treeOptionsMenu.popup(treeOptionsButton, 0, + treeOptionsButton.height + 6) + ToolTip.visible: hovered + ToolTip.text: "节点树菜单" + } + } + + RowLayout { + Layout.fillWidth: true + Button { + id: addNodeButton + Layout.fillWidth: true + text: "+ 新增子节点" + onClicked: addNodeMenu.popup(addNodeButton, 0, + addNodeButton.height + 6) + } + ToolButton { + text: "↑" + enabled: moveUpAction.enabled + onClicked: moveUpAction.trigger() + ToolTip.visible: hovered + ToolTip.text: "上移选中节点" + } + ToolButton { + text: "↓" + enabled: moveDownAction.enabled + onClicked: moveDownAction.trigger() + ToolTip.visible: hovered + ToolTip.text: "下移选中节点" + } + } + + Label { + Layout.fillWidth: true + text: "新节点将添加到 " + (win.sel.name || "根节点") + " 下" + color: win.mutedColor + font.pixelSize: 10 + elide: Text.ElideRight + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#0d1422" + radius: 8 + border.color: win.borderColor + + ListView { + id: tree + anchors.fill: parent + anchors.margins: 6 + clip: true + model: scene.treeNodes + spacing: 2 + boundsBehavior: Flickable.StopAtBounds + + delegate: Rectangle { + id: treeRow + required property var modelData + width: tree.width + height: 36 + radius: 6 + opacity: win.treeDragging && win.treeDragId === modelData.id ? 0.35 : 1.0 + color: modelData.id === scene.selectedId + ? "#283452" : (rowMouse.containsMouse ? "#182238" : "transparent") + border.color: modelData.id === scene.selectedId ? "#6476d9" : "transparent" + + Rectangle { + visible: modelData.depth > 0 + x: 12 + (modelData.depth - 1) * 17 + width: 1 + height: parent.height + color: "#2b3850" + } + + // Indicator: insert before + Rectangle { + visible: win.treeDragging && win.treeDropTargetId === modelData.id + && win.treeDropMode === 1 + anchors.left: parent.left + anchors.right: parent.right + y: -2 + height: 4 + radius: 2 + color: "#ff8794" + z: 10 + } + // Indicator: insert after + Rectangle { + visible: win.treeDragging && win.treeDropTargetId === modelData.id + && win.treeDropMode === 3 + anchors.left: parent.left + anchors.right: parent.right + y: parent.height - 2 + height: 4 + radius: 2 + color: "#ff8794" + z: 10 + } + // Indicator: become child + Rectangle { + visible: win.treeDragging && win.treeDropTargetId === modelData.id + && win.treeDropMode === 2 + anchors.fill: parent + color: "#2d4e78" + border.color: "#6db8ff" + border.width: 2 + radius: 6 + z: 5 + } + + Row { + anchors.verticalCenter: parent.verticalCenter + leftPadding: 10 + modelData.depth * 17 + spacing: 8 + Rectangle { + width: 18; height: 18; radius: 5 + anchors.verticalCenter: parent.verticalCenter + color: modelData.type === "Geometry" ? "#596de8" + : modelData.type === "Backdrop" ? "#0891b2" + : modelData.type === "Transform" ? "#d28a2d" : "#556176" + Label { + anchors.centerIn: parent + text: modelData.type === "Geometry" ? "G" + : modelData.type === "Backdrop" ? "B" + : modelData.type === "Transform" ? "T" : "·" + color: "#ffffff" + font.pixelSize: 9 + font.bold: true + } + } + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: -1 + Text { + text: modelData.name || win.typeLabel(modelData.type) + color: modelData.visible ? win.textColor : "#667289" + font.pixelSize: 12 + } + Text { + text: win.typeLabel(modelData.type) + + (modelData.occluded ? " · 已遮挡" : "") + + (modelData.culled && !modelData.occluded ? " · 已剔除" : "") + + (!modelData.visible ? " · 已隐藏" : "") + color: win.mutedColor + font.pixelSize: 9 + } + } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function(mouse) { + scene.selectedId = modelData.id + if (mouse.button === Qt.LeftButton) { + beginUserInteraction() + treePressX = mouse.x + treePressY = mouse.y + treeDragId = modelData.id + treeDragName = modelData.name + treeDragging = false + var p = rowMouse.mapToItem(win.contentItem, mouse.x, mouse.y) + treeGhostX = p.x + 10 + treeGhostY = p.y + 10 + } else if (mouse.button === Qt.RightButton) { + nodeMenu.popup(treeRow, mouse.x, mouse.y) + } + } + onPositionChanged: function(mouse) { + if (!(mouse.buttons & Qt.LeftButton)) + return + if (!win.treeDragging + && (Math.abs(mouse.x - treePressX) + Math.abs(mouse.y - treePressY) > 4)) + win.treeDragging = true + + if (win.treeDragging) { + var winPt = rowMouse.mapToItem(win.contentItem, mouse.x, mouse.y) + treeGhostX = winPt.x + 12 + treeGhostY = winPt.y + 12 + var treePt = rowMouse.mapToItem(tree, mouse.x, mouse.y) + calculateTreeDrop(treePt.x, treePt.y) + } + } + onReleased: function(mouse) { + if (mouse.button === Qt.LeftButton) { + if (win.treeDragging) { + const dragId = win.treeDragId + const dropParentId = win.treeDropParentId + const dropBeforeId = win.treeDropBeforeId + const hasValidDrop = (dragId !== 0 && win.treeDropTargetId !== 0) + // Single reset, single commit: the duplicated + // resetTreeDrag/moveNode below double-committed drops. + win.resetTreeDrag() + endUserInteraction() + if (hasValidDrop) { + Qt.callLater(function() { + scene.moveNode(dragId, dropParentId, dropBeforeId) + }) + } + } else { + endUserInteraction() + } + } + } + onCanceled: { + win.resetTreeDrag() + endUserInteraction() + } + } + } + } + } + + Label { + Layout.fillWidth: true + text: "拖动节点可调整顺序或改变父子关系;右键点击节点打开操作菜单。" + wrapMode: Text.Wrap + color: win.mutedColor + font.pixelSize: 10 + } + } + } + + Rectangle { + SplitView.fillWidth: true + SplitView.minimumWidth: 480 + color: win.color + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 10 + + RowLayout { + Layout.fillWidth: true + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Label { + text: "场景预览" + color: win.mutedColor + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + } + Label { + text: "拖动可见节点即可修改几何位置" + color: win.textColor + font.pixelSize: 14 + font.weight: Font.DemiBold + } + } + Rectangle { + implicitWidth: damageStatus.implicitWidth + 18 + implicitHeight: 28 + radius: 14 + color: (scene.damageRects.length || scene.damageRectsB.length) ? "#34202c" : "#172638" + border.color: (scene.damageRects.length || scene.damageRectsB.length) ? "#8e3f55" : "#2b526a" + Label { + id: damageStatus + anchors.centerIn: parent + text: (scene.damageRects.length || scene.damageRectsB.length) + ? ("损伤 A " + scene.damageRects.length + " · B " + scene.damageRectsB.length) + : "无损伤" + color: (scene.damageRects.length || scene.damageRectsB.length) ? "#ffacba" : "#8dc8e8" + font.pixelSize: 10 + font.weight: Font.DemiBold + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: 10 + color: "#090e18" + border.color: win.borderColor + clip: true + + Item { + id: canvas + anchors.fill: parent + anchors.margins: 10 + clip: true + + Canvas { + anchors.fill: parent + onPaint: { + const ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = "#0b1220" + ctx.fillRect(0, 0, width, height) + ctx.strokeStyle = "#172238" + ctx.lineWidth = 1 + const s = 24 + for (let x = 0.5; x < width; x += s) { + ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke() + } + for (let y = 0.5; y < height; y += s) { + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke() + } + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + Repeater { + model: scene.visualNodes + delegate: Item { + id: visualItem + + + width: 0 + height: 0 + visible: model.visible + + transform: Matrix4x4 { + matrix: Qt.matrix4x4( + model.m11, model.m21, 0, model.dx, + model.m12, model.m22, 0, model.dy, + 0, 0, 1, 0, + model.m13, model.m23, 0, model.m33 + ) + } + + Rectangle { + id: nodeRect + visible: model.hasContent + property color baseColor: model.color + property real fillAlpha: model.fullyOpaque + ? 1.0 + : (model.isBackdrop ? 0.46 : 0.58) + x: model.localX + y: model.localY + width: model.localWidth + height: model.localHeight + color: "transparent" + radius: model.isBackdrop ? 12 : 7 + border.color: model.id === scene.selectedId + ? "#ffffff" : Qt.alpha(Qt.lighter(baseColor, 1.35), 0.95) + border.width: model.id === scene.selectedId ? 2 : 1 + gradient: Gradient { + GradientStop { + position: 0 + color: Qt.alpha(Qt.lighter(nodeRect.baseColor, 1.35), nodeRect.fillAlpha) + } + GradientStop { + position: 0.55 + color: Qt.alpha(nodeRect.baseColor, nodeRect.fillAlpha) + } + GradientStop { + position: 1 + color: Qt.alpha(Qt.darker(nodeRect.baseColor, 1.45), nodeRect.fillAlpha) + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: Math.min(30, parent.height) + radius: parent.radius + color: "#26000000" + } + Text { + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 8 + text: model.name + + (model.occluded ? " · 已遮挡" : (model.culled ? " · 已剔除" : "")) + color: "#ffffff" + font.pixelSize: 11 + font.weight: Font.DemiBold + style: Text.Outline + styleColor: "#70000000" + } + MouseArea { + property real lastCanvasX: 0 + property real lastCanvasY: 0 + property bool isDragging: false + property bool interactionStarted: false + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: function(mouse) { + if (mouse.button === Qt.LeftButton) { + scene.activateNode(model.id) + isDragging = true + interactionStarted = false + var p = mapToItem(canvas, mouse.x, mouse.y) + lastCanvasX = p.x + lastCanvasY = p.y + } else { + scene.selectedId = model.id + nodeMenu.popup(nodeRect, mouse.x, mouse.y) + } + } + onPositionChanged: function(mouse) { + if (!isDragging || !(mouse.buttons & Qt.LeftButton)) + return + var p = mapToItem(canvas, mouse.x, mouse.y) + var dx = p.x - lastCanvasX + var dy = p.y - lastCanvasY + if (dx === 0 && dy === 0) + return + if (!interactionStarted) { + interactionStarted = true + beginUserInteraction() + } + lastCanvasX = p.x + lastCanvasY = p.y + scene.moveSelectedBy(dx, dy) + } + onReleased: function(mouse) { + if (mouse.button === Qt.LeftButton) { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + onCanceled: { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + } + Rectangle { + id: clipRect + visible: model.type === "Clip" + x: model.localX + y: model.localY + width: model.localWidth + height: model.localHeight + color: Qt.alpha(model.color, 0.08) + border.color: model.id === scene.selectedId ? "#ffffff" : model.color + border.width: model.id === scene.selectedId ? 2 : 1 + radius: 2 + Text { + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 6 + text: model.name + color: "#d8f9ff" + font.pixelSize: 10 + font.weight: Font.DemiBold + style: Text.Outline + styleColor: "#70000000" + } + MouseArea { + property real lastCanvasX: 0 + property real lastCanvasY: 0 + property bool isDragging: false + property bool interactionStarted: false + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: function(mouse) { + if (mouse.button === Qt.LeftButton) { + scene.selectedId = model.id + isDragging = true + interactionStarted = false + var p = mapToItem(canvas, mouse.x, mouse.y) + lastCanvasX = p.x + lastCanvasY = p.y + } else { + scene.selectedId = model.id + nodeMenu.popup(clipRect, mouse.x, mouse.y) + } + } + onPositionChanged: function(mouse) { + if (!isDragging || !(mouse.buttons & Qt.LeftButton)) + return + var p = mapToItem(canvas, mouse.x, mouse.y) + var dx = p.x - lastCanvasX + var dy = p.y - lastCanvasY + if (dx === 0 && dy === 0) + return + if (!interactionStarted) { + interactionStarted = true + beginUserInteraction() + } + lastCanvasX = p.x + lastCanvasY = p.y + scene.moveSelectedBy(dx, dy) + } + onReleased: function(mouse) { + if (mouse.button === Qt.LeftButton) { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + onCanceled: { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + } + } + } + + Rectangle { + x: scene.viewportA.x; y: scene.viewportA.y + width: scene.viewportA.width; height: scene.viewportA.height + color: "transparent"; border.color: "#78526079"; border.width: 2 + } + Rectangle { + x: scene.viewportB.x; y: scene.viewportB.y + width: scene.viewportB.width; height: scene.viewportB.height + color: "transparent"; border.color: "#78526079"; border.width: 2 + } + Label { + x: scene.viewportA.x + 10; y: scene.viewportA.y + 8 + text: "输出 A"; color: win.mutedColor; font.pixelSize: 9; font.weight: Font.DemiBold + } + Label { + x: scene.viewportB.x + 10; y: scene.viewportB.y + 8 + text: "输出 B"; color: win.mutedColor; font.pixelSize: 9; font.weight: Font.DemiBold + } + + // 真实损伤由 WSGBatchRenderer 高亮,模拟层默认隐藏 + Rectangle { + anchors.fill: parent + color: "transparent" + visible: Helper.damageDebugMode === "highlight" + Text { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 8 + text: "WSG 真实高亮(WSGDamageDebug)" + color: "#66ffacba" + font.pixelSize: 9 + } + } + + Connections { + target: scene + function onSelectedIdChanged() { Qt.callLater(win.revealSelectedNode) } + } + } + } + } + } + + Rectangle { + SplitView.preferredWidth: 300 + SplitView.minimumWidth: 270 + color: win.panelColor + border.color: win.borderColor + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 10 + + Label { + text: "属性检查器" + color: win.mutedColor + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + } + + RowLayout { + Layout.fillWidth: true + Label { + text: "损伤残留时间" + color: win.textColor + Layout.fillWidth: true + } + SpinBox { + id: damageDurationEditor + Layout.preferredWidth: 112 + from: 100 + to: 10000 + stepSize: 100 + value: win.damageHistoryDuration + editable: true + onValueModified: win.damageHistoryDuration = value + ToolTip.visible: hovered + ToolTip.text: "损伤帧保持可见的时间" + } + Label { + text: "ms" + color: win.mutedColor + font.pixelSize: 10 + } + } + + RowLayout { + Layout.fillWidth: true + Label { + text: "刷新率" + color: win.textColor + Layout.fillWidth: true + } + SpinBox { + id: refreshRateEditor + Layout.preferredWidth: 112 + from: 1 + to: 360 + stepSize: 1 + value: win.sceneRefreshRate + editable: true + onValueModified: win.sceneRefreshRate = value + ToolTip.visible: hovered + ToolTip.text: "拖动提交和损伤重绘的刷新率" + } + Label { + text: "Hz" + color: win.mutedColor + font.pixelSize: 10 + } + ToolButton { + text: "↺" + onClicked: win.sceneRefreshRate = win.detectedRefreshRate + ToolTip.visible: hovered + ToolTip.text: "恢复为当前屏幕刷新率" + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 62 + radius: 8 + color: win.raisedColor + border.color: win.borderColor + RowLayout { + anchors.fill: parent + anchors.margins: 10 + Rectangle { + width: 34; height: 34; radius: 9 + color: win.sel.type === "Geometry" ? "#596de8" + : win.sel.type === "Backdrop" ? "#0891b2" + : win.sel.type === "Transform" ? "#d28a2d" : "#556176" + Label { + anchors.centerIn: parent + text: win.sel.type ? win.sel.type.charAt(0) : "–" + color: "#ffffff" + font.bold: true + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + Label { + text: win.sel.name || "未选择节点" + color: win.textColor + font.pixelSize: 14 + font.weight: Font.DemiBold + elide: Text.ElideRight + Layout.fillWidth: true + } + Label { + text: win.sel.type ? (win.typeLabel(win.sel.type) + " · ID " + win.sel.id) + : "在节点树或画布中选择节点" + color: win.mutedColor + font.pixelSize: 10 + } + } + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + visible: !!win.sel.id + + Label { text: "常规"; color: win.mutedColor; font.pixelSize: 9; font.bold: true } + RowLayout { + Layout.fillWidth: true + Label { text: "固定名称"; color: win.mutedColor } + Label { + Layout.fillWidth: true + text: win.sel.name || "" + color: win.textColor + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + } + } + RowLayout { + Layout.fillWidth: true + Label { text: "可见"; color: win.textColor; Layout.fillWidth: true } + Switch { checked: !!win.sel.visible; onToggled: scene.setVisibleSelected(checked) } + } + RowLayout { + Layout.fillWidth: true + visible: !!win.sel.isGeometry + Label { text: "完全不透明"; color: win.textColor; Layout.fillWidth: true } + Switch { checked: !!win.sel.fullyOpaque; onToggled: scene.setFullyOpaqueSelected(checked) } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: win.borderColor } + Label { + visible: !!win.sel.isGeometry || !!win.sel.isClip + text: win.sel.isClip ? "裁剪区域" : "几何属性" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isGeometry || !!win.sel.isClip + Layout.fillWidth: true + columns: 2 + columnSpacing: 8 + rowSpacing: 8 + Label { text: "X"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.x); editable: true + onValueModified: scene.setRectSelected(value, win.num(win.sel.y), win.num(win.sel.w), win.num(win.sel.h)) + } + Label { text: "Y"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.y); editable: true + onValueModified: scene.setRectSelected(win.num(win.sel.x), value, win.num(win.sel.w), win.num(win.sel.h)) + } + Label { text: "宽度"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: 0; to: 2000 + value: win.num(win.sel.w); editable: true + onValueModified: scene.setRectSelected(win.num(win.sel.x), win.num(win.sel.y), value, win.num(win.sel.h)) + } + Label { text: "高度"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: 0; to: 2000 + value: win.num(win.sel.h); editable: true + onValueModified: scene.setRectSelected(win.num(win.sel.x), win.num(win.sel.y), win.num(win.sel.w), value) + } + } + + Label { + visible: !!win.sel.isTransform + text: "变换属性" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isTransform + Layout.fillWidth: true + columns: 2 + columnSpacing: 8 + rowSpacing: 8 + Label { text: "水平位移"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.tx); editable: true + onValueModified: scene.setTranslationSelected(value, win.num(win.sel.ty)) + } + Label { text: "垂直位移"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.ty); editable: true + onValueModified: scene.setTranslationSelected(win.num(win.sel.tx), value) + } + Label { text: "旋转轴"; color: win.mutedColor } + ComboBox { + id: rotationAxisCombo + Layout.fillWidth: true + model: [ + { text: "Z 轴 (平面旋转)", axis: 2 }, + { text: "X 轴 (水平翻转)", axis: 0 }, + { text: "Y 轴 (垂直翻转)", axis: 1 } + ] + textRole: "text" + valueRole: "axis" + currentIndex: win.num(win.sel.rotationAxis, 2) === 0 ? 1 + : (win.num(win.sel.rotationAxis, 2) === 1 ? 2 : 0) + onActivated: scene.setRotationSelected(rotationSpinBox.value, currentValue) + } + Label { text: "旋转角度"; color: win.mutedColor } + SpinBox { + id: rotationSpinBox + Layout.fillWidth: true + from: -360 + to: 360 + value: Math.round(win.num(win.sel.rotation)) + editable: true + onValueModified: scene.setRotationSelected(value, rotationAxisCombo.currentValue) + } + Label { text: "水平缩放"; color: win.mutedColor } + TextField { + Layout.fillWidth: true + text: win.num(win.sel.sx, 1).toFixed(3) + validator: DoubleValidator { bottom: 0.01; top: 20; decimals: 3 } + onEditingFinished: scene.setScaleSelected(Number(text), win.num(win.sel.sy, 1)) + } + Label { text: "垂直缩放"; color: win.mutedColor } + TextField { + Layout.fillWidth: true + text: win.num(win.sel.sy, 1).toFixed(3) + validator: DoubleValidator { bottom: 0.01; top: 20; decimals: 3 } + onEditingFinished: scene.setScaleSelected(win.num(win.sel.sx, 1), Number(text)) + } + } + Label { + visible: !!win.sel.isTransform + text: "矩阵" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isTransform + Layout.fillWidth: true + columns: 3 + columnSpacing: 8 + rowSpacing: 4 + Label { text: "m11"; color: win.mutedColor } + Label { text: win.num(win.sel.m11).toFixed(3); color: win.textColor } + Label { text: "m12 " + win.num(win.sel.m12).toFixed(3); color: win.textColor } + Label { text: "m21"; color: win.mutedColor } + Label { text: win.num(win.sel.m21).toFixed(3); color: win.textColor } + Label { text: "m22 " + win.num(win.sel.m22).toFixed(3); color: win.textColor } + Label { text: "dx"; color: win.mutedColor } + Label { text: win.num(win.sel.dx).toFixed(3); color: win.textColor } + Label { text: "dy " + win.num(win.sel.dy).toFixed(3); color: win.textColor } + + } + Label { + visible: !!win.sel.isBackdrop + text: "背景采样属性" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isBackdrop + Layout.fillWidth: true + columns: 2 + columnSpacing: 8 + rowSpacing: 8 + Label { text: "扩张范围"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: 0; to: 80 + value: win.num(win.sel.expansion); editable: true + onValueModified: scene.setExpansionSelected(value) + } + Label { text: "裁剪扩张"; color: win.mutedColor } + Switch { + checked: win.sel.clipExpansion !== false + onToggled: scene.setClipExpansionSelected(checked) + } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: win.borderColor } + RowLayout { + Layout.fillWidth: true + Button { text: "上移"; enabled: moveUpAction.enabled; onClicked: moveUpAction.trigger() } + Button { text: "下移"; enabled: moveDownAction.enabled; onClicked: moveDownAction.trigger() } + Item { Layout.fillWidth: true } + Button { text: "标记损伤"; enabled: dirtyAction.enabled; onClicked: dirtyAction.trigger() } + } + Button { + Layout.fillWidth: true + text: "删除选中节点" + enabled: deleteAction.enabled + onClicked: deleteAction.trigger() + } + } + } + } + } + } + + Rectangle { + id: treeDragGhost + parent: win.contentItem + z: 99999 + visible: win.treeDragging + x: win.treeGhostX + y: win.treeGhostY + width: 230 + height: 38 + radius: 8 + color: "#f01c2a3f" + border.color: "#7399ff" + border.width: 1.5 + + Row { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 8 + Rectangle { + width: 20 + height: 20 + radius: 5 + anchors.verticalCenter: parent.verticalCenter + color: "#4e73df" + Label { + anchors.centerIn: parent + text: "↕" + color: "#ffffff" + font.pixelSize: 11 + font.bold: true + } + } + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: -1 + Label { + text: win.treeDragName || "节点" + color: "#ffffff" + font.pixelSize: 11 + font.bold: true + } + Label { + text: win.treeDropHint || "拖动中..." + color: "#8cd3ff" + font.pixelSize: 9 + } + } + } + } +} diff --git a/waylib/tests/manual/damage/Main.qml b/waylib/tests/manual/damage/Main.qml new file mode 100644 index 0000000000..489babbd8a --- /dev/null +++ b/waylib/tests/manual/damage/Main.qml @@ -0,0 +1,77 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import Waylib.Server +import DamagePlayground + +Item { + id: root + + Shortcut { + sequences: [StandardKey.Quit] + context: Qt.ApplicationShortcut + onActivated: Qt.quit() + } + + Shortcut { + sequence: "Space" + context: Qt.ApplicationShortcut + onActivated: { + // 占位:真实损伤演示由 DemoScene 控制 + } + } + + OutputRenderWindow { + id: renderWindow + width: 1440 + height: 860 + + Row { + id: outputsContainer + anchors.fill: parent + + DynamicCreatorComponent { + id: outputDelegateCreator + creator: Helper.outputCreator + + OutputItem { + id: rootOutputItem + required property WaylandOutput waylandOutput + readonly property OutputViewport onscreenViewport: outputViewport + + output: waylandOutput + devicePixelRatio: waylandOutput.scale + + cursorDelegate: Cursor { + required property QtObject outputCursor + readonly property point position: parent.mapFromGlobal(cursor.position.x, cursor.position.y) + cursor: outputCursor.cursor + output: outputCursor.output.output + x: position.x - hotSpot.x + y: position.y - hotSpot.y + visible: valid && outputCursor.visible + OutputLayer.enabled: true + OutputLayer.keepLayer: true + OutputLayer.flags: OutputLayer.Cursor + OutputLayer.cursorHotSpot: hotSpot + OutputLayer.outputs: [outputViewport] + } + + OutputViewport { + id: outputViewport + output: waylandOutput + devicePixelRatio: parent.devicePixelRatio + anchors.centerIn: parent + } + + // 真实 Waylib 损伤演示:布局与 damagegraph-demo 1:1,渲染经 WSGBatchRenderer + DamageView { + anchors.fill: parent + } + } + } + } + } +} diff --git a/waylib/tests/manual/damage/damageoverlay.cpp b/waylib/tests/manual/damage/damageoverlay.cpp new file mode 100644 index 0000000000..dea4b10c14 --- /dev/null +++ b/waylib/tests/manual/damage/damageoverlay.cpp @@ -0,0 +1,110 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "damageoverlay.h" + +#include +#include + + +static QColor interpolateColor(const QColor &newest, const QColor &oldest, qreal progress) +{ + const qreal t = qBound(0.0, progress, 1.0); + return QColor::fromRgbF(newest.redF() + (oldest.redF() - newest.redF()) * t, + newest.greenF() + (oldest.greenF() - newest.greenF()) * t, + newest.blueF() + (oldest.blueF() - newest.blueF()) * t, + newest.alphaF() + (oldest.alphaF() - newest.alphaF()) * t); +} + +static int refreshInterval(int refreshRate) +{ + return qMax(1, qRound(1000.0 / refreshRate)); +} + +DamageOverlay::DamageOverlay(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ + setAcceptedMouseButtons(Qt::NoButton); + setAntialiasing(false); + setFillColor(Qt::transparent); + m_repaintTimer.setInterval(refreshInterval(m_refreshRate)); + connect(&m_repaintTimer, &QTimer::timeout, this, [this] { + update(); + }); +} + +void DamageOverlay::setFrames(const QVariantList &frames) +{ + if (m_frames == frames) + return; + m_frames = frames; + if (!m_frames.isEmpty()) + m_repaintTimer.start(); + emit framesChanged(); + update(); +} + +void DamageOverlay::setHistoryDuration(int duration) +{ + duration = qMax(100, duration); + if (m_historyDuration == duration) + return; + m_historyDuration = duration; + emit historyDurationChanged(); + update(); +} + +void DamageOverlay::setRefreshRate(int refreshRate) +{ + refreshRate = qBound(1, refreshRate, 1000); + if (m_refreshRate == refreshRate) + return; + m_refreshRate = refreshRate; + m_repaintTimer.setInterval(refreshInterval(m_refreshRate)); + emit refreshRateChanged(); +} + +void DamageOverlay::paintRects(QPainter *painter, const QVariantList &rects, + const QColor &color) +{ + painter->save(); + painter->setCompositionMode(QPainter::CompositionMode_Source); + painter->setPen(Qt::NoPen); + for (const QVariant &value : rects) { + const QVariantMap rect = value.toMap(); + const QRectF geometry(rect.value(QStringLiteral("x")).toReal(), + rect.value(QStringLiteral("y")).toReal(), + rect.value(QStringLiteral("w")).toReal(), + rect.value(QStringLiteral("h")).toReal()); + if (!geometry.isEmpty()) + painter->fillRect(geometry, color); + } + painter->restore(); +} + +void DamageOverlay::paint(QPainter *painter) +{ + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + const QColor newest(232, 62, 78, 110); + const QColor oldest(45, 196, 112, 65); + bool hasVisibleFrame = false; + + // Hue is based only on frame distance from the newest damage. Time controls + // expiration, but never changes a frame's color. + const qsizetype newestIndex = m_frames.size() - 1; + constexpr qreal colorSteps = 7.0; + for (qsizetype index = 0; index < m_frames.size(); ++index) { + const QVariantMap frame = m_frames.at(index).toMap(); + const qint64 timestamp = frame.value(QStringLiteral("timestamp")).toLongLong(); + const qint64 age = qMax(0, now - timestamp); + if (age >= m_historyDuration) + continue; + hasVisibleFrame = true; + const qreal progress = qMin(1.0, qreal(newestIndex - index) / colorSteps); + paintRects(painter, frame.value(QStringLiteral("rects")).toList(), + interpolateColor(newest, oldest, progress)); + } + + if (!hasVisibleFrame) + m_repaintTimer.stop(); +} diff --git a/waylib/tests/manual/damage/damageoverlay.h b/waylib/tests/manual/damage/damageoverlay.h new file mode 100644 index 0000000000..8652a4eeb1 --- /dev/null +++ b/waylib/tests/manual/damage/damageoverlay.h @@ -0,0 +1,49 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef DAMAGEOVERLAY_H +#define DAMAGEOVERLAY_H + +#include +#include +#include +#include + +class DamageOverlay : public QQuickPaintedItem +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QVariantList frames READ frames WRITE setFrames NOTIFY framesChanged) + Q_PROPERTY(int historyDuration READ historyDuration WRITE setHistoryDuration + NOTIFY historyDurationChanged) + Q_PROPERTY(int refreshRate READ refreshRate WRITE setRefreshRate NOTIFY refreshRateChanged) + +public: + explicit DamageOverlay(QQuickItem *parent = nullptr); + + QVariantList frames() const { return m_frames; } + void setFrames(const QVariantList &frames); + + int historyDuration() const { return m_historyDuration; } + void setHistoryDuration(int duration); + + int refreshRate() const { return m_refreshRate; } + void setRefreshRate(int refreshRate); + + void paint(QPainter *painter) override; + +signals: + void framesChanged(); + void historyDurationChanged(); + void refreshRateChanged(); + +private: + void paintRects(QPainter *painter, const QVariantList &rects, const QColor &color); + + QVariantList m_frames; + QTimer m_repaintTimer; + int m_historyDuration = 200; + int m_refreshRate = 60; +}; + +#endif diff --git a/waylib/tests/manual/damage/demoscene.cpp b/waylib/tests/manual/damage/demoscene.cpp new file mode 100644 index 0000000000..432fb86f1c --- /dev/null +++ b/waylib/tests/manual/damage/demoscene.cpp @@ -0,0 +1,1207 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "demoscene.h" +#include + +#include + +WAYLIB_SERVER_USE_NAMESPACE + +static const QColor kPalette[] = { + QColor("#5b8def"), + QColor("#9353d3"), + QColor("#f5a524"), + QColor("#00bcd4"), + QColor("#6366f1"), + QColor("#0284c7"), + QColor("#8b5cf6"), + QColor("#d97706"), +}; + +static QString typeString(WSGDamageNode::Type t) +{ + switch (t) { + case WSGDamageNode::Type::Basic: + return QStringLiteral("Basic"); + case WSGDamageNode::Type::Transform: + return QStringLiteral("Transform"); + case WSGDamageNode::Type::Clip: + return QStringLiteral("Clip"); + case WSGDamageNode::Type::Geometry: + return QStringLiteral("Geometry"); + } + return {}; +} + +static QVariantMap clipVisualRow(WSGDamageNode *n, const QString &name, int paintOrder) +{ + auto *clip = n->toClip(); + const QRectF r = clip->clipRect(); + const QTransform matrix = n->worldTransform().toTransform(); + const QRect aabb = mapOuter(matrix, r); + QVariantMap v; + v.insert(QStringLiteral("id"), n->id()); + v.insert(QStringLiteral("name"), name); + v.insert(QStringLiteral("type"), QStringLiteral("Clip")); + v.insert(QStringLiteral("localX"), r.x()); + v.insert(QStringLiteral("localY"), r.y()); + v.insert(QStringLiteral("localWidth"), r.width()); + v.insert(QStringLiteral("localHeight"), r.height()); + v.insert(QStringLiteral("m11"), matrix.m11()); + v.insert(QStringLiteral("m12"), matrix.m12()); + v.insert(QStringLiteral("m21"), matrix.m21()); + v.insert(QStringLiteral("m22"), matrix.m22()); + v.insert(QStringLiteral("m13"), matrix.m13()); + v.insert(QStringLiteral("m23"), matrix.m23()); + v.insert(QStringLiteral("m33"), matrix.m33()); + v.insert(QStringLiteral("dx"), matrix.dx()); + v.insert(QStringLiteral("dy"), matrix.dy()); + v.insert(QStringLiteral("x"), aabb.x()); + v.insert(QStringLiteral("y"), aabb.y()); + v.insert(QStringLiteral("w"), aabb.width()); + v.insert(QStringLiteral("h"), aabb.height()); + v.insert(QStringLiteral("hasContent"), false); + v.insert(QStringLiteral("occluded"), false); + v.insert(QStringLiteral("culled"), false); + v.insert(QStringLiteral("visible"), n->isVisible()); + v.insert(QStringLiteral("color"), QStringLiteral("#22d3ee")); + v.insert(QStringLiteral("isBackdrop"), false); + v.insert(QStringLiteral("fullyOpaque"), false); + v.insert(QStringLiteral("paintOrder"), paintOrder); + return v; +} + +static bool worldHidden(const WSGDamageNode *n) +{ + return WPixmanRegion(n->worldValidRegion()).isEmpty(); +} + +static int refreshInterval(int refreshRate) +{ + return qMax(1, int(std::round(1000.0 / refreshRate))); +} +static QVariantList regionToRects(const WPixmanRegion ®ion) +{ + QVariantList list; + int count = 0; + const pixman_box32_t *boxes = region.rectangles(&count); + for (int i = 0; i < count && boxes; ++i) { + QVariantMap m; + m.insert(QStringLiteral("x"), boxes[i].x1); + m.insert(QStringLiteral("y"), boxes[i].y1); + m.insert(QStringLiteral("w"), boxes[i].x2 - boxes[i].x1); + m.insert(QStringLiteral("h"), boxes[i].y2 - boxes[i].y1); + list.append(m); + } + return list; +} + +DemoScene::DemoScene(QObject *parent) + : QObject(parent) + , m_visualNodeModel(new VisualNodeModel(this)) +{ + m_dragFrameTimer.setInterval(refreshInterval(m_refreshRate)); + connect(&m_dragFrameTimer, &QTimer::timeout, this, [this] { + if (!m_dragFramePending) { + m_dragFrameTimer.stop(); + return; + } + m_dragFramePending = false; + updateDamage(false); + }); + m_demoTimer.setInterval(refreshInterval(m_refreshRate)); + connect(&m_demoTimer, &QTimer::timeout, this, &DemoScene::advanceDemoFrame); + resetRoot(); + loadPreset(QStringLiteral("occlusion")); +} + +DemoScene::~DemoScene() = default; + +void DemoScene::resetRoot() +{ + const bool selectionChanged = m_selectedId != 0; + m_root = std::make_unique(); + m_root->setName(QStringLiteral("根节点")); + m_tracker.setRoot(m_root.get()); + m_tracker.addViewport(m_demoViewport); + m_decor.clear(); + m_dragFrameTimer.stop(); + m_dragFramePending = false; + m_damageFrames.clear(); + m_selectedId = 0; + m_colorIndex = 0; + if (selectionChanged) + emit selectedIdChanged(); +} + +void DemoScene::setSelectedId(quint64 id) +{ + if (m_selectedId == id) + return; + m_selectedId = id; + + // Decompose transform state from matrix (best effort). + if (WSGDamageNode *n = findNode(id)) { + if (auto *tr = n->toTransform()) { + const QTransform m = tr->matrix().toTransform(); + if (!qFuzzyIsNull(m.m13()) || !qFuzzyIsNull(m.m23())) { + // Non-affine: perspective transform from X or Y axis rotation. + if (!qFuzzyIsNull(m.m23())) + m_rotationAxis = 0; // X axis: m23 = -sin/d + else + m_rotationAxis = 1; // Y axis: m13 = sin/d + m_rotation = 0; + m_scaleX = 1; + m_scaleY = 1; + } else { + // Affine (Z axis): standard decomposition. + m_rotationAxis = 2; + m_rotation = std::atan2(m.m12(), m.m11()) + * 180.0 / 3.14159265358979323846; + m_scaleX = std::hypot(m.m11(), m.m12()); + m_scaleY = std::hypot(m.m21(), m.m22()); + } + } else { + m_rotationAxis = 2; + m_rotation = 0; + m_scaleX = 1; + m_scaleY = 1; + } + } + emit selectedIdChanged(); + rebuildLists(); + refreshSelectedProps(); +} + +void DemoScene::setAutoCommit(bool enabled) +{ + if (m_autoCommit == enabled) + return; + m_autoCommit = enabled; + emit autoCommitChanged(); +} + +void DemoScene::setRefreshRate(int refreshRate) +{ + refreshRate = qBound(1, refreshRate, 1000); + if (m_refreshRate == refreshRate) + return; + m_refreshRate = refreshRate; + m_demoTimer.setInterval(refreshInterval(m_refreshRate)); + if (m_demoRunning) + m_demoTimer.start(); + m_dragFrameTimer.setInterval(refreshInterval(m_refreshRate)); + emit refreshRateChanged(); +} + +QVariantList DemoScene::demoScenes() const +{ + return { + QVariantMap{{QStringLiteral("text"), QStringLiteral("遮挡移动")}, + {QStringLiteral("value"), QStringLiteral("occlusion")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("局部内容变化")}, + {QStringLiteral("value"), QStringLiteral("content")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("裁剪视口滚动")}, + {QStringLiteral("value"), QStringLiteral("clip")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("背景采样扩散")}, + {QStringLiteral("value"), QStringLiteral("backdrop")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("揭露后方节点")}, + {QStringLiteral("value"), QStringLiteral("reveal")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("自定义渲染节点")}, + {QStringLiteral("value"), QStringLiteral("renderer")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("旋转节点")}, + {QStringLiteral("value"), QStringLiteral("rotation")}}, + QVariantMap{{QStringLiteral("text"), QStringLiteral("缩放节点")}, + {QStringLiteral("value"), QStringLiteral("scale")}}, + }; +} + +void DemoScene::setDemoRunning(bool running) +{ + if (m_demoRunning == running) + return; + m_demoRunning = running; + if (m_demoRunning) + m_demoTimer.start(); + else + m_demoTimer.stop(); + emit demoRunningChanged(); +} + + +void DemoScene::loadDemoScene(const QString &name) +{ + const QString sceneName = name.isEmpty() ? QStringLiteral("occlusion") : name; + m_demoTimer.stop(); + m_demoFrame = 0; + m_demoNodeA = 0; + m_demoNodeB = 0; + m_demoNodeC = 0; + m_demoRunning = true; + m_demoSceneName = sceneName; + resetRoot(); + buildDemoScene(sceneName); + commit(); + m_demoTimer.setInterval(refreshInterval(m_refreshRate)); + m_demoTimer.start(); + emit demoRunningChanged(); + emit demoSceneChanged(); +} + +void DemoScene::stepDemoFrame() +{ + advanceDemoFrame(); +} +void DemoScene::activateNode(quint64 id) +{ + WSGDamageNode *node = findNode(id); + if (!node) + return; + setSelectedId(id); + if (!node->parent() || !node->nextSibling()) + return; + node->parent()->appendChild(node); + maybeCommit(); +} + +WSGDamageNode *DemoScene::findNodeRecursive(WSGDamageNode *n, quint64 id) const +{ + if (!n) + return nullptr; + if (n->id() == id) + return n; + for (WSGDamageNode *c = n->firstChild(); c; c = c->nextSibling()) { + if (WSGDamageNode *f = findNodeRecursive(c, id)) + return f; + } + return nullptr; +} + +WSGDamageNode *DemoScene::findNode(quint64 id) const +{ + return findNodeRecursive(m_root.get(), id); +} + +WSGDamageNode *DemoScene::parentForInsert() const +{ + if (WSGDamageNode *sel = findNode(m_selectedId)) + return sel; + return m_root.get(); +} + +bool DemoScene::isDescendantOf(const WSGDamageNode *node, const WSGDamageNode *ancestor) const +{ + for (const WSGDamageNode *current = node; current; current = current->parent()) { + if (current == ancestor) + return true; + } + return false; +} + +void DemoScene::moveNode(quint64 nodeId, quint64 newParentId, quint64 beforeSiblingId) +{ + + WSGDamageNode *node = findNode(nodeId); + WSGDamageNode *newParent = newParentId ? findNode(newParentId) : m_root.get(); + WSGDamageNode *before = beforeSiblingId ? findNode(beforeSiblingId) : nullptr; + if (!node || !newParent || node == m_root.get() || node == newParent + || isDescendantOf(newParent, node)) + return; + if (before && (before == node || before->parent() != newParent)) + return; + + if (node->parent() == newParent && !before && node == newParent->lastChild()) + return; + if (node->parent() == newParent && before == node->nextSibling()) + return; + + if (before) + newParent->insertChildBefore(node, before); + else + newParent->appendChild(node); + setSelectedId(nodeId); + maybeCommit(); +} + +QColor DemoScene::nextColor() +{ + const QColor c = kPalette[m_colorIndex % int(sizeof(kPalette) / sizeof(kPalette[0]))]; + ++m_colorIndex; + return c; +} + +void DemoScene::addBasic() +{ + auto *n = new WSGDamageNode; + n->setName(QStringLiteral("分组")); + parentForInsert()->appendChild(n); + setSelectedId(n->id()); + maybeCommit(); +} + +void DemoScene::addTransform() +{ + auto *n = new WSGDamageTransformNode; + n->setName(QStringLiteral("变换")); + parentForInsert()->appendChild(n); + setSelectedId(n->id()); + maybeCommit(); +} + +void DemoScene::addClip() +{ + auto *n = new WSGDamageClipNode; + n->setName(QStringLiteral("裁剪")); + n->setClipRect(QRectF(40, 40, 280, 200)); + parentForInsert()->appendChild(n); + setSelectedId(n->id()); + maybeCommit(); +} + +void DemoScene::addGeometry() +{ + auto *n = new WSGDamageGeometryNode; + n->setName(QStringLiteral("矩形")); + n->setBoundingRect(QRectF(80 + m_colorIndex * 16, 80 + m_colorIndex * 12, 140, 90)); + n->setFullyOpaque(true); + parentForInsert()->appendChild(n); + m_decor.insert(n->id(), Decor{nextColor()}); + setSelectedId(n->id()); + maybeCommit(); +} + +void DemoScene::addBackdrop() +{ + auto *n = new WSGDamageBackdropNode; + n->setName(QStringLiteral("背景采样")); + n->setBoundingRect(QRectF(40, 40, 280, 200)); + parentForInsert()->appendChild(n); + m_decor.insert(n->id(), Decor{QColor("#00bcd4")}); + setSelectedId(n->id()); + maybeCommit(); +} + +void DemoScene::addRenderer() +{ + auto *n = new WSGDamageGeometryNode; + n->setName(QStringLiteral("自定义渲染")); + n->setBoundingRect(QRectF(60 + m_colorIndex * 16, 60 + m_colorIndex * 12, 180, 120)); + parentForInsert()->appendChild(n); + m_decor.insert(n->id(), Decor{QColor("#ff7043")}); + setSelectedId(n->id()); + maybeCommit(); +} + +void DemoScene::removeSelected() +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || n == m_root.get()) + return; + const quint64 parentId = n->parent() ? n->parent()->id() : 0; + delete n; + m_decor.remove(m_selectedId); + setSelectedId(parentId); + maybeCommit(); +} + +void DemoScene::raiseSelected() +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->parent() || !n->nextSibling()) + return; + WSGDamageNode *parent = n->parent(); + WSGDamageNode *after = n->nextSibling(); + parent->insertChildAfter(n, after); + maybeCommit(); +} + +void DemoScene::lowerSelected() +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->parent() || !n->previousSibling()) + return; + WSGDamageNode *parent = n->parent(); + WSGDamageNode *before = n->previousSibling(); + parent->insertChildBefore(n, before); + maybeCommit(); +} + +void DemoScene::setVisibleSelected(bool visible) +{ + if (WSGDamageNode *n = findNode(m_selectedId)) { + n->setVisible(visible); + maybeCommit(); + } +} + + +void DemoScene::setRectSelected(qreal x, qreal y, qreal w, qreal h) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n) + return; + if (auto *geo = n->toGeometry()) + geo->setBoundingRect(QRectF(x, y, w, h)); + else if (auto *clip = n->toClip()) + clip->setClipRect(QRectF(x, y, w, h)); + else + return; + maybeCommit(); +} + +void DemoScene::setTranslationSelected(qreal x, qreal y) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->toTransform()) + return; + QTransform matrix = n->toTransform()->matrix().toTransform(); + matrix.setMatrix(matrix.m11(), matrix.m12(), matrix.m13(), + matrix.m21(), matrix.m22(), matrix.m23(), + x, y, matrix.m33()); + n->toTransform()->setMatrix(matrix); + maybeCommit(); +} + +void DemoScene::setRotationSelected(qreal degrees, int axis) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->toTransform()) + return; + m_rotationAxis = axis; + m_rotation = degrees; + const QTransform current = n->toTransform()->matrix().toTransform(); + QTransform matrix; + matrix.translate(current.dx(), current.dy()); + const Qt::Axis a = (axis == 0) ? Qt::XAxis : ((axis == 1) ? Qt::YAxis : Qt::ZAxis); + matrix.rotate(degrees, a); + matrix.scale(m_scaleX, m_scaleY); + n->toTransform()->setMatrix(matrix); + maybeCommit(); +} + +void DemoScene::setScaleSelected(qreal sx, qreal sy) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->toTransform()) + return; + m_scaleX = sx; + m_scaleY = sy; + const QTransform current = n->toTransform()->matrix().toTransform(); + QTransform matrix; + matrix.translate(current.dx(), current.dy()); + const Qt::Axis a = (m_rotationAxis == 0) ? Qt::XAxis : ((m_rotationAxis == 1) ? Qt::YAxis : Qt::ZAxis); + matrix.rotate(m_rotation, a); + matrix.scale(sx, sy); + n->toTransform()->setMatrix(matrix); + maybeCommit(); +} + +void DemoScene::setFullyOpaqueSelected(bool opaque) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->toGeometry()) + return; + n->toGeometry()->setFullyOpaque(opaque); + maybeCommit(); +} + +void DemoScene::setExpansionSelected(int px) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->needsBackdrop()) + return; + Decor &decor = m_decor[n->id()]; + const int clamped = qMax(0, px); + if (decor.expansion == clamped) + return; + decor.expansion = clamped; + if (auto *backdrop = n->toBackdrop()) + backdrop->setRecopyExpansion(QMargins(clamped, clamped, clamped, clamped)); + maybeCommit(); +} + +void DemoScene::setClipExpansionSelected(bool clip) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->needsBackdrop()) + return; + Decor &decor = m_decor[n->id()]; + if (decor.clipExpansion == clip) + return; + decor.clipExpansion = clip; + maybeCommit(); +} + +void DemoScene::markSelectedContentDirty() +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->toGeometry()) + return; + const QRectF br = n->toGeometry()->boundingRect(); + n->toGeometry()->markContentDirty(QRect(int(br.x()), int(br.y()), + int(br.width()), int(br.height()))); + maybeCommit(); +} + +void DemoScene::markSelectedContentDirtyAt(qreal x, qreal y, qreal w, qreal h) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n || !n->toGeometry()) + return; + n->toGeometry()->markContentDirty(QRect(int(std::floor(x)), int(std::floor(y)), + int(std::ceil(w)), int(std::ceil(h)))); + maybeCommit(); +} + +void DemoScene::moveSelectedBy(qreal dx, qreal dy) +{ + WSGDamageNode *n = findNode(m_selectedId); + if (!n) + return; + if (auto *tr = n->toTransform()) { + const QTransform m = tr->matrix().toTransform(); + tr->setTranslation(m.dx() + dx, m.dy() + dy); + } else if (auto *geo = n->toGeometry()) { + QRectF r = geo->boundingRect(); + r.translate(dx, dy); + geo->setBoundingRect(r); + } else if (auto *clip = n->toClip()) { + QRectF r = clip->clipRect(); + r.translate(dx, dy); + clip->setClipRect(r); + } else { + return; + } + + m_dragFramePending = true; + if (!m_dragFrameTimer.isActive()) { + m_dragFramePending = false; + updateDamage(false); + m_dragFrameTimer.start(); + } +} + +void DemoScene::finishSelectedMove() +{ + if (m_dragFramePending) + updateDamage(false); + m_dragFramePending = false; + m_dragFrameTimer.stop(); + rebuildLists(); + refreshSelectedProps(); +} + +void DemoScene::clearTree() +{ + resetRoot(); + m_damageRects.clear(); + m_damageRectsB.clear(); + m_damageFrames.clear(); + rebuildLists(); + refreshSelectedProps(); + emit damageChanged(); +} + +void DemoScene::loadPreset(const QString &name) +{ + resetRoot(); + + if (name == QLatin1String("occlusion")) { + auto *back = new WSGDamageGeometryNode; + back->setName(QStringLiteral("后层不透明节点")); + back->setBoundingRect(QRectF(80, 80, 260, 200)); + back->setFullyOpaque(true); + m_decor.insert(back->id(), Decor{QColor("#5b8def")}); + + auto *front = new WSGDamageGeometryNode; + front->setName(QStringLiteral("前层不透明节点")); + front->setBoundingRect(QRectF(160, 140, 220, 180)); + front->setFullyOpaque(true); + m_decor.insert(front->id(), Decor{QColor("#9353d3")}); + + m_root->appendChild(back); + m_root->appendChild(front); + setSelectedId(back->id()); + } else if (name == QLatin1String("backdrop")) { + auto *wall = new WSGDamageGeometryNode; + wall->setName(QStringLiteral("壁纸")); + wall->setBoundingRect(QRectF(40, 40, 520, 360)); + wall->setFullyOpaque(true); + m_decor.insert(wall->id(), Decor{QColor("#0284c7")}); + + auto *bg = new WSGDamageBackdropNode; + bg->setName(QStringLiteral("模糊层")); + bg->setBoundingRect(QRectF(140, 100, 280, 200)); + m_decor.insert(bg->id(), Decor{QColor("#00bcd4")}); + + auto *chip = new WSGDamageGeometryNode; + chip->setName(QStringLiteral("前景卡片")); + chip->setBoundingRect(QRectF(200, 160, 80, 60)); + chip->setFullyOpaque(true); + m_decor.insert(chip->id(), Decor{QColor("#f5a524")}); + + m_root->appendChild(wall); + m_root->appendChild(bg); + m_root->appendChild(chip); + setSelectedId(wall->id()); + } else if (name == QLatin1String("transform")) { + auto *tr = new WSGDamageTransformNode; + tr->setName(QStringLiteral("位移组")); + tr->setTranslation(40, 30); + + auto *g = new WSGDamageGeometryNode; + g->setName(QStringLiteral("卡片")); + g->setBoundingRect(QRectF(80, 80, 160, 120)); + g->setFullyOpaque(true); + m_decor.insert(g->id(), Decor{QColor("#9353d3")}); + + auto *g2 = new WSGDamageGeometryNode; + g2->setName(QStringLiteral("徽标")); + g2->setBoundingRect(QRectF(200, 140, 90, 70)); + g2->setFullyOpaque(true); + m_decor.insert(g2->id(), Decor{QColor("#6366f1")}); + + tr->appendChild(g); + tr->appendChild(g2); + m_root->appendChild(tr); + setSelectedId(tr->id()); + } else { + addGeometry(); + return; + } + + commit(); +} + +void DemoScene::buildDemoScene(const QString &name) +{ + if (name == QLatin1String("content")) { + auto *wall = new WSGDamageGeometryNode; + wall->setName(QStringLiteral("内容背景")); + wall->setBoundingRect(QRectF(20, 20, 560, 380)); + wall->setFullyOpaque(true); + m_decor.insert(wall->id(), Decor{QColor("#0284c7")}); + + auto *content = new WSGDamageGeometryNode; + content->setName(QStringLiteral("局部变化内容")); + content->setBoundingRect(QRectF(100, 110, 240, 150)); + m_decor.insert(content->id(), Decor{QColor("#f5a524")}); + m_root->appendChild(wall); + m_root->appendChild(content); + m_demoNodeA = content->id(); + setSelectedId(content->id()); + return; + } + + if (name == QLatin1String("clip")) { + auto *wall = new WSGDamageGeometryNode; + wall->setName(QStringLiteral("裁剪外背景")); + wall->setBoundingRect(QRectF(20, 20, 320, 440)); + wall->setFullyOpaque(true); + m_decor.insert(wall->id(), Decor{QColor("#0284c7")}); + + auto *clip = new WSGDamageClipNode; + clip->setName(QStringLiteral("视口裁剪")); + clip->setClipRect(QRectF(40, 50, 280, 300)); + + auto *scroll = new WSGDamageTransformNode; + scroll->setName(QStringLiteral("列表滚动")); + + const QColor cardColors[] = { + QColor("#5b8def"), QColor("#9353d3"), QColor("#f5a524"), + QColor("#00bcd4"), QColor("#6366f1"), + }; + for (int i = 0; i < 5; ++i) { + auto *card = new WSGDamageGeometryNode; + card->setName(QStringLiteral("列表卡片")); + card->setBoundingRect(QRectF(50, 60 + i * 100, 260, 80)); + card->setFullyOpaque(true); + m_decor.insert(card->id(), Decor{cardColors[i]}); + scroll->appendChild(card); + } + clip->appendChild(scroll); + m_root->appendChild(wall); + m_root->appendChild(clip); + m_demoNodeA = scroll->id(); + m_demoNodeB = clip->id(); + setSelectedId(clip->id()); + return; + } + + if (name == QLatin1String("backdrop")) { + auto *wall = new WSGDamageGeometryNode; + wall->setName(QStringLiteral("被采样背景")); + wall->setBoundingRect(QRectF(20, 20, 560, 380)); + wall->setFullyOpaque(true); + m_decor.insert(wall->id(), Decor{QColor("#0284c7")}); + + auto *backdrop = new WSGDamageBackdropNode; + backdrop->setName(QStringLiteral("背景采样")); + backdrop->setBoundingRect(QRectF(150, 100, 280, 200)); + m_decor.insert(backdrop->id(), Decor{QColor("#00bcd4")}); + + auto *chip = new WSGDamageGeometryNode; + chip->setName(QStringLiteral("前景内容")); + chip->setBoundingRect(QRectF(230, 165, 90, 70)); + chip->setFullyOpaque(true); + m_decor.insert(chip->id(), Decor{QColor("#f5a524")}); + m_root->appendChild(wall); + m_root->appendChild(backdrop); + m_root->appendChild(chip); + m_demoNodeA = wall->id(); + m_demoNodeB = backdrop->id(); + m_demoNodeC = chip->id(); + setSelectedId(backdrop->id()); + return; + } + + if (name == QLatin1String("rotation")) { + auto *transform = new WSGDamageTransformNode; + transform->setName(QStringLiteral("旋转变换")); + QTransform matrix; + matrix.translate(300, 220); + matrix.rotate(0, Qt::ZAxis); + transform->setMatrix(matrix); + + auto *geometry = new WSGDamageGeometryNode; + geometry->setName(QStringLiteral("旋转卡片")); + geometry->setBoundingRect(QRectF(-110, -70, 220, 140)); + geometry->setFullyOpaque(true); + m_decor.insert(geometry->id(), Decor{QColor("#9353d3")}); + transform->appendChild(geometry); + m_root->appendChild(transform); + m_demoNodeA = transform->id(); + m_demoNodeB = geometry->id(); + setSelectedId(transform->id()); + return; + } + + if (name == QLatin1String("scale")) { + auto *transform = new WSGDamageTransformNode; + transform->setName(QStringLiteral("缩放变换")); + QTransform matrix; + matrix.translate(180, 150); + transform->setMatrix(matrix); + + auto *geometry = new WSGDamageGeometryNode; + geometry->setName(QStringLiteral("缩放卡片")); + geometry->setBoundingRect(QRectF(0, 0, 220, 140)); + geometry->setFullyOpaque(true); + m_decor.insert(geometry->id(), Decor{QColor("#0284c7")}); + transform->appendChild(geometry); + m_root->appendChild(transform); + m_demoNodeA = transform->id(); + m_demoNodeB = geometry->id(); + setSelectedId(transform->id()); + return; + } + + if (name == QLatin1String("reveal")) { + auto *wall = new WSGDamageGeometryNode; + wall->setName(QStringLiteral("后方静止底板")); + wall->setBoundingRect(QRectF(50, 60, 480, 280)); + wall->setFullyOpaque(true); + m_decor.insert(wall->id(), Decor{QColor("#5b8def")}); + + auto *badge = new WSGDamageGeometryNode; + badge->setName(QStringLiteral("被遮挡内容")); + badge->setBoundingRect(QRectF(170, 100, 200, 160)); + badge->setFullyOpaque(true); + m_decor.insert(badge->id(), Decor{QColor("#00bcd4")}); + + auto *cover = new WSGDamageGeometryNode; + cover->setName(QStringLiteral("移开的遮挡窗口")); + cover->setBoundingRect(QRectF(150, 80, 240, 200)); + cover->setFullyOpaque(true); + m_decor.insert(cover->id(), Decor{QColor("#9353d3")}); + + m_root->appendChild(wall); + m_root->appendChild(badge); + m_root->appendChild(cover); + m_demoNodeA = wall->id(); + m_demoNodeB = cover->id(); + setSelectedId(cover->id()); + return; + } + + if (name == QLatin1String("renderer")) { + auto *wall = new WSGDamageGeometryNode; + wall->setName(QStringLiteral("底层动态卡片")); + wall->setBoundingRect(QRectF(60, 60, 240, 180)); + wall->setFullyOpaque(true); + m_decor.insert(wall->id(), Decor{QColor("#5b8def")}); + + auto *rnd = new WSGDamageGeometryNode; + rnd->setName(QStringLiteral("自适应渲染器")); + rnd->setBoundingRect(QRectF(140, 100, 240, 160)); + m_decor.insert(rnd->id(), Decor{QColor("#ff7043")}); + + m_root->appendChild(wall); + m_root->appendChild(rnd); + m_demoNodeA = wall->id(); + m_demoNodeB = rnd->id(); + setSelectedId(rnd->id()); + return; + } + + auto *back = new WSGDamageGeometryNode; + back->setName(QStringLiteral("被遮挡内容")); + back->setBoundingRect(QRectF(80, 80, 260, 200)); + back->setFullyOpaque(true); + m_decor.insert(back->id(), Decor{QColor("#5b8def")}); + auto *front = new WSGDamageGeometryNode; + front->setName(QStringLiteral("移动遮挡层")); + front->setBoundingRect(QRectF(160, 140, 220, 180)); + front->setFullyOpaque(true); + m_decor.insert(front->id(), Decor{QColor("#9353d3")}); + m_root->appendChild(back); + m_root->appendChild(front); + m_demoNodeA = back->id(); + m_demoNodeB = front->id(); + setSelectedId(front->id()); +} + +void DemoScene::advanceDemoFrame() +{ + if (!m_demoRunning) + return; + + const int phase = ++m_demoFrame % 120; + const int triangle = phase < 60 ? phase : 120 - phase; + if (m_demoSceneName == QLatin1String("content")) { + if (auto *node = findNode(m_demoNodeA)) { + if (auto *geometry = node->toGeometry()) + geometry->markContentDirty(QRect(112 + triangle * 2, 134, 26, 20)); + } + } else if (m_demoSceneName == QLatin1String("clip")) { + if (auto *node = findNode(m_demoNodeA)) { + if (auto *transform = node->toTransform()) + transform->setTranslation(0, -triangle * 3.2); + } + } else if (m_demoSceneName == QLatin1String("backdrop")) { + if (auto *node = findNode(m_demoNodeA)) { + if (auto *geometry = node->toGeometry()) + geometry->markContentDirty(QRect(36 + triangle, 46, 24, 18)); + } + } else if (m_demoSceneName == QLatin1String("renderer")) { + if (auto *node = findNode(m_demoNodeA)) { + if (auto *geometry = node->toGeometry()) + geometry->setBoundingRect(QRectF(60 + triangle * 2, 60, 240, 180)); + } + } else if (m_demoSceneName == QLatin1String("rotation")) { + if (auto *node = findNode(m_demoNodeA)) { + if (auto *transform = node->toTransform()) { + QTransform matrix; + matrix.translate(300, 220); + const Qt::Axis a = (m_rotationAxis == 0) ? Qt::XAxis : ((m_rotationAxis == 1) ? Qt::YAxis : Qt::ZAxis); + matrix.rotate(triangle * 3.0, a); + transform->setMatrix(matrix); + } + } + } else if (m_demoSceneName == QLatin1String("scale")) { + if (auto *node = findNode(m_demoNodeA)) { + if (auto *transform = node->toTransform()) { + const qreal scale = 0.65 + qreal(triangle) / 60.0 * 0.75; + QTransform matrix; + matrix.translate(180, 150); + matrix.scale(scale, scale); + transform->setMatrix(matrix); + } + } + } else if (m_demoSceneName == QLatin1String("reveal")) { + if (auto *node = findNode(m_demoNodeB)) { + if (auto *geometry = node->toGeometry()) + geometry->setBoundingRect(QRectF(150 + triangle * 3.5, 80, 240, 200)); + } + } else { + if (auto *node = findNode(m_demoNodeB)) { + if (auto *geometry = node->toGeometry()) + geometry->setBoundingRect(QRectF(80 + triangle * 3, 140, 220, 180)); + } + } + updateDamage(false); +} + +void DemoScene::commit() +{ + updateDamage(true); +} + +void DemoScene::updateDamage(bool rebuildScene) +{ + // Single tracker commit, dual-clipped: one scene-space commit feeds both + // simulated outputs; each view only clips the shared damage to its own + // viewport rect for display. + m_tracker.commit(); + WPixmanRegion damageA = m_demoViewport->damageRegion().region; + WPixmanRegion damageB = damageA; + m_demoViewport->finishFrame(); + if (!m_viewportA.isEmpty()) + damageA &= m_viewportA; + if (!m_viewportB.isEmpty()) + damageB &= m_viewportB; + m_damageRects = regionToRects(damageA); + m_damageRectsB = regionToRects(damageB); + + const WPixmanRegion combined = damageA + damageB; + if (!combined.isEmpty()) { + QVariantMap frame; + frame.insert(QStringLiteral("timestamp"), QDateTime::currentMSecsSinceEpoch()); + frame.insert(QStringLiteral("rects"), regionToRects(combined)); + m_damageFrames.append(frame); + constexpr qsizetype maximumFrames = 120; + while (m_damageFrames.size() > maximumFrames) + m_damageFrames.removeFirst(); + } + + if (rebuildScene) + rebuildLists(); + else + rebuildVisualNodes(); + refreshSelectedProps(); + emit damageChanged(); +} + +void DemoScene::maybeCommit() +{ + if (m_autoCommit) + commit(); + else { + rebuildLists(); + refreshSelectedProps(); + } +} + +void DemoScene::rebuildLists() +{ + QVector visual; + QVariantList tree; + int paintOrder = 0; + m_displayNames.clear(); + collectVisual(m_root.get(), &visual, &tree, 0, &paintOrder); + m_visualNodeModel->setNodes(visual); + m_treeNodes = tree; + emit treeNodesChanged(); + emit sceneChanged(); +} + +void DemoScene::rebuildVisualNodes() +{ + QVector visual; + int paintOrder = 0; + collectVisualOnly(m_root.get(), &visual, &paintOrder); + m_visualNodeModel->setNodes(visual); + emit sceneChanged(); +} + +void DemoScene::collectVisualOnly(WSGDamageNode *n, QVector *visual, int *paintOrder) +{ + if (n->hasContent()) { + auto *geo = n->toGeometry(); + const QRectF localRect = geo->boundingRect(); + const QTransform matrix = n->worldTransform().toTransform(); + const QRect aabb = n->worldBounds(); + + QVariantMap v; + v.insert(QStringLiteral("id"), n->id()); + v.insert(QStringLiteral("name"), m_displayNames.value(n->id(), n->name())); + v.insert(QStringLiteral("type"), typeString(n->type())); + v.insert(QStringLiteral("localX"), localRect.x()); + v.insert(QStringLiteral("localY"), localRect.y()); + v.insert(QStringLiteral("localWidth"), localRect.width()); + v.insert(QStringLiteral("localHeight"), localRect.height()); + v.insert(QStringLiteral("m11"), matrix.m11()); + v.insert(QStringLiteral("m12"), matrix.m12()); + v.insert(QStringLiteral("m21"), matrix.m21()); + v.insert(QStringLiteral("m22"), matrix.m22()); + v.insert(QStringLiteral("m13"), matrix.m13()); + v.insert(QStringLiteral("m23"), matrix.m23()); + v.insert(QStringLiteral("m33"), matrix.m33()); + v.insert(QStringLiteral("dx"), matrix.dx()); + v.insert(QStringLiteral("dy"), matrix.dy()); + v.insert(QStringLiteral("x"), aabb.x()); + v.insert(QStringLiteral("y"), aabb.y()); + v.insert(QStringLiteral("w"), aabb.width()); + v.insert(QStringLiteral("h"), aabb.height()); + v.insert(QStringLiteral("hasContent"), n->hasContent()); + v.insert(QStringLiteral("occluded"), worldHidden(n)); + v.insert(QStringLiteral("culled"), worldHidden(n)); + v.insert(QStringLiteral("visible"), n->isVisible()); + const QColor c = m_decor.value(n->id()).color; + v.insert(QStringLiteral("color"), (c.isValid() ? c : QColor("#888888")).name()); + v.insert(QStringLiteral("isBackdrop"), n->needsBackdrop()); + v.insert(QStringLiteral("fullyOpaque"), geo->isFullyOpaque()); + v.insert(QStringLiteral("paintOrder"), (*paintOrder)++); + visual->append(v); + } else if (n->toClip()) { + visual->append(clipVisualRow(n, m_displayNames.value(n->id(), n->name()), (*paintOrder)++)); + } + for (WSGDamageNode *c = n->firstChild(); c; c = c->nextSibling()) + collectVisualOnly(c, visual, paintOrder); +} + +void DemoScene::collectVisual(WSGDamageNode *n, QVector *visual, QVariantList *tree, + int depth, int *paintOrder) +{ + const int order = (*paintOrder)++; + QString label; + if (!n->parent()) { + label = QStringLiteral("根节点 #%1").arg(n->id()); + } else if (n->needsBackdrop()) { + auto *bg = n->toGeometry(); + const QRectF r = bg->boundingRect(); + label = QStringLiteral("背景采样 #%1 [%2x%3]") + .arg(n->id()) + .arg(qRound(r.width())) + .arg(qRound(r.height())); + } else if (auto *geometry = n->toGeometry()) { + const QRectF r = geometry->boundingRect(); + const QString typeDesc = geometry->isFullyOpaque() ? QStringLiteral("不透明几何") + : QStringLiteral("透明几何"); + label = QStringLiteral("%1 #%2 [%3x%4]") + .arg(typeDesc) + .arg(n->id()) + .arg(qRound(r.width())) + .arg(qRound(r.height())); + } else if (auto *clip = n->toClip()) { + const QRectF r = clip->clipRect(); + label = QStringLiteral("裁剪节点 #%1 [%2x%3]") + .arg(n->id()) + .arg(qRound(r.width())) + .arg(qRound(r.height())); + } else if (auto *tr = n->toTransform()) { + const QTransform m = tr->matrix().toTransform(); + label = QStringLiteral("变换节点 #%1 (dx:%2, dy:%3)") + .arg(n->id()) + .arg(qRound(m.dx())) + .arg(qRound(m.dy())); + } else { + label = QStringLiteral("分组节点 #%1").arg(n->id()); + } + const QString displayName = QStringLiteral("%1:%2").arg(order).arg(label); + n->setName(displayName); + m_displayNames.insert(n->id(), displayName); + QVariantMap row; + row.insert(QStringLiteral("id"), n->id()); + row.insert(QStringLiteral("name"), displayName); + row.insert(QStringLiteral("type"), typeString(n->type())); + row.insert(QStringLiteral("depth"), depth); + row.insert(QStringLiteral("visible"), n->isVisible()); + row.insert(QStringLiteral("occluded"), worldHidden(n)); + row.insert(QStringLiteral("culled"), worldHidden(n)); + row.insert(QStringLiteral("childCount"), n->childCount()); + row.insert(QStringLiteral("parentId"), n->parent() ? n->parent()->id() : 0); + tree->append(row); + + if (n->hasContent()) { + auto *geo = n->toGeometry(); + const QRectF localRect = geo->boundingRect(); + const QTransform matrix = n->worldTransform().toTransform(); + const QRect aabb = n->worldBounds(); + + QVariantMap v; + v.insert(QStringLiteral("id"), n->id()); + v.insert(QStringLiteral("name"), displayName); + v.insert(QStringLiteral("type"), typeString(n->type())); + v.insert(QStringLiteral("localX"), localRect.x()); + v.insert(QStringLiteral("localY"), localRect.y()); + v.insert(QStringLiteral("localWidth"), localRect.width()); + v.insert(QStringLiteral("localHeight"), localRect.height()); + v.insert(QStringLiteral("m11"), matrix.m11()); + v.insert(QStringLiteral("m12"), matrix.m12()); + v.insert(QStringLiteral("m21"), matrix.m21()); + v.insert(QStringLiteral("m22"), matrix.m22()); + v.insert(QStringLiteral("m13"), matrix.m13()); + v.insert(QStringLiteral("m23"), matrix.m23()); + v.insert(QStringLiteral("m33"), matrix.m33()); + v.insert(QStringLiteral("dx"), matrix.dx()); + v.insert(QStringLiteral("dy"), matrix.dy()); + v.insert(QStringLiteral("x"), aabb.x()); + v.insert(QStringLiteral("y"), aabb.y()); + v.insert(QStringLiteral("w"), aabb.width()); + v.insert(QStringLiteral("h"), aabb.height()); + v.insert(QStringLiteral("hasContent"), n->hasContent()); + v.insert(QStringLiteral("occluded"), worldHidden(n)); + v.insert(QStringLiteral("culled"), worldHidden(n)); + v.insert(QStringLiteral("visible"), n->isVisible()); + const QColor c = m_decor.value(n->id()).color; + v.insert(QStringLiteral("color"), (c.isValid() ? c : QColor("#888888")).name()); + v.insert(QStringLiteral("isBackdrop"), n->needsBackdrop()); + v.insert(QStringLiteral("fullyOpaque"), geo->isFullyOpaque()); + v.insert(QStringLiteral("paintOrder"), order); + visual->append(v); + } else if (n->toClip()) { + visual->append(clipVisualRow(n, displayName, order)); + } + for (WSGDamageNode *c = n->firstChild(); c; c = c->nextSibling()) + collectVisual(c, visual, tree, depth + 1, paintOrder); +} + +void DemoScene::refreshSelectedProps() +{ + QVariantMap p; + WSGDamageNode *n = findNode(m_selectedId); + if (!n) { + m_selectedProps = p; + emit selectedPropsChanged(); + return; + } + if (!m_displayNames.contains(m_selectedId)) + rebuildLists(); + p.insert(QStringLiteral("id"), n->id()); + p.insert(QStringLiteral("name"), m_displayNames.value(n->id(), n->name())); + p.insert(QStringLiteral("type"), typeString(n->type())); + p.insert(QStringLiteral("visible"), n->isVisible()); + p.insert(QStringLiteral("occluded"), worldHidden(n)); + p.insert(QStringLiteral("culled"), worldHidden(n)); + p.insert(QStringLiteral("isRoot"), n == m_root.get()); + p.insert(QStringLiteral("isGeometry"), n->toGeometry() != nullptr); + p.insert(QStringLiteral("isTransform"), n->toTransform() != nullptr); + p.insert(QStringLiteral("isClip"), n->toClip() != nullptr); + p.insert(QStringLiteral("isBackdrop"), n->needsBackdrop()); + p.insert(QStringLiteral("canDelete"), n != m_root.get()); + p.insert(QStringLiteral("canRaise"), n->parent() && n->nextSibling()); + p.insert(QStringLiteral("canLower"), n->parent() && n->previousSibling()); + p.insert(QStringLiteral("canDirty"), n->toGeometry() != nullptr); + if (auto *geo = n->toGeometry()) { + const QRectF r = geo->boundingRect(); + p.insert(QStringLiteral("x"), r.x()); + p.insert(QStringLiteral("y"), r.y()); + p.insert(QStringLiteral("w"), r.width()); + p.insert(QStringLiteral("h"), r.height()); + p.insert(QStringLiteral("fullyOpaque"), geo->isFullyOpaque()); + } + if (auto *clip = n->toClip()) { + const QRectF r = clip->clipRect(); + p.insert(QStringLiteral("x"), r.x()); + p.insert(QStringLiteral("y"), r.y()); + p.insert(QStringLiteral("w"), r.width()); + p.insert(QStringLiteral("h"), r.height()); + } + if (auto *tr = n->toTransform()) { + const QTransform matrix = tr->matrix().toTransform(); + p.insert(QStringLiteral("tx"), matrix.dx()); + p.insert(QStringLiteral("ty"), matrix.dy()); + p.insert(QStringLiteral("rotation"), m_rotation); + p.insert(QStringLiteral("rotationAxis"), m_rotationAxis); + p.insert(QStringLiteral("sx"), m_scaleX); + p.insert(QStringLiteral("sy"), m_scaleY); + p.insert(QStringLiteral("m11"), matrix.m11()); + p.insert(QStringLiteral("m12"), matrix.m12()); + p.insert(QStringLiteral("m21"), matrix.m21()); + p.insert(QStringLiteral("m22"), matrix.m22()); + p.insert(QStringLiteral("m13"), matrix.m13()); + p.insert(QStringLiteral("m23"), matrix.m23()); + p.insert(QStringLiteral("m33"), matrix.m33()); + p.insert(QStringLiteral("dx"), matrix.dx()); + p.insert(QStringLiteral("dy"), matrix.dy()); + } + if (n->needsBackdrop()) { + const Decor &decor = m_decor.value(n->id()); + p.insert(QStringLiteral("expansion"), decor.expansion); + p.insert(QStringLiteral("clipExpansion"), decor.clipExpansion); + } + m_selectedProps = p; + emit selectedPropsChanged(); +} diff --git a/waylib/tests/manual/damage/demoscene.h b/waylib/tests/manual/damage/demoscene.h new file mode 100644 index 0000000000..522121875a --- /dev/null +++ b/waylib/tests/manual/damage/demoscene.h @@ -0,0 +1,166 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef DEMOSCENE_H +#define DEMOSCENE_H + +#include "wsgdamagetracker_p.h" +#include "visualnodemodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +WAYLIB_SERVER_USE_NAMESPACE + +class DemoScene : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(VisualNodeModel* visualNodes READ visualNodesModel CONSTANT) + Q_PROPERTY(QVariantList treeNodes READ treeNodes NOTIFY treeNodesChanged) + Q_PROPERTY(QVariantList damageRects READ damageRects NOTIFY damageChanged) + Q_PROPERTY(QVariantList damageRectsB READ damageRectsB NOTIFY damageChanged) + Q_PROPERTY(QVariantList damageFrames READ damageFrames NOTIFY damageChanged) + Q_PROPERTY(QRect viewportA READ viewportA NOTIFY sceneChanged) + Q_PROPERTY(QRect viewportB READ viewportB NOTIFY sceneChanged) + Q_PROPERTY(quint64 selectedId READ selectedId WRITE setSelectedId NOTIFY selectedIdChanged) + Q_PROPERTY(QVariantMap selectedProps READ selectedProps NOTIFY selectedPropsChanged) + Q_PROPERTY(bool autoCommit READ autoCommit WRITE setAutoCommit NOTIFY autoCommitChanged) + Q_PROPERTY(int refreshRate READ refreshRate WRITE setRefreshRate NOTIFY refreshRateChanged) + Q_PROPERTY(QVariantList demoScenes READ demoScenes CONSTANT) + Q_PROPERTY(QString demoSceneName READ demoSceneName NOTIFY demoSceneChanged) + Q_PROPERTY(bool demoRunning READ demoRunning WRITE setDemoRunning NOTIFY demoRunningChanged) + +public: + explicit DemoScene(QObject *parent = nullptr); + ~DemoScene() override; + + VisualNodeModel *visualNodesModel() const { return m_visualNodeModel; } + QVariantList treeNodes() const { return m_treeNodes; } + QVariantList damageRects() const { return m_damageRects; } + QVariantList damageRectsB() const { return m_damageRectsB; } + QVariantList damageFrames() const { return m_damageFrames; } + QRect viewportA() const { return m_viewportA; } + QRect viewportB() const { return m_viewportB; } + quint64 selectedId() const { return m_selectedId; } + QVariantMap selectedProps() const { return m_selectedProps; } + bool autoCommit() const { return m_autoCommit; } + int refreshRate() const { return m_refreshRate; } + QVariantList demoScenes() const; + QString demoSceneName() const { return m_demoSceneName; } + bool demoRunning() const { return m_demoRunning; } + + void setSelectedId(quint64 id); + void setAutoCommit(bool enabled); + void setRefreshRate(int refreshRate); + void setDemoRunning(bool running); + + Q_INVOKABLE void loadDemoScene(const QString &name); + Q_INVOKABLE void stepDemoFrame(); + Q_INVOKABLE void moveNode(quint64 nodeId, quint64 newParentId, + quint64 beforeSiblingId = 0); + Q_INVOKABLE void activateNode(quint64 id); + + Q_INVOKABLE void addBasic(); + Q_INVOKABLE void addTransform(); + Q_INVOKABLE void addClip(); + Q_INVOKABLE void addGeometry(); + Q_INVOKABLE void addBackdrop(); + Q_INVOKABLE void addRenderer(); + Q_INVOKABLE void removeSelected(); + Q_INVOKABLE void raiseSelected(); + Q_INVOKABLE void lowerSelected(); + Q_INVOKABLE void setVisibleSelected(bool visible); + Q_INVOKABLE void setRectSelected(qreal x, qreal y, qreal w, qreal h); + Q_INVOKABLE void setTranslationSelected(qreal x, qreal y); + Q_INVOKABLE void setRotationSelected(qreal degrees, int axis = 2); + Q_INVOKABLE void setScaleSelected(qreal sx, qreal sy); + Q_INVOKABLE void setFullyOpaqueSelected(bool opaque); + Q_INVOKABLE void setExpansionSelected(int px); + Q_INVOKABLE void setClipExpansionSelected(bool clip); + Q_INVOKABLE void markSelectedContentDirty(); + Q_INVOKABLE void markSelectedContentDirtyAt(qreal x, qreal y, qreal w, qreal h); + Q_INVOKABLE void moveSelectedBy(qreal dx, qreal dy); + Q_INVOKABLE void finishSelectedMove(); + Q_INVOKABLE void loadPreset(const QString &name); + Q_INVOKABLE void commit(); + Q_INVOKABLE void clearTree(); + +signals: + void sceneChanged(); + void treeNodesChanged(); + void damageChanged(); + void selectedIdChanged(); + void selectedPropsChanged(); + void autoCommitChanged(); + void refreshRateChanged(); + void demoSceneChanged(); + void demoRunningChanged(); + +private: + struct Decor { + QColor color; + int expansion = 0; + bool clipExpansion = true; + }; + + WSGDamageNode *findNode(quint64 id) const; + WSGDamageNode *findNodeRecursive(WSGDamageNode *n, quint64 id) const; + WSGDamageNode *parentForInsert() const; + QColor nextColor(); + void rebuildLists(); + void rebuildVisualNodes(); + void collectVisualOnly(WSGDamageNode *n, QVector *visual, int *paintOrder); + void collectVisual(WSGDamageNode *n, QVector *visual, QVariantList *tree, + int depth, int *paintOrder); + void refreshSelectedProps(); + void maybeCommit(); + void updateDamage(bool rebuildScene); + void resetRoot(); + void advanceDemoFrame(); + void buildDemoScene(const QString &name); + bool isDescendantOf(const WSGDamageNode *node, const WSGDamageNode *ancestor) const; + + std::unique_ptr m_root; + WSGDamageTracker m_tracker; + std::shared_ptr m_demoViewport = std::make_shared(); + QHash m_decor; + QHash m_displayNames; + VisualNodeModel *m_visualNodeModel = nullptr; + QVariantList m_treeNodes; + QVariantList m_damageRects; + QVariantList m_damageRectsB; + QVariantList m_damageFrames; + QRect m_viewportA{0, 0, 360, 480}; + QRect m_viewportB{360, 0, 360, 480}; + quint64 m_selectedId = 0; + QVariantMap m_selectedProps; + bool m_autoCommit = true; + bool m_dragFramePending = false; + bool m_demoRunning = false; + int m_refreshRate = 60; + int m_demoFrame = 0; + QString m_demoSceneName; + quint64 m_demoNodeA = 0; + quint64 m_demoNodeB = 0; + quint64 m_demoNodeC = 0; + int m_colorIndex = 0; + int m_rotationAxis = 2; + qreal m_rotation = 0; + qreal m_scaleX = 1; + qreal m_scaleY = 1; + QTimer m_dragFrameTimer; + QTimer m_demoTimer; +}; + +#endif diff --git a/waylib/tests/manual/damage/helper.h b/waylib/tests/manual/damage/helper.h new file mode 100644 index 0000000000..b0fd243e90 --- /dev/null +++ b/waylib/tests/manual/damage/helper.h @@ -0,0 +1,68 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +#include +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE +class WServer; +class WOutputRenderWindow; +class WQuickOutputLayout; +class WCursor; +class WSeat; +class WBackend; +WAYLIB_SERVER_END_NAMESPACE + +struct wlr_renderer; +struct wlr_allocator; +struct wlr_compositor; + +WAYLIB_SERVER_USE_NAMESPACE + +class Q_DECL_HIDDEN Helper : public QObject +{ + Q_OBJECT + Q_PROPERTY(WQmlCreator *outputCreator MEMBER m_outputCreator CONSTANT) + Q_PROPERTY(QString damageVisual READ damageVisual WRITE setDamageVisual NOTIFY damageVisualChanged) + Q_PROPERTY(QString damageDebugMode READ damageDebugMode WRITE setDamageDebugMode NOTIFY damageDebugModeChanged) + QML_ELEMENT + QML_SINGLETON + +public: + explicit Helper(QObject *parent = nullptr); + + void initProtocols(WOutputRenderWindow *window, QQmlEngine *qmlEngine); + + inline WBackend *backend() const + { + return m_backend; + } + + QString damageVisual() const; + void setDamageVisual(const QString &mode); + QString damageDebugMode() const { return damageVisual(); } + void setDamageDebugMode(const QString &mode) { setDamageVisual(mode); } + +Q_SIGNALS: + void damageVisualChanged(); + void damageDebugModeChanged(); + +private: + WServer *m_server = nullptr; + WQmlCreator *m_outputCreator = nullptr; + + WBackend *m_backend = nullptr; + wlr_renderer *m_renderer = nullptr; + wlr_allocator *m_allocator = nullptr; + wlr_compositor *m_compositor = nullptr; + WQuickOutputLayout *m_outputLayout = nullptr; + WCursor *m_cursor = nullptr; + QPointer m_seat; + QPointer m_window; +}; diff --git a/waylib/tests/manual/damage/main.cpp b/waylib/tests/manual/damage/main.cpp new file mode 100644 index 0000000000..fcd8ff2fae --- /dev/null +++ b/waylib/tests/manual/damage/main.cpp @@ -0,0 +1,169 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include "helper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +Helper::Helper(QObject *parent) + : QObject(parent) + , m_server(new WServer(this)) + , m_outputCreator(new WQmlCreator(this)) + , m_outputLayout(new WQuickOutputLayout(m_server)) + , m_cursor(new WCursor(this)) +{ + m_seat = m_server->attach(); + m_seat->setCursor(m_cursor); + m_cursor->setLayout(m_outputLayout); +} + +QString Helper::damageVisual() const +{ + if (!m_window) + return QStringLiteral("off"); + switch (m_window->damageVisual()) { + case WOutputRenderWindow::DamageVisual::Highlight: + return QStringLiteral("highlight"); + case WOutputRenderWindow::DamageVisual::Off: + break; + } + return QStringLiteral("off"); +} + +void Helper::setDamageVisual(const QString &mode) +{ + if (!m_window) + return; + const QString lower = mode.toLower().trimmed(); + WOutputRenderWindow::DamageVisual visual = WOutputRenderWindow::DamageVisual::Off; + if (lower == "highlight" || lower == "1" || lower == "true" || lower == "on") + visual = WOutputRenderWindow::DamageVisual::Highlight; + else if (lower == "off" || lower == "none" || lower == "0" || lower == "false") + visual = WOutputRenderWindow::DamageVisual::Off; + else + return; + if (m_window->damageVisual() == visual) + return; + m_window->setDamageVisual(visual); + Q_EMIT damageVisualChanged(); + Q_EMIT damageDebugModeChanged(); +} + +void Helper::initProtocols(WOutputRenderWindow *window, QQmlEngine *qmlEngine) +{ + m_window = window; + m_backend = m_server->attach(); + m_server->start(); + + m_renderer = WRenderHelper::createRenderer(m_backend->handle()); + + if (!m_renderer) { + qFatal("Failed to create renderer"); + } + + connect(m_backend, &WBackend::outputAdded, this, [this, qmlEngine] (WOutput *output) { + auto initProperties = qmlEngine->newObject(); + initProperties.setProperty("waylandOutput", qmlEngine->toScriptValue(output)); + initProperties.setProperty("layout", qmlEngine->toScriptValue(m_outputLayout)); + initProperties.setProperty("x", qmlEngine->toScriptValue(m_outputLayout->implicitWidth())); + + m_outputCreator->add(output, initProperties); + }); + + connect(m_backend, &WBackend::outputRemoved, this, [this] (WOutput *output) { + m_outputCreator->removeByOwner(output); + }); + + connect(m_backend, &WBackend::inputAdded, this, [this] (WInputDevice *device) { + m_seat->attachInputDevice(device); + }); + + connect(m_backend, &WBackend::inputRemoved, this, [this] (WInputDevice *device) { + m_seat->detachInputDevice(device); + }); + + m_allocator = wlr_allocator_autocreate(m_backend->handle(), m_renderer); + wlr_renderer_init_wl_display(m_renderer, m_server->handle()); + + // free follow display + m_compositor = wlr_compositor_create(m_server->handle(), 6, m_renderer); + wlr_subcompositor_create(m_server->handle()); + + connect(window, &WOutputRenderWindow::outputViewportInitialized, this, [] (WOutputViewport *viewport) { + { + WOutput *output = viewport->output(); + auto *wlrOutput = output->handle(); + if (!output->property("_Enabled").toBool()) { + output->setProperty("_Enabled", true); + wlr_output_state newState; + wlr_output_state_init(&newState); + + if (!wlrOutput->current_mode) { + auto mode = wlr_output_preferred_mode(wlrOutput); + if (mode) + wlr_output_state_set_mode(&newState, mode); + } + wlr_output_state_set_enabled(&newState, true); + if (!wlr_output_commit_state(wlrOutput, &newState)) { + qCritical("commit failed on output %s", wlrOutput->name); + } + wlr_output_state_finish(&newState); + } + } + }); + connect(window, &WOutputRenderWindow::damageVisualChanged, this, [this] { + Q_EMIT damageVisualChanged(); + Q_EMIT damageDebugModeChanged(); + }); + window->init(m_renderer, m_allocator); + + // Default highlight unless env already sets + if (qEnvironmentVariableIsEmpty("WAYLIB_DEBUG_DAMAGE") && window->damageVisual() == WOutputRenderWindow::DamageVisual::Off) { + window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + } + + wlr_backend_start(m_backend->handle()); +} + +int main(int argc, char *argv[]) +{ + WLog::init(); + WServer::initializeQPA(); + QQuickStyle::setStyle("Fusion"); + + QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + QGuiApplication::setQuitOnLastWindowClosed(false); + QGuiApplication app(argc, argv); + + QQmlApplicationEngine waylandEngine; + waylandEngine.loadFromModule("DamagePlayground", "Main"); + + auto window = waylandEngine.rootObjects().first()->findChild(); + Q_ASSERT(window); + + Helper *helper = waylandEngine.singletonInstance("DamagePlayground", "Helper"); + Q_ASSERT(helper); + + helper->initProtocols(window, &waylandEngine); + + return app.exec(); +} diff --git a/waylib/tests/manual/damage/visualnodemodel.cpp b/waylib/tests/manual/damage/visualnodemodel.cpp new file mode 100644 index 0000000000..3e39a9356c --- /dev/null +++ b/waylib/tests/manual/damage/visualnodemodel.cpp @@ -0,0 +1,102 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "visualnodemodel.h" + +VisualNodeModel::VisualNodeModel(QObject *parent) + : QAbstractListModel(parent) +{ + m_roleNames = { + {IdRole, "id"}, + {NameRole, "name"}, + {TypeRole, "type"}, + {LocalXRole, "localX"}, + {LocalYRole, "localY"}, + {LocalWidthRole, "localWidth"}, + {LocalHeightRole, "localHeight"}, + {M11Role, "m11"}, + {M12Role, "m12"}, + {M21Role, "m21"}, + {M22Role, "m22"}, + {M13Role, "m13"}, + {M23Role, "m23"}, + {M33Role, "m33"}, + {DxRole, "dx"}, + {DyRole, "dy"}, + {XRole, "x"}, + {YRole, "y"}, + {WRole, "w"}, + {HRole, "h"}, + {OccludedRole, "occluded"}, + {CulledRole, "culled"}, + {VisibleRole, "visible"}, + {ColorRole, "color"}, + {IsBackdropRole, "isBackdrop"}, + {FullyOpaqueRole, "fullyOpaque"}, + {PaintOrderRole, "paintOrder"}, + {HasContentRole, "hasContent"}, + }; +} + +int VisualNodeModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_nodes.size(); +} + +QVariant VisualNodeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_nodes.size()) + return {}; + const QVariantMap &node = m_nodes.at(index.row()); + auto it = m_roleNames.constFind(role); + if (it == m_roleNames.cend()) + return {}; + return node.value(it.value()); +} + +QHash VisualNodeModel::roleNames() const +{ + return m_roleNames; +} + +void VisualNodeModel::setNodes(const QVector &nodes) +{ + if (nodes.size() == m_nodes.size()) { + // Same count: update in place, emit dataChanged per row. + for (int i = 0; i < nodes.size(); ++i) { + m_nodes[i] = nodes.at(i); + const QModelIndex idx = index(i); + emit dataChanged(idx, idx); + } + } else { + // Different count: full reset. + beginResetModel(); + m_nodes = nodes; + endResetModel(); + } +} + +void VisualNodeModel::updateNode(int row, const QVariantMap &data) +{ + if (row < 0 || row >= m_nodes.size()) + return; + m_nodes[row] = data; + const QModelIndex idx = index(row); + emit dataChanged(idx, idx); +} + +void VisualNodeModel::clear() +{ + beginResetModel(); + m_nodes.clear(); + endResetModel(); +} + +QVariantMap VisualNodeModel::getRow(int row) const +{ + if (row < 0 || row >= m_nodes.size()) + return {}; + return m_nodes.at(row); +} diff --git a/waylib/tests/manual/damage/visualnodemodel.h b/waylib/tests/manual/damage/visualnodemodel.h new file mode 100644 index 0000000000..e3ff059993 --- /dev/null +++ b/waylib/tests/manual/damage/visualnodemodel.h @@ -0,0 +1,66 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef VISUALNODEMODEL_H +#define VISUALNODEMODEL_H + +#include +#include +#include +#include +#include + +class VisualNodeModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + NameRole, + TypeRole, + LocalXRole, + LocalYRole, + LocalWidthRole, + LocalHeightRole, + M11Role, + M12Role, + M21Role, + M22Role, + M13Role, + M23Role, + M33Role, + DxRole, + DyRole, + XRole, + YRole, + WRole, + HRole, + OccludedRole, + CulledRole, + VisibleRole, + ColorRole, + IsBackdropRole, + FullyOpaqueRole, + PaintOrderRole, + HasContentRole, + }; + Q_ENUM(Roles) + + explicit VisualNodeModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setNodes(const QVector &nodes); + void updateNode(int row, const QVariantMap &data); + void clear(); + QVariantMap getRow(int row) const; + +private: + QVector m_nodes; + QHash m_roleNames; +}; + +#endif // VISUALNODEMODEL_H diff --git a/waylib/tests/manual/damagegraph-demo/CMakeLists.txt b/waylib/tests/manual/damagegraph-demo/CMakeLists.txt new file mode 100644 index 0000000000..c01b91d3ad --- /dev/null +++ b/waylib/tests/manual/damagegraph-demo/CMakeLists.txt @@ -0,0 +1,44 @@ +# Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +find_package(Qt6 COMPONENTS Quick QuickControls2 Qml REQUIRED) +qt_standard_project_setup(REQUIRES 6.4) + +if(QT_KNOWN_POLICY_QTP0001) + qt_policy(SET QTP0001 NEW) +endif() +if(POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +set(DAMAGEGRAPH_CORE "${CMAKE_CURRENT_SOURCE_DIR}/../damage") + +qt_add_executable(damagegraph-demo + main.cpp +) + +qt_add_qml_module(damagegraph-demo + URI DamageGraphDemo + VERSION 1.0 + QML_FILES + Main.qml + SOURCES + ${DAMAGEGRAPH_CORE}/demoscene.h + ${DAMAGEGRAPH_CORE}/demoscene.cpp + ${DAMAGEGRAPH_CORE}/damageoverlay.h + ${DAMAGEGRAPH_CORE}/damageoverlay.cpp + ${DAMAGEGRAPH_CORE}/visualnodemodel.h + ${DAMAGEGRAPH_CORE}/visualnodemodel.cpp +) + +target_include_directories(damagegraph-demo PRIVATE + ${DAMAGEGRAPH_CORE} +) + +target_link_libraries(damagegraph-demo PRIVATE + waylibserver + Qt6::Quick + Qt6::QuickControls2 + Qt6::Qml + Qt6::Gui +) diff --git a/waylib/tests/manual/damagegraph-demo/Main.qml b/waylib/tests/manual/damagegraph-demo/Main.qml new file mode 100644 index 0000000000..9d92c22eef --- /dev/null +++ b/waylib/tests/manual/damagegraph-demo/Main.qml @@ -0,0 +1,1418 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import DamageGraphDemo + +ApplicationWindow { + id: win + width: 1440 + height: 860 + minimumWidth: 1080 + minimumHeight: 680 + visible: true + title: "Waylib 损伤跟踪器" + color: "#0a0f1a" + + readonly property color panelColor: "#111827" + readonly property color raisedColor: "#172033" + readonly property color borderColor: "#263247" + readonly property color textColor: "#f3f6fb" + readonly property color mutedColor: "#8996aa" + readonly property color accentColor: "#6d7cff" + property var sel: scene.selectedProps + property int damageHistoryDuration: 200 + readonly property int detectedRefreshRate: win.screen && win.screen.refreshRate > 0 + ? Math.round(win.screen.refreshRate) : 60 + property int sceneRefreshRate: detectedRefreshRate + property bool treeDragging: false + property int treeDragId: 0 + property string treeDragName: "" + property int treeDropTargetId: 0 + property int treeDropParentId: 0 + property int treeDropBeforeId: 0 + property int treeDropMode: 0 + property real treePressX: 0 + property real treePressY: 0 + property real treeGhostX: 0 + property real treeGhostY: 0 + property string treeDropHint: "" + property bool demoPausedByInteraction: false + + function beginUserInteraction() { + if (scene.demoRunning) { + demoPausedByInteraction = true + scene.demoRunning = false + } + } + + function endUserInteraction() { + if (demoPausedByInteraction) { + demoPausedByInteraction = false + scene.demoRunning = true + } + } + palette.window: color + palette.windowText: textColor + palette.base: raisedColor + palette.alternateBase: panelColor + palette.text: textColor + palette.button: raisedColor + palette.buttonText: textColor + palette.highlight: accentColor + palette.highlightedText: "#ffffff" + palette.mid: borderColor + + DemoScene { + id: scene + refreshRate: win.sceneRefreshRate + Component.onCompleted: loadDemoScene("occlusion") + } + + Connections { + target: scene + function onSceneChanged() { + if (!win.treeDragging) + win.resetTreeDrag() + } + } + + function num(v, fallback) { + const n = Number(v) + return Number.isFinite(n) ? n : (fallback ?? 0) + } + + function typeLabel(type) { + if (type === "Geometry") + return "几何节点" + if (type === "Backdrop") + return "背景采样节点" + if (type === "Transform") + return "变换节点" + return "分组节点" + } + + function revealSelectedNode() { + for (let i = 0; i < scene.treeNodes.length; ++i) { + if (scene.treeNodes[i].id === scene.selectedId) { + tree.positionViewAtIndex(i, ListView.Contain) + return + } + } + } + function isNodeDescendantOf(targetId, ancestorId) { + if (!targetId || !ancestorId) + return false + if (targetId === ancestorId) + return true + let curr = targetId + while (curr !== 0) { + let found = false + for (let i = 0; i < scene.treeNodes.length; ++i) { + const item = scene.treeNodes[i] + if (item.id === curr) { + if (item.parentId === ancestorId) + return true + curr = item.parentId || 0 + found = true + break + } + } + if (!found || curr === 0) + break + } + return false + } + + function calculateTreeDrop(mouseXInTree, mouseYInTree) { + const count = scene.treeNodes.length + if (!count || !treeDragId) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "" + return + } + + let index = tree.indexAt(mouseXInTree, mouseYInTree) + if (index < 0) { + if (mouseYInTree <= 0) + index = 0 + else + index = count - 1 + } + index = Math.max(0, Math.min(count - 1, index)) + const target = scene.treeNodes[index] + if (!target) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "" + return + } + + if (target.id === treeDragId) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "保持原位置" + return + } + if (isNodeDescendantOf(target.id, treeDragId)) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "无法移动至子孙节点" + return + } + + // Derive the stride from delegate geometry instead of hard-coding 38. + let rowStride = 36 + tree.spacing + if (tree.contentItem.children.length > 0) + rowStride = tree.contentItem.children[0].height + tree.spacing + const rowTop = index * rowStride - tree.contentY + const relY = mouseYInTree - rowTop + + let dragParentId = 0 + let dragIndex = -1 + for (let i = 0; i < count; ++i) { + if (scene.treeNodes[i].id === treeDragId) { + dragParentId = scene.treeNodes[i].parentId || 0 + dragIndex = i + break + } + } + + if (target.id === scene.treeNodes[0].id) { + treeDropTargetId = target.id + treeDropMode = 2 + treeDropParentId = target.id + treeDropBeforeId = 0 + treeDropHint = "作为 " + target.name + " 的子节点" + return + } + + if (relY < 10) { + const targetParentId = target.parentId || 0 + if (targetParentId === dragParentId && dragIndex >= 0 && index === dragIndex + 1) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "保持原位置" + return + } + treeDropTargetId = target.id + treeDropMode = 1 + treeDropParentId = targetParentId + treeDropBeforeId = target.id + treeDropHint = "插入到 " + target.name + " 之前" + } else if (relY > 26) { + const targetParentId = target.parentId || 0 + if (targetParentId === dragParentId && dragIndex >= 0 && index === dragIndex - 1) { + treeDropTargetId = 0 + treeDropMode = 0 + treeDropHint = "保持原位置" + return + } + treeDropTargetId = target.id + treeDropMode = 3 + treeDropParentId = targetParentId + // treeNodes is a pre-order flattening: the target's descendants + // sit right behind it, so skip them to find the next sibling. + const targetDepth = target.depth || 0 + let nextId = 0 + for (let j = index + 1; j < count; ++j) { + const following = scene.treeNodes[j] + if ((following.depth || 0) > targetDepth) + continue + if ((following.parentId || 0) === targetParentId) + nextId = following.id + break + } + treeDropBeforeId = nextId + treeDropHint = "插入到 " + target.name + " 之后" + } else { + treeDropTargetId = target.id + treeDropMode = 2 + treeDropParentId = target.id + treeDropBeforeId = 0 + treeDropHint = "作为 " + target.name + " 的子节点" + } + } + function resetTreeDrag() { + treeDragging = false + treeDragId = 0 + treeDropTargetId = 0 + treeDropParentId = 0 + treeDropBeforeId = 0 + treeDropMode = 0 + treeDropHint = "" + treeDragName = "" + } + Action { + id: addGeometryAction + text: "几何节点" + shortcut: "Ctrl+Shift+G" + onTriggered: scene.addGeometry() + } + Action { + id: addTransformAction + text: "变换节点" + shortcut: "Ctrl+Shift+T" + onTriggered: scene.addTransform() + } + Action { + id: addClipAction + text: "裁剪节点" + onTriggered: scene.addClip() + } + Action { + id: addBackdropAction + text: "背景采样节点" + onTriggered: scene.addBackdrop() + } + Action { + id: addRendererAction + text: "自定义渲染节点" + onTriggered: scene.addRenderer() + } + Action { + id: addGroupAction + text: "分组节点" + onTriggered: scene.addBasic() + shortcut: "Ctrl+Shift+N" + } + Action { + id: moveUpAction + text: "上移一层" + enabled: !!win.sel.canRaise + onTriggered: scene.raiseSelected() + } + Action { + id: moveDownAction + text: "下移一层" + enabled: !!win.sel.canLower + onTriggered: scene.lowerSelected() + } + Action { + id: visibilityAction + text: win.sel.visible ? "隐藏节点" : "显示节点" + enabled: !!win.sel.id + onTriggered: scene.setVisibleSelected(!win.sel.visible) + } + Action { + id: dirtyAction + text: "标记内容损伤" + shortcut: "Ctrl+D" + enabled: !!win.sel.canDirty + onTriggered: scene.markSelectedContentDirty() + } + Action { + id: deleteAction + text: "删除节点" + shortcut: "Delete" + enabled: !!win.sel.canDelete + onTriggered: scene.removeSelected() + } + + Menu { + id: addNodeMenu + y: addNodeButton.height + 6 + MenuItem { action: addGeometryAction } + MenuItem { action: addTransformAction } + MenuItem { action: addClipAction } + MenuItem { action: addBackdropAction } + MenuItem { action: addRendererAction } + MenuSeparator {} + MenuItem { action: addGroupAction } + } + + Menu { + id: nodeMenu + title: win.sel.name || "节点" + Menu { + title: "新增子节点" + MenuItem { action: addGeometryAction } + MenuItem { action: addTransformAction } + MenuItem { action: addClipAction } + MenuItem { action: addBackdropAction } + MenuItem { action: addRendererAction } + MenuItem { action: addGroupAction } + } + MenuSeparator {} + MenuItem { action: moveUpAction } + MenuItem { action: moveDownAction } + MenuItem { action: visibilityAction } + MenuItem { action: dirtyAction } + MenuSeparator {} + MenuItem { action: deleteAction } + } + + Menu { + id: treeOptionsMenu + y: treeOptionsButton.height + 6 + MenuItem { + text: "选择根节点" + onTriggered: scene.selectedId = scene.treeNodes.length ? scene.treeNodes[0].id : 0 + } + MenuItem { + text: "立即提交" + onTriggered: scene.commit() + } + MenuSeparator {} + MenuItem { + text: "清空节点树" + onTriggered: scene.clearTree() + } + } + + header: Rectangle { + height: 72 + color: "#0f1625" + border.color: win.borderColor + border.width: 1 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 22 + anchors.rightMargin: 18 + spacing: 18 + + ColumnLayout { + spacing: 1 + Layout.minimumWidth: 250 + Label { + text: "损伤跟踪器" + color: win.textColor + font.pixelSize: 20 + font.weight: Font.DemiBold + } + Label { + text: "场景树、遮挡剔除与多输出损伤" + color: win.mutedColor + font.pixelSize: 11 + } + } + + Item { Layout.fillWidth: true } + + RowLayout { + spacing: 10 + Label { text: "图例"; color: win.mutedColor; font.pixelSize: 10 } + Rectangle { + width: 40; height: 16; radius: 4 + border.color: "#8794ff" + gradient: Gradient { + GradientStop { position: 0; color: "#a6b0ff" } + GradientStop { position: 1; color: "#4e5ee8" } + } + } + Label { text: "节点"; color: win.mutedColor; font.pixelSize: 10 } + Rectangle { width: 40; height: 16; radius: 3; color: "#2dc470" } + Label { text: "较旧损伤"; color: win.mutedColor; font.pixelSize: 10 } + Rectangle { width: 40; height: 16; radius: 3; color: "#e83e4e" } + Label { text: "最新损伤"; color: win.mutedColor; font.pixelSize: 10 } + + Rectangle { width: 1; height: 34; color: win.borderColor } + + ComboBox { + id: demoSceneCombo + Layout.preferredWidth: 150 + model: scene.demoScenes + textRole: "text" + valueRole: "value" + onActivated: scene.loadDemoScene(currentValue) + ToolTip.visible: hovered + ToolTip.text: "选择只读自动演示场景" + } + Button { + text: scene.demoRunning ? "暂停演示" : "继续演示" + onClicked: scene.demoRunning = !scene.demoRunning + } + Label { + text: scene.demoRunning ? "自动演示中" : "演示已暂停" + color: scene.demoRunning ? "#8dc8e8" : "#ffcf70" + font.pixelSize: 10 + } + Switch { + text: "自动提交" + checked: scene.autoCommit + onToggled: scene.autoCommit = checked + ToolTip.visible: hovered + ToolTip.text: "每次编辑后自动提交" + } + Button { + text: "提交" + highlighted: true + onClicked: scene.commit() + } + } + } + + } + SplitView { + anchors.fill: parent + orientation: Qt.Horizontal + + Rectangle { + SplitView.preferredWidth: 330 + SplitView.minimumWidth: 285 + color: win.panelColor + border.color: win.borderColor + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 10 + + RowLayout { + Layout.fillWidth: true + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Label { + text: "节点树" + color: win.mutedColor + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + } + Label { + text: "共 " + scene.treeNodes.length + " 个节点" + color: win.textColor + font.pixelSize: 15 + font.weight: Font.DemiBold + } + } + ToolButton { + id: treeOptionsButton + text: "•••" + onClicked: treeOptionsMenu.popup(treeOptionsButton, 0, + treeOptionsButton.height + 6) + ToolTip.visible: hovered + ToolTip.text: "节点树菜单" + } + } + + RowLayout { + Layout.fillWidth: true + Button { + id: addNodeButton + Layout.fillWidth: true + text: "+ 新增子节点" + onClicked: addNodeMenu.popup(addNodeButton, 0, + addNodeButton.height + 6) + } + ToolButton { + text: "↑" + enabled: moveUpAction.enabled + onClicked: moveUpAction.trigger() + ToolTip.visible: hovered + ToolTip.text: "上移选中节点" + } + ToolButton { + text: "↓" + enabled: moveDownAction.enabled + onClicked: moveDownAction.trigger() + ToolTip.visible: hovered + ToolTip.text: "下移选中节点" + } + } + + Label { + Layout.fillWidth: true + text: "新节点将添加到 " + (win.sel.name || "根节点") + " 下" + color: win.mutedColor + font.pixelSize: 10 + elide: Text.ElideRight + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#0d1422" + radius: 8 + border.color: win.borderColor + + ListView { + id: tree + anchors.fill: parent + anchors.margins: 6 + clip: true + model: scene.treeNodes + spacing: 2 + boundsBehavior: Flickable.StopAtBounds + + delegate: Rectangle { + id: treeRow + required property var modelData + width: tree.width + height: 36 + radius: 6 + opacity: win.treeDragging && win.treeDragId === modelData.id ? 0.35 : 1.0 + color: modelData.id === scene.selectedId + ? "#283452" : (rowMouse.containsMouse ? "#182238" : "transparent") + border.color: modelData.id === scene.selectedId ? "#6476d9" : "transparent" + + Rectangle { + visible: modelData.depth > 0 + x: 12 + (modelData.depth - 1) * 17 + width: 1 + height: parent.height + color: "#2b3850" + } + + // Indicator: insert before + Rectangle { + visible: win.treeDragging && win.treeDropTargetId === modelData.id + && win.treeDropMode === 1 + anchors.left: parent.left + anchors.right: parent.right + y: -2 + height: 4 + radius: 2 + color: "#ff8794" + z: 10 + } + // Indicator: insert after + Rectangle { + visible: win.treeDragging && win.treeDropTargetId === modelData.id + && win.treeDropMode === 3 + anchors.left: parent.left + anchors.right: parent.right + y: parent.height - 2 + height: 4 + radius: 2 + color: "#ff8794" + z: 10 + } + // Indicator: become child + Rectangle { + visible: win.treeDragging && win.treeDropTargetId === modelData.id + && win.treeDropMode === 2 + anchors.fill: parent + color: "#2d4e78" + border.color: "#6db8ff" + border.width: 2 + radius: 6 + z: 5 + } + + Row { + anchors.verticalCenter: parent.verticalCenter + leftPadding: 10 + modelData.depth * 17 + spacing: 8 + Rectangle { + width: 18; height: 18; radius: 5 + anchors.verticalCenter: parent.verticalCenter + color: modelData.type === "Geometry" ? "#596de8" + : modelData.type === "Backdrop" ? "#0891b2" + : modelData.type === "Transform" ? "#d28a2d" : "#556176" + Label { + anchors.centerIn: parent + text: modelData.type === "Geometry" ? "G" + : modelData.type === "Backdrop" ? "B" + : modelData.type === "Transform" ? "T" : "·" + color: "#ffffff" + font.pixelSize: 9 + font.bold: true + } + } + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: -1 + Text { + text: modelData.name || win.typeLabel(modelData.type) + color: modelData.visible ? win.textColor : "#667289" + font.pixelSize: 12 + } + Text { + text: win.typeLabel(modelData.type) + + (modelData.occluded ? " · 已遮挡" : "") + + (modelData.culled && !modelData.occluded ? " · 已剔除" : "") + + (!modelData.visible ? " · 已隐藏" : "") + color: win.mutedColor + font.pixelSize: 9 + } + } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: function(mouse) { + scene.selectedId = modelData.id + if (mouse.button === Qt.LeftButton) { + beginUserInteraction() + treePressX = mouse.x + treePressY = mouse.y + treeDragId = modelData.id + treeDragName = modelData.name + treeDragging = false + var p = rowMouse.mapToItem(win.contentItem, mouse.x, mouse.y) + treeGhostX = p.x + 10 + treeGhostY = p.y + 10 + } else if (mouse.button === Qt.RightButton) { + nodeMenu.popup(treeRow, mouse.x, mouse.y) + } + } + onPositionChanged: function(mouse) { + if (!(mouse.buttons & Qt.LeftButton)) + return + if (!win.treeDragging + && (Math.abs(mouse.x - treePressX) + Math.abs(mouse.y - treePressY) > 4)) + win.treeDragging = true + + if (win.treeDragging) { + var winPt = rowMouse.mapToItem(win.contentItem, mouse.x, mouse.y) + treeGhostX = winPt.x + 12 + treeGhostY = winPt.y + 12 + var treePt = rowMouse.mapToItem(tree, mouse.x, mouse.y) + calculateTreeDrop(treePt.x, treePt.y) + } + } + onReleased: function(mouse) { + if (mouse.button === Qt.LeftButton) { + if (win.treeDragging) { + const dragId = win.treeDragId + const dropParentId = win.treeDropParentId + const dropBeforeId = win.treeDropBeforeId + const hasValidDrop = (dragId !== 0 && win.treeDropTargetId !== 0) + // Single reset, single commit: the duplicated + // resetTreeDrag/moveNode below double-committed drops. + win.resetTreeDrag() + endUserInteraction() + if (hasValidDrop) { + Qt.callLater(function() { + scene.moveNode(dragId, dropParentId, dropBeforeId) + }) + } + } else { + endUserInteraction() + } + } + } + onCanceled: { + win.resetTreeDrag() + endUserInteraction() + } + } + } + } + } + + Label { + Layout.fillWidth: true + text: "拖动节点可调整顺序或改变父子关系;右键点击节点打开操作菜单。" + wrapMode: Text.Wrap + color: win.mutedColor + font.pixelSize: 10 + } + } + } + + Rectangle { + SplitView.fillWidth: true + SplitView.minimumWidth: 480 + color: win.color + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 10 + + RowLayout { + Layout.fillWidth: true + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Label { + text: "场景预览" + color: win.mutedColor + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + } + Label { + text: "拖动可见节点即可修改几何位置" + color: win.textColor + font.pixelSize: 14 + font.weight: Font.DemiBold + } + } + Rectangle { + implicitWidth: damageStatus.implicitWidth + 18 + implicitHeight: 28 + radius: 14 + color: (scene.damageRects.length || scene.damageRectsB.length) ? "#34202c" : "#172638" + border.color: (scene.damageRects.length || scene.damageRectsB.length) ? "#8e3f55" : "#2b526a" + Label { + id: damageStatus + anchors.centerIn: parent + text: (scene.damageRects.length || scene.damageRectsB.length) + ? ("损伤 A " + scene.damageRects.length + " · B " + scene.damageRectsB.length) + : "无损伤" + color: (scene.damageRects.length || scene.damageRectsB.length) ? "#ffacba" : "#8dc8e8" + font.pixelSize: 10 + font.weight: Font.DemiBold + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: 10 + color: "#090e18" + border.color: win.borderColor + clip: true + + Item { + id: canvas + anchors.fill: parent + anchors.margins: 10 + clip: true + + Canvas { + anchors.fill: parent + onPaint: { + const ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = "#0b1220" + ctx.fillRect(0, 0, width, height) + ctx.strokeStyle = "#172238" + ctx.lineWidth = 1 + const s = 24 + for (let x = 0.5; x < width; x += s) { + ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke() + } + for (let y = 0.5; y < height; y += s) { + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke() + } + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + Repeater { + model: scene.visualNodes + delegate: Item { + id: visualItem + + + width: 0 + height: 0 + visible: model.visible + + transform: Matrix4x4 { + matrix: Qt.matrix4x4( + model.m11, model.m21, 0, model.dx, + model.m12, model.m22, 0, model.dy, + 0, 0, 1, 0, + model.m13, model.m23, 0, model.m33 + ) + } + + Rectangle { + id: nodeRect + visible: model.hasContent + property color baseColor: model.color + property real fillAlpha: model.fullyOpaque + ? 1.0 + : (model.isBackdrop ? 0.46 : 0.58) + x: model.localX + y: model.localY + width: model.localWidth + height: model.localHeight + color: "transparent" + radius: model.isBackdrop ? 12 : 7 + border.color: model.id === scene.selectedId + ? "#ffffff" : Qt.alpha(Qt.lighter(baseColor, 1.35), 0.95) + border.width: model.id === scene.selectedId ? 2 : 1 + gradient: Gradient { + GradientStop { + position: 0 + color: Qt.alpha(Qt.lighter(nodeRect.baseColor, 1.35), nodeRect.fillAlpha) + } + GradientStop { + position: 0.55 + color: Qt.alpha(nodeRect.baseColor, nodeRect.fillAlpha) + } + GradientStop { + position: 1 + color: Qt.alpha(Qt.darker(nodeRect.baseColor, 1.45), nodeRect.fillAlpha) + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: Math.min(30, parent.height) + radius: parent.radius + color: "#26000000" + } + Text { + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 8 + text: model.name + + (model.occluded ? " · 已遮挡" : (model.culled ? " · 已剔除" : "")) + color: "#ffffff" + font.pixelSize: 11 + font.weight: Font.DemiBold + style: Text.Outline + styleColor: "#70000000" + } + MouseArea { + property real lastCanvasX: 0 + property real lastCanvasY: 0 + property bool isDragging: false + property bool interactionStarted: false + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: function(mouse) { + if (mouse.button === Qt.LeftButton) { + scene.activateNode(model.id) + isDragging = true + interactionStarted = false + var p = mapToItem(canvas, mouse.x, mouse.y) + lastCanvasX = p.x + lastCanvasY = p.y + } else { + scene.selectedId = model.id + nodeMenu.popup(nodeRect, mouse.x, mouse.y) + } + } + onPositionChanged: function(mouse) { + if (!isDragging || !(mouse.buttons & Qt.LeftButton)) + return + var p = mapToItem(canvas, mouse.x, mouse.y) + var dx = p.x - lastCanvasX + var dy = p.y - lastCanvasY + if (dx === 0 && dy === 0) + return + if (!interactionStarted) { + interactionStarted = true + beginUserInteraction() + } + lastCanvasX = p.x + lastCanvasY = p.y + scene.moveSelectedBy(dx, dy) + } + onReleased: function(mouse) { + if (mouse.button === Qt.LeftButton) { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + onCanceled: { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + } + Rectangle { + id: clipRect + visible: model.type === "Clip" + x: model.localX + y: model.localY + width: model.localWidth + height: model.localHeight + color: Qt.alpha(model.color, 0.08) + border.color: model.id === scene.selectedId ? "#ffffff" : model.color + border.width: model.id === scene.selectedId ? 2 : 1 + radius: 2 + Text { + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 6 + text: model.name + color: "#d8f9ff" + font.pixelSize: 10 + font.weight: Font.DemiBold + style: Text.Outline + styleColor: "#70000000" + } + MouseArea { + property real lastCanvasX: 0 + property real lastCanvasY: 0 + property bool isDragging: false + property bool interactionStarted: false + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: function(mouse) { + if (mouse.button === Qt.LeftButton) { + scene.selectedId = model.id + isDragging = true + interactionStarted = false + var p = mapToItem(canvas, mouse.x, mouse.y) + lastCanvasX = p.x + lastCanvasY = p.y + } else { + scene.selectedId = model.id + nodeMenu.popup(clipRect, mouse.x, mouse.y) + } + } + onPositionChanged: function(mouse) { + if (!isDragging || !(mouse.buttons & Qt.LeftButton)) + return + var p = mapToItem(canvas, mouse.x, mouse.y) + var dx = p.x - lastCanvasX + var dy = p.y - lastCanvasY + if (dx === 0 && dy === 0) + return + if (!interactionStarted) { + interactionStarted = true + beginUserInteraction() + } + lastCanvasX = p.x + lastCanvasY = p.y + scene.moveSelectedBy(dx, dy) + } + onReleased: function(mouse) { + if (mouse.button === Qt.LeftButton) { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + onCanceled: { + isDragging = false + if (interactionStarted) { + interactionStarted = false + scene.finishSelectedMove() + endUserInteraction() + } + } + } + } + } + } + + Rectangle { + x: scene.viewportA.x; y: scene.viewportA.y + width: scene.viewportA.width; height: scene.viewportA.height + color: "transparent"; border.color: "#78526079"; border.width: 2 + } + Rectangle { + x: scene.viewportB.x; y: scene.viewportB.y + width: scene.viewportB.width; height: scene.viewportB.height + color: "transparent"; border.color: "#78526079"; border.width: 2 + } + Label { + x: scene.viewportA.x + 10; y: scene.viewportA.y + 8 + text: "输出 A"; color: win.mutedColor; font.pixelSize: 9; font.weight: Font.DemiBold + } + Label { + x: scene.viewportB.x + 10; y: scene.viewportB.y + 8 + text: "输出 B"; color: win.mutedColor; font.pixelSize: 9; font.weight: Font.DemiBold + } + + DamageOverlay { + anchors.fill: parent + frames: scene.damageFrames + historyDuration: win.damageHistoryDuration + refreshRate: win.sceneRefreshRate + } + + Connections { + target: scene + function onSelectedIdChanged() { Qt.callLater(win.revealSelectedNode) } + } + } + } + } + } + + Rectangle { + SplitView.preferredWidth: 300 + SplitView.minimumWidth: 270 + color: win.panelColor + border.color: win.borderColor + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 10 + + Label { + text: "属性检查器" + color: win.mutedColor + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + } + + RowLayout { + Layout.fillWidth: true + Label { + text: "损伤残留时间" + color: win.textColor + Layout.fillWidth: true + } + SpinBox { + id: damageDurationEditor + Layout.preferredWidth: 112 + from: 100 + to: 10000 + stepSize: 100 + value: win.damageHistoryDuration + editable: true + onValueModified: win.damageHistoryDuration = value + ToolTip.visible: hovered + ToolTip.text: "损伤帧保持可见的时间" + } + Label { + text: "ms" + color: win.mutedColor + font.pixelSize: 10 + } + } + + RowLayout { + Layout.fillWidth: true + Label { + text: "刷新率" + color: win.textColor + Layout.fillWidth: true + } + SpinBox { + id: refreshRateEditor + Layout.preferredWidth: 112 + from: 1 + to: 360 + stepSize: 1 + value: win.sceneRefreshRate + editable: true + onValueModified: win.sceneRefreshRate = value + ToolTip.visible: hovered + ToolTip.text: "拖动提交和损伤重绘的刷新率" + } + Label { + text: "Hz" + color: win.mutedColor + font.pixelSize: 10 + } + ToolButton { + text: "↺" + onClicked: win.sceneRefreshRate = win.detectedRefreshRate + ToolTip.visible: hovered + ToolTip.text: "恢复为当前屏幕刷新率" + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 62 + radius: 8 + color: win.raisedColor + border.color: win.borderColor + RowLayout { + anchors.fill: parent + anchors.margins: 10 + Rectangle { + width: 34; height: 34; radius: 9 + color: win.sel.type === "Geometry" ? "#596de8" + : win.sel.type === "Backdrop" ? "#0891b2" + : win.sel.type === "Transform" ? "#d28a2d" : "#556176" + Label { + anchors.centerIn: parent + text: win.sel.type ? win.sel.type.charAt(0) : "–" + color: "#ffffff" + font.bold: true + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + Label { + text: win.sel.name || "未选择节点" + color: win.textColor + font.pixelSize: 14 + font.weight: Font.DemiBold + elide: Text.ElideRight + Layout.fillWidth: true + } + Label { + text: win.sel.type ? (win.typeLabel(win.sel.type) + " · ID " + win.sel.id) + : "在节点树或画布中选择节点" + color: win.mutedColor + font.pixelSize: 10 + } + } + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + visible: !!win.sel.id + + Label { text: "常规"; color: win.mutedColor; font.pixelSize: 9; font.bold: true } + RowLayout { + Layout.fillWidth: true + Label { text: "固定名称"; color: win.mutedColor } + Label { + Layout.fillWidth: true + text: win.sel.name || "" + color: win.textColor + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + } + } + RowLayout { + Layout.fillWidth: true + Label { text: "可见"; color: win.textColor; Layout.fillWidth: true } + Switch { checked: !!win.sel.visible; onToggled: scene.setVisibleSelected(checked) } + } + RowLayout { + Layout.fillWidth: true + visible: !!win.sel.isGeometry + Label { text: "完全不透明"; color: win.textColor; Layout.fillWidth: true } + Switch { checked: !!win.sel.fullyOpaque; onToggled: scene.setFullyOpaqueSelected(checked) } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: win.borderColor } + Label { + visible: !!win.sel.isGeometry || !!win.sel.isClip + text: win.sel.isClip ? "裁剪区域" : "几何属性" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isGeometry || !!win.sel.isClip + Layout.fillWidth: true + columns: 2 + columnSpacing: 8 + rowSpacing: 8 + Label { text: "X"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.x); editable: true + onValueModified: scene.setRectSelected(value, win.num(win.sel.y), win.num(win.sel.w), win.num(win.sel.h)) + } + Label { text: "Y"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.y); editable: true + onValueModified: scene.setRectSelected(win.num(win.sel.x), value, win.num(win.sel.w), win.num(win.sel.h)) + } + Label { text: "宽度"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: 0; to: 2000 + value: win.num(win.sel.w); editable: true + onValueModified: scene.setRectSelected(win.num(win.sel.x), win.num(win.sel.y), value, win.num(win.sel.h)) + } + Label { text: "高度"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: 0; to: 2000 + value: win.num(win.sel.h); editable: true + onValueModified: scene.setRectSelected(win.num(win.sel.x), win.num(win.sel.y), win.num(win.sel.w), value) + } + } + + Label { + visible: !!win.sel.isTransform + text: "变换属性" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isTransform + Layout.fillWidth: true + columns: 2 + columnSpacing: 8 + rowSpacing: 8 + Label { text: "水平位移"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.tx); editable: true + onValueModified: scene.setTranslationSelected(value, win.num(win.sel.ty)) + } + Label { text: "垂直位移"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: -2000; to: 2000 + value: win.num(win.sel.ty); editable: true + onValueModified: scene.setTranslationSelected(win.num(win.sel.tx), value) + } + Label { text: "旋转轴"; color: win.mutedColor } + ComboBox { + id: rotationAxisCombo + Layout.fillWidth: true + model: [ + { text: "Z 轴 (平面旋转)", axis: 2 }, + { text: "X 轴 (水平翻转)", axis: 0 }, + { text: "Y 轴 (垂直翻转)", axis: 1 } + ] + textRole: "text" + valueRole: "axis" + currentIndex: win.num(win.sel.rotationAxis, 2) === 0 ? 1 + : (win.num(win.sel.rotationAxis, 2) === 1 ? 2 : 0) + onActivated: scene.setRotationSelected(rotationSpinBox.value, currentValue) + } + Label { text: "旋转角度"; color: win.mutedColor } + SpinBox { + id: rotationSpinBox + Layout.fillWidth: true + from: -360 + to: 360 + value: Math.round(win.num(win.sel.rotation)) + editable: true + onValueModified: scene.setRotationSelected(value, rotationAxisCombo.currentValue) + } + Label { text: "水平缩放"; color: win.mutedColor } + TextField { + Layout.fillWidth: true + text: win.num(win.sel.sx, 1).toFixed(3) + validator: DoubleValidator { bottom: 0.01; top: 20; decimals: 3 } + onEditingFinished: scene.setScaleSelected(Number(text), win.num(win.sel.sy, 1)) + } + Label { text: "垂直缩放"; color: win.mutedColor } + TextField { + Layout.fillWidth: true + text: win.num(win.sel.sy, 1).toFixed(3) + validator: DoubleValidator { bottom: 0.01; top: 20; decimals: 3 } + onEditingFinished: scene.setScaleSelected(win.num(win.sel.sx, 1), Number(text)) + } + } + Label { + visible: !!win.sel.isTransform + text: "矩阵" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isTransform + Layout.fillWidth: true + columns: 3 + columnSpacing: 8 + rowSpacing: 4 + Label { text: "m11"; color: win.mutedColor } + Label { text: win.num(win.sel.m11).toFixed(3); color: win.textColor } + Label { text: "m12 " + win.num(win.sel.m12).toFixed(3); color: win.textColor } + Label { text: "m21"; color: win.mutedColor } + Label { text: win.num(win.sel.m21).toFixed(3); color: win.textColor } + Label { text: "m22 " + win.num(win.sel.m22).toFixed(3); color: win.textColor } + Label { text: "dx"; color: win.mutedColor } + Label { text: win.num(win.sel.dx).toFixed(3); color: win.textColor } + Label { text: "dy " + win.num(win.sel.dy).toFixed(3); color: win.textColor } + + } + Label { + visible: !!win.sel.isBackdrop + text: "背景采样属性" + color: win.mutedColor + font.pixelSize: 9 + font.bold: true + } + GridLayout { + visible: !!win.sel.isBackdrop + Layout.fillWidth: true + columns: 2 + columnSpacing: 8 + rowSpacing: 8 + Label { text: "扩张范围"; color: win.mutedColor } + SpinBox { + Layout.fillWidth: true; from: 0; to: 80 + value: win.num(win.sel.expansion); editable: true + onValueModified: scene.setExpansionSelected(value) + } + Label { text: "裁剪扩张"; color: win.mutedColor } + Switch { + checked: win.sel.clipExpansion !== false + onToggled: scene.setClipExpansionSelected(checked) + } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: win.borderColor } + RowLayout { + Layout.fillWidth: true + Button { text: "上移"; enabled: moveUpAction.enabled; onClicked: moveUpAction.trigger() } + Button { text: "下移"; enabled: moveDownAction.enabled; onClicked: moveDownAction.trigger() } + Item { Layout.fillWidth: true } + Button { text: "标记损伤"; enabled: dirtyAction.enabled; onClicked: dirtyAction.trigger() } + } + Button { + Layout.fillWidth: true + text: "删除选中节点" + enabled: deleteAction.enabled + onClicked: deleteAction.trigger() + } + } + } + } + } + } + + Rectangle { + id: treeDragGhost + parent: win.contentItem + z: 99999 + visible: win.treeDragging + x: win.treeGhostX + y: win.treeGhostY + width: 230 + height: 38 + radius: 8 + color: "#f01c2a3f" + border.color: "#7399ff" + border.width: 1.5 + + Row { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 8 + Rectangle { + width: 20 + height: 20 + radius: 5 + anchors.verticalCenter: parent.verticalCenter + color: "#4e73df" + Label { + anchors.centerIn: parent + text: "↕" + color: "#ffffff" + font.pixelSize: 11 + font.bold: true + } + } + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: -1 + Label { + text: win.treeDragName || "节点" + color: "#ffffff" + font.pixelSize: 11 + font.bold: true + } + Label { + text: win.treeDropHint || "拖动中..." + color: "#8cd3ff" + font.pixelSize: 9 + } + } + } + } +} diff --git a/waylib/tests/manual/damagegraph-demo/main.cpp b/waylib/tests/manual/damagegraph-demo/main.cpp new file mode 100644 index 0000000000..9e7f23d581 --- /dev/null +++ b/waylib/tests/manual/damagegraph-demo/main.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QGuiApplication::setOrganizationName(QStringLiteral("Waylib")); + QGuiApplication::setApplicationName(QStringLiteral("damagegraph-demo")); + + QQmlApplicationEngine engine; + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreationFailed, + &app, [] { QCoreApplication::exit(1); }, + Qt::QueuedConnection); + engine.loadFromModule("DamageGraphDemo", "Main"); + return app.exec(); +} diff --git a/waylib/tests/unit_tests/CMakeLists.txt b/waylib/tests/unit_tests/CMakeLists.txt index c76abe2737..629154427d 100644 --- a/waylib/tests/unit_tests/CMakeLists.txt +++ b/waylib/tests/unit_tests/CMakeLists.txt @@ -1,5 +1,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) +add_subdirectory(test_wsgdamage) +add_subdirectory(test_wsgdamage_visual) +add_subdirectory(test_wdamagegraph) +add_subdirectory(test_wdamagegraph_demo) add_subdirectory(test_qsgrenderer_accessor) add_subdirectory(test_native_handles) add_subdirectory(test_native_lifecycle) diff --git a/waylib/tests/unit_tests/test_output_frame/main.cpp b/waylib/tests/unit_tests/test_output_frame/main.cpp index 63426f0e45..c43b0b2805 100644 --- a/waylib/tests/unit_tests/test_output_frame/main.cpp +++ b/waylib/tests/unit_tests/test_output_frame/main.cpp @@ -78,14 +78,6 @@ private Q_SLOTS: QCOMPARE(rendered.size(), 1); QCOMPARE(qvariant_cast>>(rendered.takeFirst().first()), QList>{outputs[1]}); - - prepare(); - window.render(); - QCOMPARE(rendered.size(), 1); - const auto committed = qvariant_cast>>(rendered.takeFirst().first()); - QCOMPARE(committed.size(), 2); - QVERIFY(committed.contains(outputs[0])); - QVERIFY(committed.contains(outputs[1])); } }; diff --git a/waylib/tests/unit_tests/test_wdamagegraph/CMakeLists.txt b/waylib/tests/unit_tests/test_wdamagegraph/CMakeLists.txt new file mode 100644 index 0000000000..cead0fdf8a --- /dev/null +++ b/waylib/tests/unit_tests/test_wdamagegraph/CMakeLists.txt @@ -0,0 +1,36 @@ +# Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +find_package(Qt6 REQUIRED COMPONENTS Test Gui) + +add_executable(test_wdamagegraph + tst_wdamagegraph.cpp +) + +target_link_libraries(test_wdamagegraph + PRIVATE + Waylib::WaylibServer + Qt::Test + Qt::Gui +) + +add_test(NAME test_wdamagegraph COMMAND test_wdamagegraph) + +set_tests_properties(test_wdamagegraph PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 60 +) + +add_executable(bench_wdamagegraph + bench_wdamagegraph.cpp +) + +target_link_libraries(bench_wdamagegraph + PRIVATE + Waylib::WaylibServer + Qt::Test + Qt::Gui +) + +# Wall-clock benchmark: kept buildable/runnable by hand, but not part of +# default CTest (timing assertions are environment-sensitive). diff --git a/waylib/tests/unit_tests/test_wdamagegraph/bench_wdamagegraph.cpp b/waylib/tests/unit_tests/test_wdamagegraph/bench_wdamagegraph.cpp new file mode 100644 index 0000000000..866d94ce08 --- /dev/null +++ b/waylib/tests/unit_tests/test_wdamagegraph/bench_wdamagegraph.cpp @@ -0,0 +1,460 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgdamagetracker_p.h" + +#include +#include +#include +#include + +#include +#include +WAYLIB_SERVER_USE_NAMESPACE + +class WSGDamageGraphBench : public QObject +{ + Q_OBJECT + +private slots: + void benchmarkSuite(); +}; + +struct BenchmarkResult +{ + QString category; + QString scenario; + int nodeCount; + int viewportCount; + double avgTimeUs; + double maxFps; + double budget60Hz; + double budget144Hz; + double budget240Hz; +}; + +static std::vector s_results; + +static WSGDamageNode *buildRealisticTree( + int totalNodes, + std::vector *outGeos = nullptr, + std::vector *outTransforms = nullptr, + std::vector *outBackdrops = nullptr) +{ + auto *root = new WSGDamageNode(); + root->setName(QStringLiteral("root")); + + int created = 0; + int containerId = 0; + + while (created < totalNodes) { + auto *layer = new WSGDamageTransformNode(); + layer->setName(QStringLiteral("container_%1").arg(++containerId)); + layer->setTranslation((created % 10) * 80.0, (created / 10) * 60.0); + root->appendChild(layer); + if (outTransforms) + outTransforms->push_back(layer); + created++; + + const int childrenInLayer = std::min(15, totalNodes - created); + for (int i = 0; i < childrenInLayer; ++i) { + if (i == 3 && outBackdrops && created + 1 < totalNodes) { + // A real backdrop node (not a flagged geometry node) so the + // tracker accumulates behind-damage and dilates it by the + // 16px recopy expansion, like a blur blitter. + auto *bg = new WSGDamageBackdropNode(); + bg->setName(QStringLiteral("backdrop_%1").arg(created)); + bg->setBoundingRect(QRectF(10 * i, 10 * i, 200, 150)); + bg->setRecopyExpansion(QMargins(16, 16, 16, 16)); + layer->appendChild(bg); + outBackdrops->push_back(bg); + created++; + continue; + } + + auto *geo = new WSGDamageGeometryNode(); + geo->setName(QStringLiteral("item_%1").arg(created)); + geo->setBoundingRect(QRectF(15 * i, 10 * i, 120, 80)); + geo->setFullyOpaque(i % 2 == 0); + layer->appendChild(geo); + if (outGeos) + outGeos->push_back(geo); + created++; + if (created >= totalNodes) + break; + } + } + + return root; +} + +static int getIterations(int nodes) +{ + if (nodes <= 50) + return 600; + if (nodes <= 200) + return 300; + if (nodes <= 1000) + return 80; + return 20; +} + +static BenchmarkResult runBenchmark( + const QString &category, + const QString &scenario, + int nodeCount, + int viewportCount, + int iterations, + const std::function &, + const std::vector &, + const std::vector &, + int)> &prepareIteration) +{ + std::vector geos; + std::vector transforms; + std::vector backdrops; + std::unique_ptr root( + buildRealisticTree(nodeCount, &geos, &transforms, &backdrops)); + WSGDamageTracker tracker(root.get()); + std::vector> viewports(viewportCount); + for (int i = 0; i < viewportCount; ++i) { + viewports[i] = std::make_shared(); + tracker.addViewport(viewports[i]); + } + + auto cycle = [&] { + // The first commit includes backdrop sampling and dilation; subsequent + // output consumers reuse the same final scene damage. + tracker.commit(); + for (int i = 0; i < viewportCount; ++i) { + (void)viewports[i]->damageRegion(); + viewports[i]->finishFrame(); + } + // iteration measures the same work and timing stays deterministic. + for (auto *bd : backdrops) { + if (auto *backdrop = bd->toBackdrop()) + backdrop->consumeRecopy(backdrop->pendingRecopy(viewports[0].get()), viewports[0].get()); + } + }; + cycle(); // warm-up & initial commit + + QElapsedTimer timer; + timer.start(); + + for (int i = 0; i < iterations; ++i) { + prepareIteration(tracker, root.get(), geos, transforms, backdrops, i); + cycle(); + } + + const qint64 elapsedNs = timer.nsecsElapsed(); + const double avgUs = double(elapsedNs) / (double(iterations) * 1000.0); + const double maxFps = 1000000.0 / std::max(0.001, avgUs); + + BenchmarkResult res; + res.category = category; + res.scenario = scenario; + res.nodeCount = nodeCount; + res.viewportCount = viewportCount; + res.avgTimeUs = avgUs; + res.maxFps = maxFps; + res.budget60Hz = (avgUs / 16666.67) * 100.0; + res.budget144Hz = (avgUs / 6944.44) * 100.0; + res.budget240Hz = (avgUs / 4166.67) * 100.0; + return res; +} + +enum class LargeSubtreeMode +{ + ExactMove, + ExactContent, + RendererAggregateMove, + RendererAggregateContent +}; + +static BenchmarkResult runLargeSubtreeBenchmark(const QString &scenario, + int nodeCount, + LargeSubtreeMode mode) +{ + auto root = std::make_unique(); + auto *group = new WSGDamageTransformNode; + root->appendChild(group); + + constexpr int columns = 100; + constexpr int cellSize = 8; + const int rows = (nodeCount + columns - 1) / columns; + const QRect aggregateBounds(0, 0, columns * cellSize, rows * cellSize); + const bool aggregated = mode == LargeSubtreeMode::RendererAggregateMove + || mode == LargeSubtreeMode::RendererAggregateContent; + + std::vector geos; + WSGDamageGeometryNode *proxy = nullptr; + if (aggregated) { + proxy = new WSGDamageGeometryNode; + proxy->setBoundingRect(aggregateBounds); + group->appendChild(proxy); + } else { + geos.reserve(nodeCount); + for (int i = 0; i < nodeCount; ++i) { + auto *geo = new WSGDamageGeometryNode; + geo->setBoundingRect(QRectF((i % columns) * cellSize, (i / columns) * cellSize, 6, 6)); + group->appendChild(geo); + geos.push_back(geo); + } + } + + WSGDamageTracker tracker(root.get()); + auto viewport = std::make_shared(); + tracker.addViewport(viewport); + auto cycle = [&] { + tracker.commit(); + (void)viewport->damageRegion(); + viewport->finishFrame(); + }; + + constexpr int iterations = 20; + QElapsedTimer timer; + timer.start(); + for (int i = 0; i < iterations; ++i) { + if (mode == LargeSubtreeMode::ExactContent) { + geos[i % geos.size()]->markContentDirty(QRect(1, 1, 2, 2)); + } else if (mode == LargeSubtreeMode::RendererAggregateContent) { + proxy->markContentDirty(aggregateBounds); + } else { + group->setTranslation((i & 1) ? 0 : 20, 0); + } + cycle(); + } + + const double avgUs = double(timer.nsecsElapsed()) / (double(iterations) * 1000.0); + return { + QStringLiteral("万节点 Renderer 子树聚合"), + scenario, + nodeCount, + 1, + avgUs, + 1000000.0 / std::max(0.001, avgUs), + (avgUs / 16666.67) * 100.0, + (avgUs / 6944.44) * 100.0, + (avgUs / 4166.67) * 100.0, + }; +} + +void WSGDamageGraphBench::benchmarkSuite() +{ + qInfo() << "===================================================================================" + "======="; + qInfo() << " GUI Damage WSGDamageTracker Performance Benchmark Suite " + " "; + qInfo() << "===================================================================================" + "======="; + + // 1. Idle Frame Fast-Path + for (int nodes : { 50, 200, 1000, 5000 }) { + for (int vps : { 1, 2, 4 }) { + s_results.push_back(runBenchmark(QStringLiteral("空闲帧 (无脏位快速路径)"), + QStringLiteral("Idle commit (0 changes)"), + nodes, + vps, + getIterations(nodes) * 2, + [](WSGDamageTracker &, + WSGDamageNode *, + const std::vector &, + const std::vector &, + const std::vector &, + int) { + // no-op + })); + } + } + + // 2. Single Leaf Content Damage (e.g., text cursor blink or small widget repaint) + for (int nodes : { 50, 200, 1000, 5000 }) { + for (int vps : { 1, 2, 4 }) { + s_results.push_back(runBenchmark(QStringLiteral("局部内容损伤 (16x16 px)"), + QStringLiteral("Single leaf node markContentDirty"), + nodes, + vps, + getIterations(nodes), + [](WSGDamageTracker &, + WSGDamageNode *, + const std::vector &geos, + const std::vector &, + const std::vector &, + int iter) { + if (!geos.empty()) { + auto *target = geos[iter % geos.size()]; + target->markContentDirty( + QRect(10, 10, 16, 16)); + } + })); + } + } + + // 3. WSGDamageNode Geometry Translation (Window drag / widget motion) + for (int nodes : { 50, 200, 1000, 5000 }) { + for (int vps : { 1, 2, 4 }) { + s_results.push_back(runBenchmark( + QStringLiteral("单节点几何移动 (平移)"), + QStringLiteral("Single node setBoundingRect translate"), + nodes, + vps, + getIterations(nodes), + [](WSGDamageTracker &, + WSGDamageNode *, + const std::vector &geos, + const std::vector &, + const std::vector &, + int iter) { + if (!geos.empty()) { + auto *target = geos[iter % geos.size()]; + target->setBoundingRect( + QRectF(15.0 + (iter % 50), 10.0 + (iter % 30), 120.0, 80.0)); + } + })); + } + } + + // 4. Subtree Transform Rotation + for (int nodes : { 50, 200, 1000 }) { + for (int vps : { 1, 2 }) { + s_results.push_back( + runBenchmark(QStringLiteral("子树层级旋转 (WSGDamageTransformNode)"), + QStringLiteral("Subtree rotation matrix change"), + nodes, + vps, + getIterations(nodes), + [](WSGDamageTracker &, + WSGDamageNode *, + const std::vector &, + const std::vector &transforms, + const std::vector &, + int iter) { + if (!transforms.empty()) { + auto *target = transforms[iter % transforms.size()]; + QTransform matrix; + matrix.translate(200, 200); + matrix.rotate(double(iter % 360), Qt::ZAxis); + target->setMatrix(matrix); + } + })); + } + } + + // 5. Backdrop Induced Damage Dilation + for (int nodes : { 50, 200, 1000 }) { + for (int vps : { 1, 2 }) { + s_results.push_back(runBenchmark(QStringLiteral("背景采样扩散 (Backdrop 16px)"), + QStringLiteral("Backdrop induced damage dilation"), + nodes, + vps, + getIterations(nodes), + [](WSGDamageTracker &, + WSGDamageNode *, + const std::vector &geos, + const std::vector &, + const std::vector &, + int) { + if (!geos.empty()) { + geos[0]->markContentDirty( + QRect(10, 10, 40, 40)); + } + })); + } + } + + // 6. High-Density Scattered Damage (10% of all nodes dirty simultaneously) + for (int nodes : { 50, 200, 1000 }) { + for (int vps : { 1, 2 }) { + s_results.push_back( + runBenchmark(QStringLiteral("高密度多节点损坏 (10% 节点)"), + QStringLiteral("10% nodes concurrently dirty"), + nodes, + vps, + getIterations(nodes), + [](WSGDamageTracker &, + WSGDamageNode *, + const std::vector &geos, + const std::vector &, + const std::vector &, + int iter) { + const size_t dirtyCount = std::max(1, geos.size() / 10); + for (size_t k = 0; k < dirtyCount; ++k) { + const size_t idx = (iter + k * 7) % geos.size(); + geos[idx]->markContentDirty(QRect(5, 5, 20, 20)); + } + })); + } + } + + // 7. Hidden subtree receives changes that must be deferred until reveal. + for (int nodes : { 1000, 5000 }) { + s_results.push_back(runBenchmark(QStringLiteral("隐藏子树延迟更新"), + QStringLiteral("Hidden root with one dirty descendant"), + nodes, + 1, + getIterations(nodes), + [](WSGDamageTracker &, + WSGDamageNode *root, + const std::vector &geos, + const std::vector &, + const std::vector &, + int iter) { + root->setVisible(false); + if (!geos.empty()) + geos[iter % geos.size()]->markContentDirty( + QRect(10, 10, 16, 16)); + })); + } + + // 8. Renderer replaces a very large QSG subtree with one ordinary damage proxy. + constexpr int largeSubtreeNodes = 10000; + s_results.push_back(runLargeSubtreeBenchmark(QStringLiteral("Exact large-subtree move"), + largeSubtreeNodes, + LargeSubtreeMode::ExactMove)); + s_results.push_back(runLargeSubtreeBenchmark(QStringLiteral("Renderer aggregate move"), + largeSubtreeNodes, + LargeSubtreeMode::RendererAggregateMove)); + s_results.push_back(runLargeSubtreeBenchmark(QStringLiteral("Exact descendant content"), + largeSubtreeNodes, + LargeSubtreeMode::ExactContent)); + s_results.push_back( + runLargeSubtreeBenchmark(QStringLiteral("Renderer aggregate descendant content"), + largeSubtreeNodes, + LargeSubtreeMode::RendererAggregateContent)); + + // Print Formatted Markdown Table + printf("\n\n### 性能基准测试结果汇总 (Performance Benchmark Summary)\n\n"); + printf( + "| 场景分类 (Scenario) | 用例 (Case) | 节点数 (Nodes) | Viewport数 | 平均单帧耗时 (Avg Time) | " + "最大理论吞吐量 (Throughput) | 60Hz 帧预算占比 | 144Hz 帧预算占比 | 240Hz 帧预算占比 |\n"); + printf("|---|---|---|---|---|---|---|---|---|\n"); + for (const auto &r : s_results) { + printf("| %s | %s | %d | %d VP | **%.2f µs** | %.0f FPS | %.3f%% | %.3f%% | %.3f%% |\n", + r.category.toUtf8().constData(), + r.scenario.toUtf8().constData(), + r.nodeCount, + r.viewportCount, + r.avgTimeUs, + r.maxFps, + r.budget60Hz, + r.budget144Hz, + r.budget240Hz); + } + for (const auto &r : s_results) { + const double limitUs = r.nodeCount >= 10000 ? 250000.0 + : r.nodeCount >= 5000 ? 50000.0 + : 16666.0; + QVERIFY2(r.avgTimeUs < limitUs, + qPrintable(QStringLiteral("%1 nodes=%2 vps=%3 avg=%4 us exceeds %5 us budget") + .arg(r.scenario) + .arg(r.nodeCount) + .arg(r.viewportCount) + .arg(r.avgTimeUs, 0, 'f', 2) + .arg(limitUs, 0, 'f', 0))); + } +} + +QTEST_MAIN(WSGDamageGraphBench) +#include "bench_wdamagegraph.moc" diff --git a/waylib/tests/unit_tests/test_wdamagegraph/tst_wdamagegraph.cpp b/waylib/tests/unit_tests/test_wdamagegraph/tst_wdamagegraph.cpp new file mode 100644 index 0000000000..d212412702 --- /dev/null +++ b/waylib/tests/unit_tests/test_wdamagegraph/tst_wdamagegraph.cpp @@ -0,0 +1,2724 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgdamagetracker_p.h" + +WAYLIB_SERVER_USE_NAMESPACE + +#include +#include +#include +#include +#include +#include + +static QRegion toQRegion(const pixman_region32_t *region) +{ + QRegion result; + if (!region) + return result; + int count = 0; + const pixman_box32_t *boxes = pixman_region32_rectangles(region, &count); + for (int i = 0; i < count; ++i) { + result += + QRect(boxes[i].x1, boxes[i].y1, boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); + } + return result; +} + +static QRegion toQRegion(const WPixmanRegion ®ion) +{ + return toQRegion(region.native()); +} + +static QRegion toQRegion(const QRegion ®ion) +{ + return region; +} + +static QString regionStr(const QRegion &r) +{ + if (r.isEmpty()) + return QStringLiteral(""); + QString s; + for (const QRect &rect : r) { + if (!s.isEmpty()) + s += QLatin1Char(' '); + s += QStringLiteral("[%1,%2 %3x%4]") + .arg(rect.x()) + .arg(rect.y()) + .arg(rect.width()) + .arg(rect.height()); + } + return s; +} + +static QMatrix4x4 sceneToBuffer(const WSGViewport &v, const QSize &buf, qreal dpr = 1.0) +{ + QTransform t; + const auto outputSize = QSizeF(buf) / dpr; + const QRectF sourceRect = v.sourceRect(); + const QRectF targetRect = v.targetRect(); + if (sourceRect.isValid()) + t.translate(-sourceRect.x(), -sourceRect.y()); + if (targetRect.isValid()) + t.translate(targetRect.x(), targetRect.y()); + if (sourceRect.isValid()) { + t.scale(outputSize.width() / sourceRect.width(), + outputSize.height() / sourceRect.height()); + } + if (targetRect.isValid()) { + t.scale(targetRect.width() / outputSize.width(), + targetRect.height() / outputSize.height()); + } + QMatrix4x4 scale; + scale.scale(dpr, dpr); + return scale * (QMatrix4x4(t) * v.renderMatrix()); +} + +#define COMPARE_REGION(actual, expected) \ + do { \ + const QRegion _gdtActual = toQRegion(actual); \ + const QRegion _gdtExpected = toQRegion(expected); \ + QVERIFY2((_gdtActual ^ _gdtExpected).isEmpty(), \ + qPrintable(QStringLiteral("actual=%1 expected=%2") \ + .arg(regionStr(_gdtActual), regionStr(_gdtExpected)))); \ + } while (0) + +class TestTracker : public WSGDamageTracker +{ +public: + TestTracker(WSGDamageNode *root = nullptr) + : WSGDamageTracker(root) + , viewport(std::make_shared()) + { + addViewport(viewport); + } + + QRegion commit() + { + WSGDamageTracker::commit(); + const QRegion result = viewport->damageRegion().region.toQRegion(); + viewport->finishFrame(); + return result; + } + + std::shared_ptr viewport; +}; + +class WSGDamageGraphTest : public QObject +{ + Q_OBJECT + +private slots: + void regionBooleanParity(); + void regionCopyMoveAndNativeHandle(); + void regionTransformMapping(); + void regionMatrixMapping(); + void regionTransformParityWithQRegion(); + void emptyCommit(); + void addGeometry(); + void idempotentCommit(); + void settleWithoutViewportDamage(); + void fullViewportSkipsAccumulation(); + void removeGeometry(); + void addRemoveSameFrame(); + void moveGeometryRect(); + void resizeGeometry(); + void partialContentDamage(); + void contentDamageClippedToBounds(); + void clipIntersectsGeometryBounds(); + void clipScrollKeepsUnclippedLocalBounds(); + void clipResizeDamagesCrescent(); + void clipDoesNotOccludeOutside(); + void nestedClips(); + void emptyClipHidesContent(); + void clippedSubtreeDefersChanges(); + void contentDirtyClippedToClipNode(); + void clipValidAndVisibleFollowClip(); + void clipBackdropVisibleStaysInsideClip(); + void nonRectangularClipClipsBoundsWithoutOpaque(); + void roundedClipInnerOmitsCorners(); + void waylandOpaqueRegionPunchesBehind(); + void hideShow(); + void hiddenSubtreeDefersChanges(); + void aggregateProxyIsOrdinaryGeometry(); + void hideShowSameFrame(); + void hideThenRemoveWithoutCommit(); + void translateTransform(); + void nestedTransforms(); + void scaleTransform(); + void rotate90(); + void rotate45Conservative(); + void siblingOcclusionFullyCovered(); + void siblingOcclusionPartial(); + void transparentDoesNotOcclude(); + void parentGeometryBehindChildren(); + void occludedRegionReported(); + void insertBetweenSiblings(); + void reparent(); + void raiseLowersZOrder(); + void fullyOpaqueFlag(); + void opaqueRegionUpdate(); + void contentLocalDirtyFollowsBox(); + void multipleDirtyRegions(); + void destroySubtree(); + void basicNodeGrouping(); + void parentHideHidesChildren(); + void fractionalTranslationOverestimates(); + void moveOpaqueRevealsBehind(); + void movingFrontDoesNotDamageCleanBackWhole(); + void contentDamageUnderOpaqueSibling(); + void partiallyOccludedMoveAvoidsOpaqueFront(); + void fullyOccludedMoveProducesNoDamage(); + void zeroSizeGeometry(); + void negativeCoordinates(); + void deepTree(); + void setMatrixIdentityNoDamage(); + void geometryChildrenZOrder(); + void nonAxisAlignedDropsOpaque(); + void matrix4x4(); + void firstCommitAppearing(); + void contentDirtyOnNewNode(); + void removeUncommittedNode(); + void siblingOrderPaint(); + void nodeAccessorsUseSceneCoordinates(); + void nodeHasContentProperty(); + void worldVisiblePartialOpaqueRegion(); + void worldVisibleTwoOpaqueFronts(); + void worldVisibleLocalizedSubtreeComposition(); + void worldVisibleRaiseReveals(); + void worldVisibleHasContentOff(); + void worldVisibleParentHideClearsChild(); + void worldVisibleUnderTranslation(); + void worldVisibleNestedAndScale(); + void worldVisibleOverlappingOpaqueNodes(); + void worldVisibleNonAxisAlignedFront(); + void worldVisibleAfterGeometryMove(); + void worldVisibleZeroSizeEmpty(); + void worldVisibleRemoveFront(); + void worldVisibleIdempotentSecondCommit(); + void worldVisibleGroupHasNone(); + void worldVisiblePartialChildCover(); + void worldVisibleDeepNested(); + void worldVisibleInsertAndReparent(); + void worldVisibleHideShow(); + void worldVisibleNegativeCoords(); + void worldVisibleRotate90(); + void backdropBehindDamageMatchesAccumulator(); + void backdropUncoveredRegion(); + void backdropKeepsCoveredBehindDamage(); + void backdropPunchesFrontOpaque(); + void backdropRecopyClippedToSampleBounds(); + void stackedBackdropsVisibleRegions(); + void stackedBackdropsRecaptureOverlap(); + void stackedBackdropsExclusiveDirtySkipsFront(); + void stackedBackdropsIndependentCopySources(); + void backdropSeesRemovedSiblingHole(); + void backgroundDamageRecopySurvivesRepeatedCommit(); + void multiOutputDisjointSceneMapping(); + void viewportDirtyUsesVisibleSourceRegion(); + void highDpiScaledOutputMapping(); + void rotatedOutputMapping(); + void singularOutputMapping(); + void viewportRegistrationLifecycle(); +}; + +void WSGDamageGraphTest::regionBooleanParity() +{ + WPixmanRegion a; + a += QRect(-10, -5, 20, 10); + a += QRect(20, 5, 8, 12); + WPixmanRegion b; + b += QRect(0, -8, 24, 20); + b += QRect(25, 10, 8, 8); + + const QRegion qa = QRegion(-10, -5, 20, 10) + QRegion(20, 5, 8, 12); + const QRegion qb = QRegion(0, -8, 24, 20) + QRegion(25, 10, 8, 8); + + COMPARE_REGION(a + b, qa + qb); + COMPARE_REGION(a & b, qa & qb); + COMPARE_REGION(a - b, qa - qb); + QCOMPARE(a.rectCount(), qa.rectCount()); + QCOMPARE(a.boundingRect(), qa.boundingRect()); +} + +void WSGDamageGraphTest::regionCopyMoveAndNativeHandle() +{ + WPixmanRegion original; + original += QRect(-20, 3, 7, 9); + original += QRect(4, 8, 11, 6); + + WPixmanRegion copy(original); + COMPARE_REGION(copy, original); + + WPixmanRegion moved(std::move(copy)); + COMPARE_REGION(moved, original); + COMPARE_REGION(copy, QRegion()); + QVERIFY(pixman_region32_equal(moved.native(), original.native())); + + WPixmanRegion assigned; + assigned = moved; + COMPARE_REGION(assigned, original); + + WPixmanRegion moveAssigned; + moveAssigned = std::move(assigned); + COMPARE_REGION(moveAssigned, original); + COMPARE_REGION(assigned, QRegion()); +} + +void WSGDamageGraphTest::regionTransformMapping() +{ + WPixmanRegion region; + region += QRect(0, 0, 4, 3); + region += QRect(8, 2, 2, 2); + + COMPARE_REGION(region.mappedOuter(QTransform()), region); + COMPARE_REGION(region.mappedInner(QTransform()), region); + + const QTransform translation = QTransform::fromTranslate(3, -2); + const QRegion translated = QRegion(3, -2, 4, 3) + QRegion(11, 0, 2, 2); + COMPARE_REGION(region.mappedOuter(translation), translated); + COMPARE_REGION(region.mappedInner(translation), translated); + + const QTransform fractional = QTransform::fromTranslate(0.25, -0.5); + COMPARE_REGION(region.mappedOuter(fractional), QRegion(0, -1, 5, 4) + QRegion(8, 1, 3, 3)); + COMPARE_REGION(region.mappedInner(fractional), QRegion(1, 0, 3, 2) + QRegion(9, 2, 1, 1)); + + QTransform mirrored; + mirrored.translate(20, 10); + mirrored.scale(-2, 3); + COMPARE_REGION(region.mappedOuter(mirrored), QRegion(12, 10, 8, 9) + QRegion(0, 16, 4, 6)); + COMPARE_REGION(region.mappedInner(mirrored), QRegion(12, 10, 8, 9) + QRegion(0, 16, 4, 6)); + + QTransform quarterTurn; + quarterTurn.rotate(90); + COMPARE_REGION(region.mappedOuter(quarterTurn), QRegion(-3, 0, 3, 4) + QRegion(-4, 8, 2, 2)); + COMPARE_REGION(region.mappedInner(quarterTurn), QRegion(-3, 0, 3, 4) + QRegion(-4, 8, 2, 2)); + + QTransform arbitrary; + arbitrary.rotate(33); + const WPixmanRegion arbitraryOuter = region.mappedOuter(arbitrary); + COMPARE_REGION(arbitraryOuter, + QRegion(mapOuter(arbitrary, QRectF(0, 0, 4, 3))) + + QRegion(mapOuter(arbitrary, QRectF(8, 2, 2, 2)))); + QVERIFY(region.mappedInner(arbitrary).isEmpty()); +} + +void WSGDamageGraphTest::regionMatrixMapping() +{ + WPixmanRegion region; + region += QRect(-3, 2, 7, 5); + region += QRect(10, -4, 3, 6); + + QMatrix4x4 matrix; + matrix.translate(3.25f, -1.5f); + matrix.scale(1.5f, 0.5f); + + const QTransform transform = matrix.toTransform(); + COMPARE_REGION(region.mappedOuter(matrix), region.mappedOuter(transform)); + COMPARE_REGION(region.mappedInner(matrix), region.mappedInner(transform)); + + matrix.setToIdentity(); + matrix.rotate(90, 0, 0, 1); + COMPARE_REGION(region.mappedOuter(matrix), region.mappedOuter(matrix.toTransform())); + COMPARE_REGION(region.mappedInner(matrix), region.mappedInner(matrix.toTransform())); +} + +void WSGDamageGraphTest::regionTransformParityWithQRegion() +{ + WPixmanRegion region; + region += QRect(-5, -7, 6, 9); + region += QRect(0, 0, 4, 3); + region += QRect(8, 2, 2, 2); + const QRegion qtRegion = region.toQRegion(); + COMPARE_REGION(region, qtRegion); + + QList exactTransforms{ + QTransform(), + QTransform::fromTranslate(3, -2), + QTransform::fromScale(2, 3), + QTransform::fromScale(-2, 1), + }; + QTransform quarterTurn; + quarterTurn.rotate(90); + exactTransforms.append(quarterTurn); + QTransform halfTurn; + halfTurn.rotate(180); + exactTransforms.append(halfTurn); + for (const QTransform &t : exactTransforms) { + const QRegion expected = t.map(qtRegion); + COMPARE_REGION(region.mappedOuter(t), expected); + COMPARE_REGION(region.mappedInner(t), expected); + } + + // Fractional/rotated transforms: Qt rounds to nearest; damage is an + // over-approximation and opaque an under-approximation of it. + QList fuzzyTransforms{ + QTransform::fromTranslate(0.25, -0.5), + QTransform::fromScale(1.5, 0.5), + quarterTurn * QTransform::fromScale(1.5, 2.0), + }; + QTransform arbitrary; + arbitrary.rotate(33); + for (const QTransform &t : fuzzyTransforms) { + const QRegion qtMapped = t.map(qtRegion); + QVERIFY2((qtMapped - toQRegion(region.mappedOuter(t))).isEmpty(), + qPrintable( + QStringLiteral("damage %1 misses Qt %2") + .arg(regionStr(region.mappedOuter(t).toQRegion()), regionStr(qtMapped)))); + QVERIFY2((toQRegion(region.mappedInner(t)) - qtMapped).isEmpty(), + qPrintable( + QStringLiteral("opaque %1 escapes Qt %2") + .arg(regionStr(region.mappedInner(t).toQRegion()), regionStr(qtMapped)))); + } + + // QMatrix4x4 has no map(QRegion); it must match its 2D projection and + // cover the mapped bounding rect. + QMatrix4x4 matrix; + matrix.translate(3.25f, -1.5f); + matrix.rotate(90, 0, 0, 1); + matrix.scale(1.5f, 0.5f); + COMPARE_REGION(region.mappedOuter(matrix), region.mappedOuter(matrix.toTransform())); + COMPARE_REGION(region.mappedInner(matrix), region.mappedInner(matrix.toTransform())); + QVERIFY((qtRegion.intersects(matrix.mapRect(region.boundingRect())))); + QVERIFY( + (toQRegion(region.mappedOuter(matrix)) - matrix.mapRect(region.boundingRect())).isEmpty()); +} + +void WSGDamageGraphTest::emptyCommit() +{ + WSGDamageNode root; + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::addGeometry() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(10, 20, 30, 40)); + root.appendChild(g); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(10, 20, 30, 40)); + QCOMPARE(g->worldBounds(), QRect(10, 20, 30, 40)); +} + +void WSGDamageGraphTest::idempotentCommit() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 50, 50)); + root.appendChild(g); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 50, 50)); + COMPARE_REGION(tracker.commit(), QRegion()); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::settleWithoutViewportDamage() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(10, 20, 30, 40)); + root.appendChild(g); + + WSGDamageTracker tracker(&root); + QVERIFY(g->isDirty()); + tracker.commit(); + QVERIFY(!g->isDirty()); + QCOMPARE(g->worldBounds(), QRect(10, 20, 30, 40)); +} + +void WSGDamageGraphTest::fullViewportSkipsAccumulation() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(10, 20, 30, 40)); + root.appendChild(g); + + auto viewport = std::make_shared(); + viewport->markFull(); + WSGDamageTracker tracker(&root); + tracker.addViewport(viewport); + tracker.commit(); + + QVERIFY(viewport->isFull()); + QVERIFY(viewport->damageRegion().region.isEmpty()); + QVERIFY(viewport->isFull()); + + // A skipped full viewport still establishes the scene baseline at frame end. + viewport->finishFrame(); + tracker.commit(); + QVERIFY(!viewport->isFull()); + COMPARE_REGION(viewport->damageRegion().region, QRegion()); + viewport->finishFrame(); + + g->setBoundingRect(QRectF(20, 20, 30, 40)); + tracker.commit(); + COMPARE_REGION(viewport->damageRegion().region, QRegion(10, 20, 40, 40)); +} + +void WSGDamageGraphTest::removeGeometry() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(5, 5, 10, 10)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + root.removeChild(g); + COMPARE_REGION(tracker.commit(), QRegion(5, 5, 10, 10)); + delete g; +} + +void WSGDamageGraphTest::addRemoveSameFrame() +{ + WSGDamageNode root; + TestTracker tracker(&root); + tracker.commit(); + + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 40, 40)); + root.appendChild(g); + root.removeChild(g); + delete g; + + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::moveGeometryRect() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->setBoundingRect(QRectF(50, 50, 20, 20)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 20, 20) + QRegion(50, 50, 20, 20)); +} + +void WSGDamageGraphTest::resizeGeometry() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 10, 10)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->setBoundingRect(QRectF(0, 0, 30, 10)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 30, 10)); +} + +void WSGDamageGraphTest::partialContentDamage() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 100, 100)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 100, 100)); + + g->markContentDirty(QRect(10, 15, 4, 5)); + COMPARE_REGION(tracker.commit(), QRegion(10, 15, 4, 5)); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 100, 100)); +} + +void WSGDamageGraphTest::contentDamageClippedToBounds() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->markContentDirty(QRect(-10, -10, 15, 15)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 5, 5)); +} + +void WSGDamageGraphTest::clipIntersectsGeometryBounds() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 40, 30)); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 100, 100)); + clip->appendChild(g); + root.appendChild(clip); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 40, 30)); + QCOMPARE(g->boundingRect(), QRectF(0, 0, 100, 100)); + QCOMPARE(g->worldBounds(), QRect(0, 0, 40, 30)); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 40, 30)); + COMPARE_REGION(g->worldVisibleRegion(), QRegion(0, 0, 40, 30)); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::clipScrollKeepsUnclippedLocalBounds() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 100, 50)); + auto *tr = new WSGDamageTransformNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + tr->appendChild(g); + clip->appendChild(tr); + root.appendChild(clip); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 20, 20)); + + tr->setTranslation(0, 30); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 20, 20) + QRegion(0, 30, 20, 20)); + QCOMPARE(g->boundingRect(), QRectF(0, 0, 20, 20)); + QCOMPARE(g->worldBounds(), QRect(0, 30, 20, 20)); + + tr->setTranslation(0, 80); + COMPARE_REGION(tracker.commit(), QRegion(0, 30, 20, 20)); + QVERIFY(g->worldBounds().isEmpty()); + COMPARE_REGION(g->worldValidRegion(), QRegion()); + COMPARE_REGION(g->worldVisibleRegion(), QRegion()); +} + +void WSGDamageGraphTest::clipResizeDamagesCrescent() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 100, 100)); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 200, 200)); + clip->appendChild(g); + root.appendChild(clip); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 100, 100)); + + clip->setClipRect(QRectF(0, 0, 50, 50)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 100, 100)); + QCOMPARE(g->worldBounds(), QRect(0, 0, 50, 50)); +} + +void WSGDamageGraphTest::clipDoesNotOccludeOutside() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 100)); + back->setFullyOpaque(true); + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 50, 50)); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 100, 100)); + front->setFullyOpaque(true); + clip->appendChild(front); + root.appendChild(back); + root.appendChild(clip); + + TestTracker tracker(&root); + tracker.commit(); + QCOMPARE(front->worldBounds(), QRect(0, 0, 50, 50)); + COMPARE_REGION(front->worldOpaqueRegion(), QRegion(0, 0, 50, 50)); + COMPARE_REGION(front->worldValidRegion(), QRegion(0, 0, 50, 50)); + COMPARE_REGION(front->worldVisibleRegion(), QRegion(0, 0, 50, 50)); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 100, 100) - QRegion(0, 0, 50, 50)); + COMPARE_REGION(back->worldVisibleRegion(), QRegion(0, 0, 100, 100)); + + back->markContentDirty(QRect(0, 0, 100, 100)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 100, 100) - QRegion(0, 0, 50, 50)); +} + +void WSGDamageGraphTest::nestedClips() +{ + WSGDamageNode root; + auto *outer = new WSGDamageClipNode; + outer->setClipRect(QRectF(0, 0, 100, 100)); + auto *inner = new WSGDamageClipNode; + inner->setClipRect(QRectF(50, 50, 100, 100)); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 200, 200)); + inner->appendChild(g); + outer->appendChild(inner); + root.appendChild(outer); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(50, 50, 50, 50)); + QCOMPARE(g->worldBounds(), QRect(50, 50, 50, 50)); + COMPARE_REGION(g->worldValidRegion(), QRegion(50, 50, 50, 50)); + COMPARE_REGION(g->worldVisibleRegion(), QRegion(50, 50, 50, 50)); +} + +void WSGDamageGraphTest::emptyClipHidesContent() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 40, 40)); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 80, 80)); + clip->appendChild(g); + root.appendChild(clip); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 40, 40)); + + clip->setClipRect(QRectF()); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 40, 40)); + QVERIFY(g->worldBounds().isEmpty()); + COMPARE_REGION(g->worldValidRegion(), QRegion()); + COMPARE_REGION(g->worldVisibleRegion(), QRegion()); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::clippedSubtreeDefersChanges() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 40, 40)); + auto *group = new WSGDamageNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(80, 0, 20, 20)); + group->appendChild(g); + clip->appendChild(group); + root.appendChild(clip); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion()); + QVERIFY(g->worldBounds().isEmpty()); + + g->markContentDirty(QRect(0, 0, 10, 10)); + COMPARE_REGION(tracker.commit(), QRegion()); + QVERIFY(g->isDirty()); + + g->setBoundingRect(QRectF(10, 10, 20, 20)); + COMPARE_REGION(tracker.commit(), QRegion(10, 10, 20, 20)); + QCOMPARE(g->worldBounds(), QRect(10, 10, 20, 20)); +} + +void WSGDamageGraphTest::contentDirtyClippedToClipNode() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(10, 10, 20, 20)); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 100, 100)); + clip->appendChild(g); + root.appendChild(clip); + + TestTracker tracker(&root); + tracker.commit(); + + g->markContentDirty(QRect(0, 0, 100, 100)); + COMPARE_REGION(tracker.commit(), QRegion(10, 10, 20, 20)); +} + +void WSGDamageGraphTest::clipValidAndVisibleFollowClip() +{ + WSGDamageNode root; + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(10, 20, 30, 40)); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 200, 200)); + g->setFullyOpaque(true); + clip->appendChild(g); + root.appendChild(clip); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(10, 20, 30, 40)); + COMPARE_REGION(g->worldVisibleRegion(), QRegion(10, 20, 30, 40)); + + clip->setClipRect(QRectF(10, 20, 10, 10)); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(10, 20, 10, 10)); + COMPARE_REGION(g->worldVisibleRegion(), QRegion(10, 20, 10, 10)); +} + +void WSGDamageGraphTest::clipBackdropVisibleStaysInsideClip() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 100)); + back->setFullyOpaque(true); + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(20, 20, 40, 40)); + auto *glass = new WSGDamageBackdropNode; + glass->setBoundingRect(QRectF(0, 0, 100, 100)); + clip->appendChild(glass); + root.appendChild(back); + root.appendChild(clip); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(glass->worldValidRegion(), QRegion(20, 20, 40, 40)); + COMPARE_REGION(glass->worldVisibleRegion(), QRegion(20, 20, 40, 40)); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 100, 100)); + COMPARE_REGION(back->worldVisibleRegion(), QRegion(0, 0, 100, 100) - QRegion(20, 20, 40, 40)); +} + +void WSGDamageGraphTest::nonRectangularClipClipsBoundsWithoutOpaque() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 100)); + back->setFullyOpaque(true); + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 50, 50)); + clip->setIsRectangular(false); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 100, 100)); + front->setFullyOpaque(true); + clip->appendChild(front); + root.appendChild(back); + root.appendChild(clip); + + TestTracker tracker(&root); + tracker.commit(); + QCOMPARE(front->worldBounds(), QRect(0, 0, 50, 50)); + COMPARE_REGION(front->worldOpaqueRegion(), QRegion()); + COMPARE_REGION(front->worldValidRegion(), QRegion(0, 0, 50, 50)); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 100, 100)); + + back->markContentDirty(QRect(0, 0, 100, 100)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 100, 100)); + + clip->setIsRectangular(true); + tracker.commit(); + COMPARE_REGION(front->worldOpaqueRegion(), QRegion(0, 0, 50, 50)); + back->markContentDirty(QRect(0, 0, 100, 100)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 100, 100) - QRegion(0, 0, 50, 50)); +} + +void WSGDamageGraphTest::roundedClipInnerOmitsCorners() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 100)); + back->setFullyOpaque(true); + auto *clip = new WSGDamageClipNode; + clip->setClipRect(QRectF(0, 0, 50, 50)); + clip->setIsRectangular(false); + clip->setRadius(10); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 100, 100)); + front->setFullyOpaque(true); + clip->appendChild(front); + root.appendChild(back); + root.appendChild(clip); + + TestTracker tracker(&root); + tracker.commit(); + QCOMPARE(front->worldBounds(), QRect(0, 0, 50, 50)); + const QRegion inner = QRegion(0, 0, 50, 50) - QRect(0, 0, 10, 10) - QRect(40, 0, 10, 10) + - QRect(0, 40, 10, 10) - QRect(40, 40, 10, 10); + COMPARE_REGION(front->worldOpaqueRegion(), inner); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 100, 100) - inner); + + back->markContentDirty(QRect(0, 0, 100, 100)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 100, 100) - inner); +} + +void WSGDamageGraphTest::waylandOpaqueRegionPunchesBehind() +{ + // Client opaque_region is (0,0,80,50) on a 100x50 surface placed at (10,20). + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 200, 200)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(10, 20, 100, 50)); + front->setOpaqueRegion(QRegion(0, 0, 80, 50)); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(front->worldOpaqueRegion(), QRegion(10, 20, 80, 50)); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 200, 200) - QRegion(10, 20, 80, 50)); + + back->markContentDirty(QRect(0, 0, 200, 200)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 200, 200) - QRegion(10, 20, 80, 50)); +} + +void WSGDamageGraphTest::hideShow() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(8, 8, 16, 16)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->setVisible(false); + COMPARE_REGION(tracker.commit(), QRegion(8, 8, 16, 16)); + + COMPARE_REGION(tracker.commit(), QRegion()); + + g->setVisible(true); + COMPARE_REGION(tracker.commit(), QRegion(8, 8, 16, 16)); +} + +void WSGDamageGraphTest::hiddenSubtreeDefersChanges() +{ + WSGDamageNode root; + auto *group = new WSGDamageNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + group->appendChild(g); + root.appendChild(group); + + TestTracker tracker(&root); + tracker.commit(); + group->setVisible(false); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 20, 20)); + + g->setBoundingRect(QRectF(100, 0, 20, 20)); + COMPARE_REGION(tracker.commit(), QRegion()); + QVERIFY(g->isDirty()); + + group->setVisible(true); + COMPARE_REGION(tracker.commit(), QRegion(100, 0, 20, 20)); + QCOMPARE(g->worldBounds(), QRect(100, 0, 20, 20)); +} + +void WSGDamageGraphTest::aggregateProxyIsOrdinaryGeometry() +{ + WSGDamageNode root; + auto *group = new WSGDamageTransformNode; + group->setTranslation(20, 30); + auto *proxy = new WSGDamageGeometryNode; + proxy->setBoundingRect(QRectF(0, 0, 100, 80)); + group->appendChild(proxy); + root.appendChild(group); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(20, 30, 100, 80)); + QCOMPARE(proxy->worldBounds(), QRect(20, 30, 100, 80)); + + proxy->markContentDirty(QRect(0, 0, 100, 80)); + COMPARE_REGION(tracker.commit(), QRegion(20, 30, 100, 80)); + + group->setTranslation(200, 30); + COMPARE_REGION(tracker.commit(), QRegion(20, 30, 100, 80) + QRegion(200, 30, 100, 80)); + QCOMPARE(proxy->worldBounds(), QRect(200, 30, 100, 80)); +} + +void WSGDamageGraphTest::hideShowSameFrame() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 12, 12)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->setVisible(false); + g->setVisible(true); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::hideThenRemoveWithoutCommit() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(1, 2, 3, 4)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->setVisible(false); + root.removeChild(g); + COMPARE_REGION(tracker.commit(), QRegion(1, 2, 3, 4)); + delete g; +} + +void WSGDamageGraphTest::translateTransform() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + tr->setTranslation(10, 20); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 5, 6)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(10, 20, 5, 6)); + + tr->setTranslation(40, 20); + COMPARE_REGION(tracker.commit(), QRegion(10, 20, 5, 6) + QRegion(40, 20, 5, 6)); +} + +void WSGDamageGraphTest::nestedTransforms() +{ + WSGDamageNode root; + auto *a = new WSGDamageTransformNode; + a->setTranslation(10, 0); + auto *b = new WSGDamageTransformNode; + b->setTranslation(0, 20); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(1, 2, 3, 4)); + b->appendChild(g); + a->appendChild(b); + root.appendChild(a); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(11, 22, 3, 4)); +} + +void WSGDamageGraphTest::scaleTransform() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + tr->setScale(2, 3); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(1, 1, 4, 2)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(2, 3, 8, 6)); +} + +void WSGDamageGraphTest::rotate90() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QTransform t; + t.translate(50, 50); + t.rotate(90); + t.translate(-50, -50); + tr->setMatrix(t); + + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(40, 40, 20, 10)); + g->setFullyOpaque(true); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(g->worldBounds())); + QVERIFY(isAxisAligned(g->worldTransform().toTransform())); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion(g->worldBounds())); +} + +void WSGDamageGraphTest::rotate45Conservative() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QTransform t; + t.translate(100, 100); + t.rotate(45); + t.translate(-50, -50); + tr->setMatrix(t); + + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 100, 100)); + g->setFullyOpaque(true); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + const QRegion d = tracker.commit(); + const QRect aabb = g->worldBounds(); + COMPARE_REGION(d, QRegion(aabb)); + // Must not claim opacity under a non-axis-aligned transform. + COMPARE_REGION(g->worldOpaqueRegion(), QRegion()); +} + +void WSGDamageGraphTest::siblingOcclusionFullyCovered() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setName(QStringLiteral("back")); + back->setBoundingRect(QRectF(0, 0, 100, 100)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setName(QStringLiteral("front")); + front->setBoundingRect(QRectF(0, 0, 100, 100)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + back->markContentDirty(QRect(10, 10, 8, 8)); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::siblingOcclusionPartial() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 100)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(25, 25, 50, 50)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + back->markContentDirty(QRect(0, 0, 10, 10)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 10, 10)); + + back->markContentDirty(QRect(40, 40, 10, 10)); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::transparentDoesNotOcclude() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 40, 40)); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 40, 40)); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + back->markContentDirty(QRect(2, 2, 4, 4)); + COMPARE_REGION(tracker.commit(), QRegion(2, 2, 4, 4)); +} + +void WSGDamageGraphTest::parentGeometryBehindChildren() +{ + WSGDamageNode root; + auto *parent = new WSGDamageGeometryNode; + parent->setBoundingRect(QRectF(0, 0, 80, 80)); + parent->setFullyOpaque(true); + auto *child = new WSGDamageGeometryNode; + child->setBoundingRect(QRectF(0, 0, 80, 80)); + child->setFullyOpaque(true); + parent->appendChild(child); + root.appendChild(parent); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 80, 80)); + parent->markContentDirty(QRect(0, 0, 80, 80)); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::occludedRegionReported() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 30, 30)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(10, 0, 30, 30)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + COMPARE_REGION(front->worldValidRegion(), QRegion(10, 0, 30, 30)); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 10, 30)); +} + +void WSGDamageGraphTest::insertBetweenSiblings() +{ + WSGDamageNode root; + auto *a = new WSGDamageGeometryNode; + a->setBoundingRect(QRectF(0, 0, 10, 10)); + auto *c = new WSGDamageGeometryNode; + c->setBoundingRect(QRectF(20, 0, 10, 10)); + root.appendChild(a); + root.appendChild(c); + + TestTracker tracker(&root); + tracker.commit(); + + auto *b = new WSGDamageGeometryNode; + b->setBoundingRect(QRectF(10, 0, 10, 10)); + root.insertChildBefore(b, c); + QCOMPARE(a->nextSibling(), b); + QCOMPARE(b->nextSibling(), c); + COMPARE_REGION(tracker.commit(), QRegion(10, 0, 10, 10)); +} + +void WSGDamageGraphTest::reparent() +{ + WSGDamageNode root; + auto *t1 = new WSGDamageTransformNode; + t1->setTranslation(0, 0); + auto *t2 = new WSGDamageTransformNode; + t2->setTranslation(80, 0); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 10, 10)); + t1->appendChild(g); + root.appendChild(t1); + root.appendChild(t2); + + TestTracker tracker(&root); + tracker.commit(); + + t2->appendChild(g); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 10, 10) + QRegion(80, 0, 10, 10)); +} + +void WSGDamageGraphTest::raiseLowersZOrder() +{ + WSGDamageNode root; + auto *a = new WSGDamageGeometryNode; + a->setBoundingRect(QRectF(0, 0, 40, 40)); + a->setFullyOpaque(true); + auto *b = new WSGDamageGeometryNode; + b->setBoundingRect(QRectF(0, 0, 40, 40)); + b->setFullyOpaque(true); + root.appendChild(a); + root.appendChild(b); + + TestTracker tracker(&root); + tracker.commit(); + + root.appendChild(a); // raise a + QCOMPARE(static_cast(root.lastChild()), static_cast(a)); + QCOMPARE(static_cast(root.firstChild()), static_cast(b)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 40, 40)); + + b->markContentDirty(QRect(5, 5, 10, 10)); + COMPARE_REGION(tracker.commit(), QRegion()); + + a->markContentDirty(QRect(5, 5, 10, 10)); + COMPARE_REGION(tracker.commit(), QRegion(5, 5, 10, 10)); +} + +void WSGDamageGraphTest::fullyOpaqueFlag() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 16, 16)); + g->setFullyOpaque(true); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion(0, 0, 16, 16)); + + g->setBoundingRect(QRectF(80, 80, 32, 16)); + tracker.commit(); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion(80, 80, 32, 16)); + COMPARE_REGION(g->opaqueRegion(), QRegion(0, 0, 32, 16)); +} + +void WSGDamageGraphTest::opaqueRegionUpdate() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 40, 40)); + const WPixmanRegion opaqueStrip(0, 0, 10, 40); + g->setOpaqueRegion(opaqueStrip.native()); + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 40, 40)); + root.appendChild(back); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion(0, 0, 10, 40)); + COMPARE_REGION(back->worldValidRegion(), QRegion(10, 0, 30, 40)); + + g->setBoundingRect(QRectF(80, 80, 40, 40)); + tracker.commit(); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion(80, 80, 10, 40)); + COMPARE_REGION(g->opaqueRegion(), QRegion(0, 0, 10, 40)); +} + +void WSGDamageGraphTest::contentLocalDirtyFollowsBox() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(100, 110, 240, 150)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->markContentDirty(QRect(12, 24, 26, 20)); + COMPARE_REGION(tracker.commit(), QRegion(112, 134, 26, 20)); + + g->setBoundingRect(QRectF(180, 150, 240, 150)); + tracker.commit(); + g->markContentDirty(QRect(12, 24, 26, 20)); + COMPARE_REGION(tracker.commit(), QRegion(192, 174, 26, 20)); +} + +void WSGDamageGraphTest::multipleDirtyRegions() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 200, 200)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + + g->markContentDirty(QRect(0, 0, 2, 2)); + g->markContentDirty(QRect(50, 50, 3, 3)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 2, 2) + QRegion(50, 50, 3, 3)); +} + +void WSGDamageGraphTest::destroySubtree() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(3, 4, 5, 6)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + tracker.commit(); + + delete tr; + COMPARE_REGION(tracker.commit(), QRegion(3, 4, 5, 6)); +} + +void WSGDamageGraphTest::basicNodeGrouping() +{ + WSGDamageNode root; + auto *group = new WSGDamageNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(7, 8, 9, 10)); + group->appendChild(g); + root.appendChild(group); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(7, 8, 9, 10)); +} + +void WSGDamageGraphTest::parentHideHidesChildren() +{ + WSGDamageNode root; + auto *group = new WSGDamageNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + group->appendChild(g); + root.appendChild(group); + + TestTracker tracker(&root); + tracker.commit(); + + group->setVisible(false); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 20, 20)); +} + +void WSGDamageGraphTest::fractionalTranslationOverestimates() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QTransform t; + t.translate(0.4, 0.4); + tr->setMatrix(t); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 10, 10)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 11, 11)); +} + +void WSGDamageGraphTest::moveOpaqueRevealsBehind() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 80, 80)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 80, 80)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + front->setBoundingRect(QRectF(100, 0, 80, 80)); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 80, 80) + QRegion(100, 0, 80, 80)); +} + +void WSGDamageGraphTest::movingFrontDoesNotDamageCleanBackWhole() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 100)); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(25, 0, 50, 100)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + front->setBoundingRect(QRectF(35, 0, 50, 100)); + const QRegion damage = tracker.commit(); + COMPARE_REGION(WSGDamageNodeTestAccess::ownDamage(back), QRegion()); + COMPARE_REGION(damage, QRegion(25, 0, 60, 100)); +} + +void WSGDamageGraphTest::contentDamageUnderOpaqueSibling() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 60, 60)); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(20, 20, 20, 20)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + back->markContentDirty(QRect(22, 22, 4, 4)); + // Fully covered by the opaque front sibling: no scene damage escapes. + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::fullyOccludedMoveProducesNoDamage() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(20, 20, 20, 20)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 100, 100)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + back->setBoundingRect(QRectF(60, 20, 20, 20)); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::partiallyOccludedMoveAvoidsOpaqueFront() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(80, 80, 260, 200)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(160, 140, 220, 180)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + back->setBoundingRect(QRectF(100, 100, 260, 200)); + const QRegion expected = + (QRegion(80, 80, 260, 200) + QRegion(100, 100, 260, 200)) - QRegion(front->worldBounds()); + COMPARE_REGION(tracker.commit(), expected); +} + +void WSGDamageGraphTest::zeroSizeGeometry() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(10, 10, 0, 10)); + root.appendChild(g); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::negativeCoordinates() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(-30, -10, 20, 20)); + root.appendChild(g); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(-30, -10, 20, 20)); +} + +void WSGDamageGraphTest::deepTree() +{ + WSGDamageNode root; + WSGDamageNode *cur = &root; + for (int i = 0; i < 32; ++i) { + auto *t = new WSGDamageTransformNode; + t->setTranslation(1, 0); + cur->appendChild(t); + cur = t; + } + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 2, 2)); + cur->appendChild(g); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(32, 0, 2, 2)); +} + +void WSGDamageGraphTest::setMatrixIdentityNoDamage() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + tr->setTranslation(0, 0); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 8, 8)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + tracker.commit(); + + tr->setTranslation(0, 0); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::geometryChildrenZOrder() +{ + WSGDamageNode root; + auto *parent = new WSGDamageGeometryNode; + parent->setBoundingRect(QRectF(0, 0, 50, 50)); + auto *child = new WSGDamageGeometryNode; + child->setBoundingRect(QRectF(10, 10, 10, 10)); + child->setFullyOpaque(true); + parent->appendChild(child); + root.appendChild(parent); + + TestTracker tracker(&root); + tracker.commit(); + + parent->markContentDirty(QRect(10, 10, 10, 10)); + // The opaque child covers the dirty spot: the parent emits nothing. + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::nonAxisAlignedDropsOpaque() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QTransform t; + t.rotate(33); + tr->setMatrix(t); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 80, 80)); + front->setFullyOpaque(true); + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 80, 80)); + tr->appendChild(back); + tr->appendChild(front); + root.appendChild(tr); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(front->worldOpaqueRegion(), QRegion()); +} + +void WSGDamageGraphTest::matrix4x4() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QMatrix4x4 m; + m.translate(15, 25); + tr->setMatrix(m); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 4, 4)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(15, 25, 4, 4)); +} + +void WSGDamageGraphTest::siblingOrderPaint() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 40, 40)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 40, 40)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(front->worldValidRegion(), QRegion(0, 0, 40, 40)); + COMPARE_REGION(back->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::firstCommitAppearing() +{ + WSGDamageNode root; + auto *g1 = new WSGDamageGeometryNode; + g1->setBoundingRect(QRectF(0, 0, 5, 5)); + auto *g2 = new WSGDamageGeometryNode; + g2->setBoundingRect(QRectF(10, 0, 5, 5)); + root.appendChild(g1); + root.appendChild(g2); + + TestTracker tracker(&root); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 5, 5) + QRegion(10, 0, 5, 5)); +} + +void WSGDamageGraphTest::contentDirtyOnNewNode() +{ + WSGDamageNode root; + TestTracker tracker(&root); + tracker.commit(); + + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + g->markContentDirty(QRect(1, 1, 1, 1)); + root.appendChild(g); + COMPARE_REGION(tracker.commit(), QRegion(0, 0, 20, 20)); +} + +void WSGDamageGraphTest::removeUncommittedNode() +{ + WSGDamageNode root; + auto *keep = new WSGDamageGeometryNode; + keep->setBoundingRect(QRectF(0, 0, 5, 5)); + root.appendChild(keep); + + TestTracker tracker(&root); + tracker.commit(); + + auto *tmp = new WSGDamageGeometryNode; + tmp->setBoundingRect(QRectF(100, 100, 5, 5)); + root.appendChild(tmp); + root.removeChild(tmp); + delete tmp; + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::nodeAccessorsUseSceneCoordinates() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 10, 10)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 10, 10)); + QCOMPARE(g->worldBounds(), QRect(0, 0, 10, 10)); +} + +void WSGDamageGraphTest::nodeHasContentProperty() +{ + WSGDamageNode root; + auto *container = new WSGDamageGeometryNode; + container->setBoundingRect(QRectF(0, 0, 300, 300)); + container->setHasContent(false); // Acts as a structural container without drawing own pixels + + auto *child = new WSGDamageGeometryNode; + child->setBoundingRect(QRectF(50, 50, 100, 100)); + container->appendChild(child); + root.appendChild(container); + + TestTracker tracker(&root); + // First commit: only child generates damage, container has no own damage + const QRegion damage1 = tracker.commit(); + COMPARE_REGION(damage1, QRegion(50, 50, 100, 100)); + COMPARE_REGION(WSGDamageNodeTestAccess::ownDamage(container), QRegion()); + QCOMPARE(container->worldBounds(), QRect()); + COMPARE_REGION(container->worldOpaqueRegion(), QRegion()); + COMPARE_REGION(container->worldValidRegion(), QRegion()); + COMPARE_REGION(child->worldValidRegion(), QRegion(50, 50, 100, 100)); + + container->setBoundingRect(QRectF(0, 0, 400, 400)); + const QRegion damage2 = tracker.commit(); + COMPARE_REGION(damage2, QRegion()); + COMPARE_REGION(container->worldValidRegion(), QRegion()); + + container->setHasContent(true); + QVERIFY(container->hasContent()); + container->setBoundingRect(QRectF(0, 0, 500, 500)); + const QRegion damage3 = tracker.commit(); + COMPARE_REGION(damage3, QRegion(0, 0, 500, 500)); + COMPARE_REGION(container->worldValidRegion(), QRegion(0, 0, 500, 500)); +} + +void WSGDamageGraphTest::worldVisiblePartialOpaqueRegion() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 40, 40)); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 40, 40)); + const WPixmanRegion opaqueStrip(0, 0, 10, 40); + front->setOpaqueRegion(opaqueStrip.native()); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(front->worldValidRegion(), QRegion(0, 0, 40, 40)); + COMPARE_REGION(back->worldValidRegion(), QRegion(10, 0, 30, 40)); +} + +void WSGDamageGraphTest::worldVisibleTwoOpaqueFronts() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 100, 40)); + back->setFullyOpaque(true); + auto *left = new WSGDamageGeometryNode; + left->setBoundingRect(QRectF(0, 0, 20, 40)); + left->setFullyOpaque(true); + auto *right = new WSGDamageGeometryNode; + right->setBoundingRect(QRectF(80, 0, 20, 40)); + right->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(left); + root.appendChild(right); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(right->worldValidRegion(), QRegion(80, 0, 20, 40)); + COMPARE_REGION(left->worldValidRegion(), QRegion(0, 0, 20, 40)); + COMPARE_REGION(back->worldValidRegion(), QRegion(20, 0, 60, 40)); +} + +void WSGDamageGraphTest::worldVisibleLocalizedSubtreeComposition() +{ + WSGDamageNode root; + auto *probe = new WSGDamageGeometryNode; + probe->setBoundingRect(QRectF(0, 0, 400, 40)); + + auto *group = new WSGDamageNode; + auto *groupBack = new WSGDamageGeometryNode; + groupBack->setBoundingRect(QRectF(0, 0, 400, 40)); + const WPixmanRegion bottomOpaque(0, 20, 400, 20); + groupBack->setOpaqueRegion(bottomOpaque.native()); + auto *glass = new WSGDamageBackdropNode; + const QRect glassBounds(100, 0, 20, 20); + glass->setBoundingRect(glassBounds); + glass->setNeedsBackdrop(true); + group->appendChild(groupBack); + group->appendChild(glass); + + root.appendChild(probe); + root.appendChild(group); + + QRegion frontOpaque; + for (int i = 0; i < 17; ++i) { + const QRect rect(i * 20, 0, 10, 10); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(rect); + front->setFullyOpaque(true); + root.appendChild(front); + frontOpaque += rect; + } + + TestTracker tracker(&root); + tracker.commit(); + + const QRegion bounds(0, 0, 400, 40); + const QRegion effectiveFront = frontOpaque - glassBounds; + COMPARE_REGION(groupBack->worldValidRegion(), bounds - effectiveFront); + COMPARE_REGION(groupBack->worldVisibleRegion(), bounds - glassBounds); + COMPARE_REGION(probe->worldValidRegion(), bounds - effectiveFront - QRegion(0, 20, 400, 20)); + COMPARE_REGION(probe->worldVisibleRegion(), bounds - glassBounds); +} + +void WSGDamageGraphTest::worldVisibleRaiseReveals() +{ + WSGDamageNode root; + auto *a = new WSGDamageGeometryNode; + a->setBoundingRect(QRectF(0, 0, 40, 40)); + a->setFullyOpaque(true); + auto *b = new WSGDamageGeometryNode; + b->setBoundingRect(QRectF(0, 0, 40, 40)); + b->setFullyOpaque(true); + root.appendChild(a); + root.appendChild(b); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(b->worldValidRegion(), QRegion(0, 0, 40, 40)); + COMPARE_REGION(a->worldValidRegion(), QRegion()); + + root.appendChild(a); + tracker.commit(); + COMPARE_REGION(a->worldValidRegion(), QRegion(0, 0, 40, 40)); + COMPARE_REGION(b->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::worldVisibleHasContentOff() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 30, 30)); + g->setFullyOpaque(true); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 30, 30)); + + g->setHasContent(false); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion()); + QCOMPARE(g->worldBounds(), QRect()); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion()); +} + +void WSGDamageGraphTest::worldVisibleParentHideClearsChild() +{ + WSGDamageNode root; + auto *group = new WSGDamageNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + group->appendChild(g); + root.appendChild(group); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 20, 20)); + COMPARE_REGION(group->worldValidRegion(), QRegion()); + + group->setVisible(false); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion()); + COMPARE_REGION(group->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::worldVisibleUnderTranslation() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + tr->setTranslation(10, 20); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 5, 6)); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(10, 20, 5, 6)); + + tr->setTranslation(40, 20); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(40, 20, 5, 6)); +} + +void WSGDamageGraphTest::worldVisibleNestedAndScale() +{ + WSGDamageNode root; + auto *a = new WSGDamageTransformNode; + a->setTranslation(10, 0); + auto *b = new WSGDamageTransformNode; + b->setTranslation(0, 20); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(1, 2, 3, 4)); + b->appendChild(g); + a->appendChild(b); + root.appendChild(a); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(11, 22, 3, 4)); + + WSGDamageNode root2; + auto *tr = new WSGDamageTransformNode; + tr->setScale(2, 3); + auto *s = new WSGDamageGeometryNode; + s->setBoundingRect(QRectF(1, 1, 4, 2)); + tr->appendChild(s); + root2.appendChild(tr); + TestTracker tracker2(&root2); + tracker2.commit(); + COMPARE_REGION(s->worldValidRegion(), QRegion(2, 3, 8, 6)); +} + +void WSGDamageGraphTest::worldVisibleOverlappingOpaqueNodes() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(160, 140, 220, 180)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(80, 80, 260, 200)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + + COMPARE_REGION(front->worldValidRegion(), QRegion(80, 80, 260, 200)); + COMPARE_REGION(back->worldValidRegion(), + QRegion(160, 140, 220, 180) - QRegion(80, 80, 260, 200)); +} + +void WSGDamageGraphTest::worldVisibleNonAxisAlignedFront() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QTransform t; + t.rotate(33); + tr->setMatrix(t); + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 80, 80)); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 80, 80)); + front->setFullyOpaque(true); + tr->appendChild(back); + tr->appendChild(front); + root.appendChild(tr); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(front->worldOpaqueRegion(), QRegion()); + COMPARE_REGION(front->worldValidRegion(), QRegion(front->worldBounds())); + COMPARE_REGION(back->worldValidRegion(), QRegion(back->worldBounds())); +} + +void WSGDamageGraphTest::worldVisibleAfterGeometryMove() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 20, 20)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(0, 0, 20, 20)); + + g->setBoundingRect(QRectF(50, 50, 20, 20)); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(50, 50, 20, 20)); +} + +void WSGDamageGraphTest::worldVisibleZeroSizeEmpty() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(10, 10, 0, 10)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::worldVisibleRemoveFront() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 40, 40)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(0, 0, 40, 40)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(back->worldValidRegion(), QRegion()); + + root.removeChild(front); + delete front; + tracker.commit(); + COMPARE_REGION(back->worldValidRegion(), QRegion(0, 0, 40, 40)); +} + +void WSGDamageGraphTest::worldVisibleIdempotentSecondCommit() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 30, 30)); + back->setFullyOpaque(true); + auto *front = new WSGDamageGeometryNode; + front->setBoundingRect(QRectF(10, 0, 30, 30)); + front->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(front); + + TestTracker tracker(&root); + tracker.commit(); + const QRegion firstFront = toQRegion(front->worldValidRegion()); + const QRegion firstBack = toQRegion(back->worldValidRegion()); + tracker.commit(); + COMPARE_REGION(front->worldValidRegion(), firstFront); + COMPARE_REGION(back->worldValidRegion(), firstBack); +} + +void WSGDamageGraphTest::worldVisibleGroupHasNone() +{ + WSGDamageNode root; + auto *group = new WSGDamageNode; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(7, 8, 9, 10)); + group->appendChild(g); + root.appendChild(group); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(7, 8, 9, 10)); + COMPARE_REGION(group->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::worldVisiblePartialChildCover() +{ + WSGDamageNode root; + auto *parent = new WSGDamageGeometryNode; + parent->setBoundingRect(QRectF(0, 0, 50, 50)); + parent->setFullyOpaque(true); + auto *child = new WSGDamageGeometryNode; + child->setBoundingRect(QRectF(10, 10, 10, 10)); + child->setFullyOpaque(true); + parent->appendChild(child); + root.appendChild(parent); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(child->worldValidRegion(), QRegion(10, 10, 10, 10)); + COMPARE_REGION(parent->worldValidRegion(), QRegion(0, 0, 50, 50) - QRegion(10, 10, 10, 10)); +} + +void WSGDamageGraphTest::worldVisibleDeepNested() +{ + WSGDamageNode root; + WSGDamageNode *cur = &root; + for (int i = 0; i < 32; ++i) { + auto *t = new WSGDamageTransformNode; + t->setTranslation(1, 0); + cur->appendChild(t); + cur = t; + } + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(0, 0, 2, 2)); + cur->appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(32, 0, 2, 2)); +} + +void WSGDamageGraphTest::worldVisibleInsertAndReparent() +{ + WSGDamageNode root; + auto *a = new WSGDamageGeometryNode; + a->setBoundingRect(QRectF(0, 0, 30, 30)); + a->setFullyOpaque(true); + auto *c = new WSGDamageGeometryNode; + c->setBoundingRect(QRectF(0, 0, 30, 30)); + c->setFullyOpaque(true); + root.appendChild(a); + root.appendChild(c); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(c->worldValidRegion(), QRegion(0, 0, 30, 30)); + COMPARE_REGION(a->worldValidRegion(), QRegion()); + + auto *b = new WSGDamageGeometryNode; + b->setBoundingRect(QRectF(10, 0, 10, 30)); + b->setFullyOpaque(true); + root.insertChildBefore(b, c); + tracker.commit(); + COMPARE_REGION(c->worldValidRegion(), QRegion(0, 0, 30, 30)); + COMPARE_REGION(b->worldValidRegion(), QRegion()); + COMPARE_REGION(a->worldValidRegion(), QRegion()); + + auto *t2 = new WSGDamageTransformNode; + t2->setTranslation(80, 0); + root.appendChild(t2); + t2->appendChild(b); + tracker.commit(); + COMPARE_REGION(b->worldValidRegion(), QRegion(90, 0, 10, 30)); + COMPARE_REGION(c->worldValidRegion(), QRegion(0, 0, 30, 30)); + COMPARE_REGION(a->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::worldVisibleHideShow() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(8, 8, 16, 16)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(8, 8, 16, 16)); + + g->setVisible(false); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion()); + + g->setVisible(true); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(8, 8, 16, 16)); +} + +void WSGDamageGraphTest::worldVisibleNegativeCoords() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(-30, -10, 20, 20)); + root.appendChild(g); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(g->worldValidRegion(), QRegion(-30, -10, 20, 20)); +} + +void WSGDamageGraphTest::worldVisibleRotate90() +{ + WSGDamageNode root; + auto *tr = new WSGDamageTransformNode; + QTransform t; + t.translate(50, 50); + t.rotate(90); + t.translate(-50, -50); + tr->setMatrix(t); + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(40, 40, 20, 10)); + g->setFullyOpaque(true); + tr->appendChild(g); + root.appendChild(tr); + + TestTracker tracker(&root); + tracker.commit(); + QVERIFY(isAxisAligned(g->worldTransform().toTransform())); + COMPARE_REGION(g->worldOpaqueRegion(), QRegion(g->worldBounds())); + COMPARE_REGION(g->worldValidRegion(), QRegion(g->worldBounds())); +} + +void WSGDamageGraphTest::backdropKeepsCoveredBehindDamage() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(20, 20, 40, 40)); + back->setFullyOpaque(true); + auto *backdrop = new WSGDamageBackdropNode; + backdrop->setBoundingRect(QRectF(10, 10, 60, 60)); + auto *cover = new WSGDamageGeometryNode; + cover->setBoundingRect(QRectF(0, 0, 80, 80)); + cover->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(backdrop); + root.appendChild(cover); + + TestTracker tracker(&root); + tracker.commit(); + + back->markContentDirty(QRect(8, 8, 10, 10)); + COMPARE_REGION(tracker.commit(), QRegion(28, 28, 10, 10)); +} + +void WSGDamageGraphTest::backdropPunchesFrontOpaque() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(20, 20, 40, 40)); + back->setFullyOpaque(true); + auto *backdrop = new WSGDamageBackdropNode; + backdrop->setBoundingRect(QRectF(10, 10, 60, 60)); + backdrop->setNeedsBackdrop(true); + auto *cover = new WSGDamageGeometryNode; + cover->setBoundingRect(QRectF(0, 0, 80, 80)); + cover->setFullyOpaque(true); + root.appendChild(back); + root.appendChild(backdrop); + root.appendChild(cover); + + TestTracker tracker(&root); + tracker.commit(); + + COMPARE_REGION(back->worldValidRegion(), QRegion(20, 20, 40, 40)); + COMPARE_REGION(cover->worldValidRegion(), QRegion(0, 0, 80, 80)); + + backdrop->setFullyOpaque(true); + tracker.commit(); + COMPARE_REGION(back->worldValidRegion(), QRegion(20, 20, 40, 40)); + + auto *outside = new WSGDamageGeometryNode; + outside->setBoundingRect(QRectF(90, 0, 10, 10)); + outside->setFullyOpaque(true); + root.prependChild(outside); + auto *outsideCover = new WSGDamageGeometryNode; + outsideCover->setBoundingRect(QRectF(90, 0, 10, 10)); + outsideCover->setFullyOpaque(true); + root.appendChild(outsideCover); + tracker.commit(); + COMPARE_REGION(outside->worldValidRegion(), QRegion()); +} + +void WSGDamageGraphTest::backdropBehindDamageMatchesAccumulator() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 50, 50)); + auto *backdrop = new WSGDamageBackdropNode; + backdrop->setBoundingRect(QRectF(10, 10, 40, 40)); + backdrop->setNeedsBackdrop(true); + root.appendChild(back); + root.appendChild(backdrop); + + TestTracker tracker(&root); + tracker.commit(); + back->markContentDirty(QRect(5, 5, 8, 8)); + tracker.commit(); + + COMPARE_REGION(backdrop->behindDamageRegion(), QRegion(5, 5, 8, 8)); + COMPARE_REGION(WPixmanRegion(backdrop->behindDamageRegion()) & backdrop->worldBounds(), + QRegion(10, 10, 3, 3)); +} + +void WSGDamageGraphTest::backdropUncoveredRegion() +{ + WSGDamageNode root; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(0, 0, 50, 50)); + auto *backdrop = new WSGDamageBackdropNode; + backdrop->setBoundingRect(QRectF(10, 10, 40, 40)); + backdrop->setNeedsBackdrop(true); + root.appendChild(back); + root.appendChild(backdrop); + + TestTracker tracker(&root); + tracker.commit(); + COMPARE_REGION(back->worldVisibleRegion(), QRegion(0, 0, 50, 50) - QRegion(10, 10, 40, 40)); + COMPARE_REGION(backdrop->worldVisibleRegion(), QRegion(10, 10, 40, 40)); +} + +void WSGDamageGraphTest::stackedBackdropsVisibleRegions() +{ + WSGDamageNode root; + auto *wall = new WSGDamageGeometryNode; + wall->setBoundingRect(QRectF(0, 0, 120, 120)); + wall->setFullyOpaque(true); + auto *backGlass = new WSGDamageBackdropNode; + backGlass->setBoundingRect(QRectF(0, 0, 80, 80)); + auto *frontGlass = new WSGDamageBackdropNode; + frontGlass->setBoundingRect(QRectF(40, 40, 80, 80)); + root.appendChild(wall); + root.appendChild(backGlass); + root.appendChild(frontGlass); + + TestTracker tracker(&root); + tracker.commit(); + + COMPARE_REGION(backGlass->worldValidRegion(), QRegion(0, 0, 80, 80)); + COMPARE_REGION(frontGlass->worldValidRegion(), QRegion(40, 40, 80, 80)); + COMPARE_REGION(backGlass->worldVisibleRegion(), + QRegion(0, 0, 80, 80) - QRegion(40, 40, 80, 80)); + COMPARE_REGION(frontGlass->worldVisibleRegion(), QRegion(40, 40, 80, 80)); + COMPARE_REGION(wall->worldVisibleRegion(), + QRegion(0, 0, 120, 120) - QRegion(0, 0, 80, 80) - QRegion(40, 40, 80, 80)); +} + +void WSGDamageGraphTest::backdropRecopyClippedToSampleBounds() +{ + WSGDamageNode root; + auto *wall = new WSGDamageGeometryNode; + wall->setBoundingRect(QRectF(0, 0, 200, 200)); + auto *glass = new WSGDamageBackdropNode; + glass->setBoundingRect(QRectF(50, 50, 100, 100)); + glass->setRecopyExpansion(QMargins(10, 10, 10, 10)); // sample bounds: (40, 40, 120, 120) + root.appendChild(wall); + root.appendChild(glass); + + WSGDamageTracker tracker(&root); + auto viewport = std::make_shared(); + tracker.addViewport(viewport); + + tracker.commit(); + viewport->finishFrame(); + glass->consumeRecopy(glass->pendingRecopy(viewport.get()), viewport.get()); + + // Dirty region on wall partially outside the glass sample bounds: + // sample bounds are (40, 40, 120, 120). + // Wall dirt: (20, 50, 60, 50). Intersection with sample is (40, 50, 40, 50). + wall->markContentDirty(QRect(20, 50, 60, 50)); + tracker.commit(); + viewport->finishFrame(); + + const WPixmanRegion recopy = glass->pendingRecopy(viewport.get()); + COMPARE_REGION(recopy, QRegion(40, 50, 40, 50)); +} + +void WSGDamageGraphTest::stackedBackdropsRecaptureOverlap() +{ + WSGDamageNode root; + auto *wall = new WSGDamageGeometryNode; + wall->setBoundingRect(QRectF(0, 0, 120, 120)); + wall->setFullyOpaque(true); + auto *backGlass = new WSGDamageBackdropNode; + backGlass->setBoundingRect(QRectF(0, 0, 80, 80)); + backGlass->setNeedsBackdrop(true); + auto *frontGlass = new WSGDamageBackdropNode; + frontGlass->setBoundingRect(QRectF(40, 40, 80, 80)); + frontGlass->setNeedsBackdrop(true); + root.appendChild(wall); + root.appendChild(backGlass); + root.appendChild(frontGlass); + + TestTracker tracker(&root); + tracker.commit(); + wall->markContentDirty(QRect(50, 50, 10, 10)); + tracker.commit(); + + COMPARE_REGION(WPixmanRegion(backGlass->behindDamageRegion()) & backGlass->worldBounds(), + QRegion(50, 50, 10, 10)); + COMPARE_REGION(WPixmanRegion(frontGlass->behindDamageRegion()) & frontGlass->worldBounds(), + QRegion(50, 50, 10, 10)); +} + +void WSGDamageGraphTest::stackedBackdropsExclusiveDirtySkipsFront() +{ + WSGDamageNode root; + auto *wall = new WSGDamageGeometryNode; + wall->setBoundingRect(QRectF(0, 0, 120, 120)); + wall->setFullyOpaque(true); + auto *backGlass = new WSGDamageBackdropNode; + backGlass->setBoundingRect(QRectF(0, 0, 80, 80)); + backGlass->setNeedsBackdrop(true); + auto *frontGlass = new WSGDamageBackdropNode; + frontGlass->setBoundingRect(QRectF(40, 40, 80, 80)); + frontGlass->setNeedsBackdrop(true); + root.appendChild(wall); + root.appendChild(backGlass); + root.appendChild(frontGlass); + + TestTracker tracker(&root); + tracker.commit(); + wall->markContentDirty(QRect(5, 5, 10, 10)); + tracker.commit(); + + COMPARE_REGION(WPixmanRegion(backGlass->behindDamageRegion()) & backGlass->worldBounds(), + QRegion(5, 5, 10, 10)); + COMPARE_REGION(WPixmanRegion(frontGlass->behindDamageRegion()) & frontGlass->worldBounds(), + QRegion()); +} + +void WSGDamageGraphTest::stackedBackdropsIndependentCopySources() +{ + WSGDamageNode root; + auto *wall = new WSGDamageGeometryNode; + wall->setBoundingRect(QRectF(0, 0, 200, 100)); + wall->setFullyOpaque(true); + auto *left = new WSGDamageBackdropNode; + left->setBoundingRect(QRectF(0, 0, 80, 80)); + auto *overlap = new WSGDamageBackdropNode; + overlap->setBoundingRect(QRectF(40, 40, 80, 80)); + auto *right = new WSGDamageBackdropNode; + right->setBoundingRect(QRectF(140, 20, 50, 50)); + root.appendChild(wall); + root.appendChild(left); + root.appendChild(overlap); + root.appendChild(right); + + TestTracker tracker(&root); + tracker.commit(); + // Simulate the renderer consuming every cache refresh after the settle. + for (auto *glass : { left, overlap, right }) + glass->consumeRecopy(glass->pendingRecopy(tracker.viewport.get()), tracker.viewport.get()); + + wall->markContentDirty(QRect(50, 50, 10, 10)); + tracker.commit(); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(left, tracker.viewport.get())), + QRegion(50, 50, 10, 10)); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(overlap, tracker.viewport.get())), + QRegion(50, 50, 10, 10)); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(right, tracker.viewport.get())), + QRegion()); + + wall->markContentDirty(QRect(150, 30, 8, 8)); + tracker.commit(); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(left, tracker.viewport.get())), + QRegion(50, 50, 10, 10)); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(overlap, tracker.viewport.get())), + QRegion(50, 50, 10, 10)); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(right, tracker.viewport.get())), + QRegion(150, 30, 8, 8)); +} + +void WSGDamageGraphTest::backdropSeesRemovedSiblingHole() +{ + // Regression: a node removed BEHIND a backdrop sibling leaves its hole on + // the grouping parent. Grouping ownDamage must land before children so the + // backdrop's copy source includes the hole; otherwise the blitter keeps + // stale pixels of the removed content. + WSGDamageNode root; + auto *group = new WSGDamageNode; + auto *back = new WSGDamageGeometryNode; + back->setBoundingRect(QRectF(20, 20, 40, 40)); + auto *backdrop = new WSGDamageBackdropNode; + backdrop->setBoundingRect(QRectF(10, 10, 60, 60)); + backdrop->setNeedsBackdrop(true); + group->appendChild(back); + group->appendChild(backdrop); + root.appendChild(group); + + TestTracker tracker(&root); + tracker.commit(); + backdrop->consumeRecopy(backdrop->pendingRecopy(tracker.viewport.get()), tracker.viewport.get()); + + group->removeChild(back); + tracker.commit(); + COMPARE_REGION(WPixmanRegion(*WSGDamageNodeTestAccess::pendingRecopy(backdrop, tracker.viewport.get())), + QRegion(20, 20, 40, 40)); + COMPARE_REGION(tracker.commit(), QRegion()); +} + +void WSGDamageGraphTest::backgroundDamageRecopySurvivesRepeatedCommit() +{ + WSGDamageNode root; + // Simulated background node injected at the front of a downstream source tree + auto *background = new WSGDamageGeometryNode; + background->setBoundingRect(QRectF(0, 0, 800, 600)); + root.appendChild(background); + + // Opaque card occluding a portion of the background + auto *opaqueCard = new WSGDamageGeometryNode; + opaqueCard->setBoundingRect(QRectF(100, 100, 200, 200)); + opaqueCard->setFullyOpaque(true); + root.appendChild(opaqueCard); + + // Backdrop/glass node sampling the background + auto *glass = new WSGDamageBackdropNode; + glass->setBoundingRect(QRectF(350, 100, 150, 150)); + glass->setRecopyExpansion(QMargins(10, 10, 10, 10)); + root.appendChild(glass); + + WSGDamageTracker tracker(&root); + auto viewport = std::make_shared(); + tracker.addViewport(viewport); + + // Initial frame + tracker.commit(); + viewport->finishFrame(); + glass->consumeRecopy(glass->pendingRecopy(viewport.get()), viewport.get()); + // Dirty regions in the preceding source background: + // Region A: (150, 150, 50, 50) wholly under opaque card + // Region B: (380, 120, 40, 40) under glass node + // Region C: (600, 400, 50, 50) in clear background + const QRegion dirtA(150, 150, 50, 50); + const QRegion dirtB(380, 120, 40, 40); + const QRegion dirtC(600, 400, 50, 50); + background->markContentDirty(dirtA + dirtB + dirtC); + + tracker.commit(); + const WPixmanRegion damage = viewport->damageRegion().region; + + // 1. Occlusion: dirtA must NOT appear in damage + QVERIFY((damage & WPixmanRegion(dirtA.boundingRect())).isEmpty()); + + // 2. Clear background: dirtC must appear in damage + COMPARE_REGION(damage & WPixmanRegion(dirtC.boundingRect()), dirtC); + + // 3. Glass recopy: glass must have received dirtB as recopy debt + const WPixmanRegion recopyDebt = glass->pendingRecopy(viewport.get()); + COMPARE_REGION(recopyDebt, dirtB); + + // 4. Repeated commit in the same frame is idempotent + tracker.commit(); + COMPARE_REGION(viewport->damageRegion().region, damage); + + viewport->finishFrame(); + glass->consumeRecopy(recopyDebt, viewport.get()); + // Next idle frame produces no damage and no recopy debt + tracker.commit(); + const WPixmanRegion idleDamage = viewport->damageRegion().region; + viewport->finishFrame(); + QVERIFY(idleDamage.isEmpty()); + QVERIFY(glass->pendingRecopy(viewport.get()).isEmpty()); +} + +void WSGDamageGraphTest::multiOutputDisjointSceneMapping() +{ + WSGDamageNode root; + auto *itemScreen1 = new WSGDamageGeometryNode; + itemScreen1->setBoundingRect(QRectF(100, 100, 80, 80)); + root.appendChild(itemScreen1); + + auto *itemScreen2 = new WSGDamageGeometryNode; + itemScreen2->setBoundingRect(QRectF(2100, 100, 80, 80)); + root.appendChild(itemScreen2); + + WSGDamageTracker tracker(&root); + const QSize buf(1920, 1080); + auto viewport1 = std::make_shared(); + viewport1->setRenderParameters(QMatrix4x4(), QRectF(0, 0, 1920, 1080)); + auto viewport2 = std::make_shared(); + viewport2->setRenderParameters(QMatrix4x4(), QRectF(1920, 0, 1920, 1080)); + tracker.addViewport(viewport1); + tracker.addViewport(viewport2); + viewport1->finishFrame(); + viewport2->finishFrame(); + auto bufferDamage = [&](const WSGViewport &v) { + WPixmanRegion r = + v.damageRegion().region.mappedOuter(sceneToBuffer(v, buf)); + r &= QRect(QPoint(), buf); + return r; + }; + auto commitOutputs = [&] { + tracker.commit(); + }; + + commitOutputs(); + QVERIFY(viewport1->isDirty()); + QVERIFY(viewport2->isDirty()); + COMPARE_REGION(bufferDamage(*viewport1), QRegion(100, 100, 80, 80)); + COMPARE_REGION(bufferDamage(*viewport2), QRegion(180, 100, 80, 80)); + viewport1->finishFrame(); + + itemScreen1->setBoundingRect(QRectF(150, 100, 80, 80)); + commitOutputs(); + QVERIFY(viewport1->isDirty()); + QVERIFY(viewport2->isDirty()); + COMPARE_REGION(bufferDamage(*viewport1), QRegion(100, 100, 130, 80)); + COMPARE_REGION(bufferDamage(*viewport2), QRegion(180, 100, 80, 80)); + viewport1->finishFrame(); + + itemScreen2->setBoundingRect(QRectF(2200, 100, 80, 80)); + commitOutputs(); + QVERIFY(!viewport1->isDirty()); + QVERIFY(viewport2->isDirty()); + COMPARE_REGION(bufferDamage(*viewport2), + QRegion(180, 100, 80, 80) + QRegion(280, 100, 80, 80)); + viewport1->finishFrame(); + + itemScreen2->setBoundingRect(QRectF(2300, 100, 80, 80)); + commitOutputs(); + QVERIFY(!viewport1->isDirty()); + QVERIFY(viewport2->isDirty()); + COMPARE_REGION(bufferDamage(*viewport2), + QRegion(180, 100, 80, 80) + QRegion(280, 100, 80, 80) + + QRegion(380, 100, 80, 80)); + viewport2->finishFrame(); + + auto *straddle = new WSGDamageGeometryNode; + straddle->setBoundingRect(QRectF(1900, 200, 40, 40)); + root.appendChild(straddle); + commitOutputs(); + QVERIFY(viewport1->isDirty()); + QVERIFY(viewport2->isDirty()); + COMPARE_REGION(bufferDamage(*viewport1), QRegion(1900, 200, 20, 40)); + COMPARE_REGION(bufferDamage(*viewport2), QRegion(0, 200, 20, 40)); +} + +void WSGDamageGraphTest::viewportDirtyUsesVisibleSourceRegion() +{ + WSGViewport viewport; + viewport.setRenderParameters(QMatrix4x4(), QRectF(50, 0, 50, 100), + QRectF(-50, 0, 100, 100)); + viewport.finishFrame(); + + viewport.addDamage(WDamageRegion(WPixmanRegion(QRect(10, 10, 10, 10)), false)); + QVERIFY(!viewport.isDirty()); + + viewport.finishFrame(); + viewport.addDamage(WDamageRegion(WPixmanRegion(QRect(60, 10, 10, 10)), false)); + QVERIFY(viewport.isDirty()); +} + +void WSGDamageGraphTest::highDpiScaledOutputMapping() +{ + WSGDamageNode root; + auto *item = new WSGDamageGeometryNode; + item->setBoundingRect(QRectF(50, 60, 100, 80)); + root.appendChild(item); + + WSGDamageTracker tracker(&root); + auto viewport = std::make_shared(); + tracker.addViewport(viewport); + viewport->finishFrame(); + const QSize buf(3840, 2160); + qreal dpr = 2.0; + auto mapped = [&] { + WPixmanRegion r = + viewport->damageRegion().region.mappedOuter(sceneToBuffer(*viewport, buf, dpr)); + r &= QRect(QPoint(), buf); + return r; + }; + + tracker.commit(); + QVERIFY(viewport->isDirty()); + COMPARE_REGION(viewport->damageRegion().region, QRegion(50, 60, 100, 80)); + COMPARE_REGION(mapped(), QRegion(100, 120, 200, 160)); + viewport->finishFrame(); + + item->setBoundingRect(QRectF(100, 60, 100, 80)); + auto *edge = new WSGDamageGeometryNode; + edge->setBoundingRect(QRectF(1900, 1040, 80, 80)); + root.appendChild(edge); + auto *outside = new WSGDamageGeometryNode; + outside->setBoundingRect(QRectF(2200, 1200, 80, 80)); + root.appendChild(outside); + tracker.commit(); + QVERIFY(viewport->isDirty()); + COMPARE_REGION(mapped(), QRegion(100, 120, 300, 160) + QRegion(3800, 2080, 40, 80)); + viewport->finishFrame(); + + dpr = 1.5; + item->markContentDirty(QRect(0, 0, 100, 80)); + auto *fractionalEdge = new WSGDamageGeometryNode; + fractionalEdge->setBoundingRect(QRectF(2550, 1430, 40, 40)); + root.appendChild(fractionalEdge); + tracker.commit(); + QVERIFY(viewport->isDirty()); + COMPARE_REGION(mapped(), QRegion(150, 90, 150, 120) + QRegion(3825, 2145, 15, 15)); +} + +void WSGDamageGraphTest::rotatedOutputMapping() +{ + WSGDamageNode root; + auto *item = new WSGDamageGeometryNode; + item->setBoundingRect(QRectF(100, 200, 50, 60)); + root.appendChild(item); + + QMatrix4x4 rotation; + rotation.translate(1920, 0); + rotation.rotate(90, 0, 0, 1); + auto viewport = std::make_shared(); + viewport->setRenderParameters(rotation); + const QSize buf(1920, 1080); + auto mapped = [&] { + WPixmanRegion r = + viewport->damageRegion().region.mappedOuter(sceneToBuffer(*viewport, buf)); + r &= QRect(QPoint(), buf); + return r; + }; + WSGDamageTracker tracker(&root); + tracker.addViewport(viewport); + viewport->finishFrame(); + + tracker.commit(); + QVERIFY(viewport->isDirty()); + COMPARE_REGION(mapped(), QRegion(1660, 100, 60, 50)); + viewport->finishFrame(); + auto *lower = new WSGDamageGeometryNode; + lower->setBoundingRect(QRectF(100, 1200, 50, 60)); + root.appendChild(lower); + auto *edge = new WSGDamageGeometryNode; + edge->setBoundingRect(QRectF(1060, 1900, 50, 60)); + root.appendChild(edge); + auto *outside = new WSGDamageGeometryNode; + outside->setBoundingRect(QRectF(1500, 100, 50, 60)); + root.appendChild(outside); + tracker.commit(); + QVERIFY(viewport->isDirty()); + COMPARE_REGION(mapped(), QRegion(660, 100, 60, 50) + QRegion(0, 1060, 20, 20)); +} + +void WSGDamageGraphTest::singularOutputMapping() +{ + WSGDamageNode root; + auto *item = new WSGDamageGeometryNode; + item->setBoundingRect(QRectF(100, 100, 80, 80)); + root.appendChild(item); + + auto viewport = std::make_shared(); + QMatrix4x4 singular; + singular.scale(0, 1); + viewport->setRenderParameters(singular); + const QSize buf(1920, 1080); + WSGDamageTracker tracker(&root); + tracker.addViewport(viewport); + + tracker.commit(); + QVERIFY(viewport->isFull()); + QVERIFY(viewport->isDirty()); + QVERIFY(viewport->isFull()); + viewport->finishFrame(); + + viewport->setRenderParameters(QMatrix4x4()); + QVERIFY(viewport->isFull()); + QVERIFY(viewport->isDirty()); + tracker.commit(); + viewport->finishFrame(); + item->markContentDirty(QRect(10, 10, 10, 10)); + tracker.commit(); + QVERIFY(!viewport->isFull()); + QVERIFY(viewport->isDirty()); + COMPARE_REGION(viewport->damageRegion().region, QRegion(110, 110, 10, 10)); +} + +void WSGDamageGraphTest::viewportRegistrationLifecycle() +{ + WSGDamageNode root; + auto *g = new WSGDamageGeometryNode; + g->setBoundingRect(QRectF(10, 10, 20, 20)); + root.appendChild(g); + + WSGDamageTracker tracker(&root); + + auto vp1 = std::make_shared(); + auto vp2 = std::make_shared(); + + // 1. Initial registration + tracker.addViewport(vp1); + tracker.addViewport(vp2); + // Initial commit establishes the baseline with g at (10, 10, 20, 20) + tracker.commit(); + vp1->finishFrame(); + vp2->finishFrame(); + + // 2. Duplicate registration is a no-op + tracker.addViewport(vp1); + + // 3. One commit distributes damage to all registered viewports + g->setBoundingRect(QRectF(20, 20, 30, 30)); + tracker.commit(); + QVERIFY(vp1->isDirty()); + QVERIFY(vp2->isDirty()); + COMPARE_REGION(vp1->damageRegion().region, QRegion(10, 10, 20, 20) + QRegion(20, 20, 30, 30)); + COMPARE_REGION(vp2->damageRegion().region, QRegion(10, 10, 20, 20) + QRegion(20, 20, 30, 30)); + + // 5. Idempotent commit within same frame + tracker.commit(); + COMPARE_REGION(vp1->damageRegion().region, QRegion(10, 10, 20, 20) + QRegion(20, 20, 30, 30)); + + // 7. Viewport removal + tracker.removeViewport(vp2.get()); + vp1->finishFrame(); + vp2->finishFrame(); + + g->markContentDirty(QRect(20, 20, 5, 5)); + tracker.commit(); + QVERIFY(vp1->isDirty()); + QVERIFY(!vp2->isDirty()); + + // 8. Expired weak_ptr cleanup + { + auto vp3 = std::make_shared(); + tracker.addViewport(vp3); + // vp3 goes out of scope and is destroyed + } + // commit should prune the expired viewport without crash + tracker.commit(); +} + +QTEST_MAIN(WSGDamageGraphTest) +#include "tst_wdamagegraph.moc" diff --git a/waylib/tests/unit_tests/test_wdamagegraph_demo/CMakeLists.txt b/waylib/tests/unit_tests/test_wdamagegraph_demo/CMakeLists.txt new file mode 100644 index 0000000000..1c4d9015ff --- /dev/null +++ b/waylib/tests/unit_tests/test_wdamagegraph_demo/CMakeLists.txt @@ -0,0 +1,33 @@ +# Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +find_package(Qt6 REQUIRED COMPONENTS Test Qml Quick) + +add_executable(test_wdamagegraph_demo + tst_wdamagegraph_demo.cpp + ../../manual/damage/demoscene.cpp + ../../manual/damage/demoscene.h + ../../manual/damage/damageoverlay.cpp + ../../manual/damage/damageoverlay.h + ../../manual/damage/visualnodemodel.cpp + ../../manual/damage/visualnodemodel.h +) + +target_include_directories(test_wdamagegraph_demo PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../../manual/damage" +) + +target_link_libraries(test_wdamagegraph_demo + PRIVATE + Waylib::WaylibServer + Qt::Test + Qt::Qml + Qt::Quick +) + +add_test(NAME test_wdamagegraph_demo COMMAND test_wdamagegraph_demo) + +set_tests_properties(test_wdamagegraph_demo PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 60 +) diff --git a/waylib/tests/unit_tests/test_wdamagegraph_demo/tst_wdamagegraph_demo.cpp b/waylib/tests/unit_tests/test_wdamagegraph_demo/tst_wdamagegraph_demo.cpp new file mode 100644 index 0000000000..2de3c82d8a --- /dev/null +++ b/waylib/tests/unit_tests/test_wdamagegraph_demo/tst_wdamagegraph_demo.cpp @@ -0,0 +1,463 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "demoscene.h" +#include "damageoverlay.h" + +#include +#include +#include +#include +#include + +class tst_Demo : public QObject +{ + Q_OBJECT + +private slots: + void paintOrderNames(); + void addedChildIsNumberedAndSelected(); + void moveActionsFollowTreeDirection(); + void activatingNodeRaisesItToTop(); + void treeMoveReparentsAndReorders(); + void activatingDemoNodeRaisesItToTop(); + void rotationAndScaleScenesAnimate(); + void builtInScenesAnimateAndAllowEditing(); + void selectingTreeNodeDoesNotRaiseIt(); + void opacityChangesFixedName(); + void dragUpdatesCoordinatesInRealTime(); + void refreshRateThrottlesDragFrames(); + void damageHistoryAccumulatesFrames(); + void damageHistoryColorsOldGreenNewRed(); + void damageHistoryDurationIsConfigurable(); + void refreshRateIsConfigurable(); + void rotationAxisPersistsAfterCommit(); + void transformInspectorExposesMatrix(); + void backdropExpansionDilatesDamage(); + void clipSceneKeepsDamageInsideViewport(); + void addClipCreatesClipNode(); +}; + +void tst_Demo::paintOrderNames() +{ + DemoScene scene; + const QVariantList tree = scene.treeNodes(); + + QCOMPARE(tree.size(), 3); + const QString n0 = tree.at(0).toMap().value(QStringLiteral("name")).toString(); + const QString n1 = tree.at(1).toMap().value(QStringLiteral("name")).toString(); + const QString n2 = tree.at(2).toMap().value(QStringLiteral("name")).toString(); + QVERIFY(n0.startsWith(QStringLiteral("0:根节点 #"))); + QVERIFY(n1.startsWith(QStringLiteral("1:不透明几何 #"))); + QVERIFY(n1.contains(QStringLiteral("[260x200]"))); + QVERIFY(n2.startsWith(QStringLiteral("2:不透明几何 #"))); + QVERIFY(n2.contains(QStringLiteral("[220x180]"))); +} +void tst_Demo::addedChildIsNumberedAndSelected() +{ + DemoScene scene; + scene.addGeometry(); + + const QVariantList tree = scene.treeNodes(); + QCOMPARE(tree.size(), 4); + const QString n2 = tree.at(2).toMap().value(QStringLiteral("name")).toString(); + const QString n3 = tree.at(3).toMap().value(QStringLiteral("name")).toString(); + QVERIFY(n2.startsWith(QStringLiteral("2:不透明几何 #"))); + QVERIFY(n3.startsWith(QStringLiteral("3:不透明几何 #"))); + QCOMPARE(scene.selectedProps().value(QStringLiteral("name")).toString(), n2); +} + +void tst_Demo::moveActionsFollowTreeDirection() +{ + DemoScene scene; + const quint64 selected = scene.selectedId(); + scene.raiseSelected(); + QVariantList tree = scene.treeNodes(); + QCOMPARE(tree.at(2).toMap().value(QStringLiteral("id")).toULongLong(), selected); + QVERIFY(scene.selectedProps().value(QStringLiteral("name")).toString() + .startsWith(QStringLiteral("2:不透明几何 #"))); + + scene.lowerSelected(); + tree = scene.treeNodes(); + QCOMPARE(tree.at(1).toMap().value(QStringLiteral("id")).toULongLong(), selected); + QVERIFY(scene.selectedProps().value(QStringLiteral("name")).toString() + .startsWith(QStringLiteral("1:不透明几何 #"))); +} + +void tst_Demo::activatingNodeRaisesItToTop() +{ + DemoScene scene; + const quint64 back = scene.treeNodes().at(1).toMap() + .value(QStringLiteral("id")).toULongLong(); + + scene.activateNode(back); + + const QVariantList tree = scene.treeNodes(); + QCOMPARE(tree.at(2).toMap().value(QStringLiteral("id")).toULongLong(), back); + QVERIFY(scene.selectedProps().value(QStringLiteral("name")).toString() + .startsWith(QStringLiteral("2:不透明几何 #"))); +} + +void tst_Demo::activatingDemoNodeRaisesItToTop() +{ + DemoScene scene; + scene.loadDemoScene(QStringLiteral("occlusion")); + const quint64 back = scene.treeNodes().at(1).toMap() + .value(QStringLiteral("id")).toULongLong(); + scene.activateNode(back); + QCOMPARE(scene.treeNodes().at(2).toMap().value(QStringLiteral("id")).toULongLong(), back); +} +void tst_Demo::treeMoveReparentsAndReorders() +{ + DemoScene scene; + const QVariantList initial = scene.treeNodes(); + const quint64 rootId = initial.at(0).toMap().value(QStringLiteral("id")).toULongLong(); + const quint64 backId = initial.at(1).toMap().value(QStringLiteral("id")).toULongLong(); + const quint64 frontId = initial.at(2).toMap().value(QStringLiteral("id")).toULongLong(); + + scene.moveNode(frontId, backId); + QCOMPARE(scene.treeNodes().at(2).toMap().value(QStringLiteral("parentId")).toULongLong(), + backId); + + scene.moveNode(frontId, rootId, backId); + QCOMPARE(scene.treeNodes().at(1).toMap().value(QStringLiteral("id")).toULongLong(), + frontId); + QCOMPARE(scene.treeNodes().at(1).toMap().value(QStringLiteral("parentId")).toULongLong(), + rootId); +} + +void tst_Demo::builtInScenesAnimateAndAllowEditing() +{ + DemoScene scene; + scene.loadDemoScene(QStringLiteral("content")); + QCOMPARE(scene.demoScenes().size(), 8); + QCOMPARE(scene.demoSceneName(), QStringLiteral("content")); + QVERIFY(!scene.selectedProps().isEmpty()); + const qsizetype initialFrames = scene.damageFrames().size(); + scene.setDemoRunning(false); + scene.setDemoRunning(true); + scene.stepDemoFrame(); + QVERIFY(scene.damageFrames().size() > initialFrames); + scene.setDemoRunning(false); + const qsizetype pausedFrames = scene.damageFrames().size(); + scene.stepDemoFrame(); + QCOMPARE(scene.damageFrames().size(), pausedFrames); + + const int count = scene.treeNodes().size(); + scene.addGeometry(); + QCOMPARE(scene.treeNodes().size(), count + 1); +} +void tst_Demo::rotationAndScaleScenesAnimate() +{ + DemoScene scene; + for (const QString &name : {QStringLiteral("rotation"), QStringLiteral("scale")}) { + scene.loadDemoScene(name); + QCOMPARE(scene.selectedProps().value(QStringLiteral("type")).toString(), + QStringLiteral("Transform")); + const qsizetype initialFrames = scene.damageFrames().size(); + scene.stepDemoFrame(); + QVERIFY(scene.damageFrames().size() > initialFrames); + } +} + +void tst_Demo::selectingTreeNodeDoesNotRaiseIt() +{ + DemoScene scene; + scene.raiseSelected(); + const quint64 lowerNode = scene.treeNodes().at(1).toMap() + .value(QStringLiteral("id")).toULongLong(); + + scene.setSelectedId(lowerNode); + + QCOMPARE(scene.treeNodes().at(1).toMap() + .value(QStringLiteral("id")).toULongLong(), lowerNode); +} + +void tst_Demo::opacityChangesFixedName() +{ + DemoScene scene; + scene.setFullyOpaqueSelected(false); + + const QString name = scene.selectedProps().value(QStringLiteral("name")).toString(); + QVERIFY(name.startsWith(QStringLiteral("1:透明几何 #"))); + QCOMPARE(scene.treeNodes().at(1).toMap().value(QStringLiteral("name")).toString(), + name); +} +void tst_Demo::dragUpdatesCoordinatesInRealTime() +{ + DemoScene scene; + scene.moveSelectedBy(5, 7); + + QCOMPARE(scene.selectedProps().value(QStringLiteral("x")).toReal(), 85.0); + QCOMPARE(scene.selectedProps().value(QStringLiteral("y")).toReal(), 87.0); + QCOMPARE(scene.visualNodesModel()->getRow(0).value(QStringLiteral("x")).toReal(), 85.0); + QCOMPARE(scene.visualNodesModel()->getRow(0).value(QStringLiteral("y")).toReal(), 87.0); + scene.finishSelectedMove(); + QCOMPARE(scene.visualNodesModel()->getRow(0).value(QStringLiteral("x")).toReal(), 85.0); + QCOMPARE(scene.visualNodesModel()->getRow(0).value(QStringLiteral("y")).toReal(), 87.0); +} + +void tst_Demo::refreshRateThrottlesDragFrames() +{ + DemoScene scene; + scene.setRefreshRate(1); + const qsizetype initialFrames = scene.damageFrames().size(); + + scene.moveSelectedBy(1, 0); + QCOMPARE(scene.damageFrames().size(), initialFrames + 1); + + scene.moveSelectedBy(1, 0); + QCOMPARE(scene.damageFrames().size(), initialFrames + 1); + + scene.finishSelectedMove(); + QCOMPARE(scene.damageFrames().size(), initialFrames + 2); +} + +void tst_Demo::damageHistoryAccumulatesFrames() +{ + DemoScene scene; + const qsizetype initialFrames = scene.damageFrames().size(); + + scene.moveSelectedBy(2, 0); + scene.moveSelectedBy(3, 0); + scene.finishSelectedMove(); + + const QVariantList frames = scene.damageFrames(); + QCOMPARE(frames.size(), initialFrames + 2); + const QVariantMap previous = frames.at(frames.size() - 2).toMap(); + const QVariantMap newest = frames.constLast().toMap(); + QVERIFY(!previous.value(QStringLiteral("rects")).toList().isEmpty()); + QVERIFY(!newest.value(QStringLiteral("rects")).toList().isEmpty()); + QVERIFY(previous.value(QStringLiteral("timestamp")).toLongLong() + <= newest.value(QStringLiteral("timestamp")).toLongLong()); +} + +void tst_Demo::damageHistoryColorsOldGreenNewRed() +{ + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + const auto frame = [](qint64 timestamp, const QRect &rect) { + QVariantMap geometry; + geometry.insert(QStringLiteral("x"), rect.x()); + geometry.insert(QStringLiteral("y"), rect.y()); + geometry.insert(QStringLiteral("w"), rect.width()); + geometry.insert(QStringLiteral("h"), rect.height()); + QVariantMap damageFrame; + damageFrame.insert(QStringLiteral("timestamp"), timestamp); + damageFrame.insert(QStringLiteral("rects"), QVariantList{geometry}); + return damageFrame; + }; + + QVariantList frames; + for (int i = 0; i < 8; ++i) + frames.append(frame(now, QRect(i * 10, 0, 10, 10))); + + DamageOverlay overlay; + overlay.setHistoryDuration(1800); + overlay.setFrames(frames); + + QImage image(82, 12, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + QPainter painter(&image); + overlay.paint(&painter); + painter.end(); + + const QColor oldDamage = image.pixelColor(5, 5); + const QColor newDamage = image.pixelColor(75, 5); + QVERIFY(oldDamage.green() > oldDamage.red()); + QVERIFY(newDamage.red() > newDamage.green()); + QVERIFY(oldDamage.alpha() < 150); + QVERIFY(newDamage.alpha() < 150); + + QVariantList overlapping{ + frame(now - 100, QRect(0, 0, 10, 10)), + frame(now, QRect(0, 0, 10, 10)), + }; + overlay.setFrames(overlapping); + image.fill(Qt::transparent); + QPainter overlapPainter(&image); + overlay.paint(&overlapPainter); + overlapPainter.end(); + const QColor overlapDamage = image.pixelColor(5, 5); + QVERIFY(overlapDamage.red() > overlapDamage.green()); + QVERIFY(overlapDamage.alpha() < 130); + + // Time controls expiration only. A lone frame stays red for its lifetime. + overlay.setFrames({frame(now - 1500, QRect(0, 0, 10, 10))}); + image.fill(Qt::transparent); + QPainter singlePainter(&image); + overlay.paint(&singlePainter); + singlePainter.end(); + const QColor singleDamage = image.pixelColor(5, 5); + QVERIFY(singleDamage.red() > singleDamage.green()); +} + +void tst_Demo::damageHistoryDurationIsConfigurable() +{ + DamageOverlay overlay; + overlay.setHistoryDuration(3200); + QCOMPARE(overlay.historyDuration(), 3200); + + overlay.setHistoryDuration(50); + QCOMPARE(overlay.historyDuration(), 100); +} + +void tst_Demo::refreshRateIsConfigurable() +{ + DemoScene scene; + scene.setRefreshRate(144); + QCOMPARE(scene.refreshRate(), 144); + scene.setRefreshRate(0); + QCOMPARE(scene.refreshRate(), 1); + + DamageOverlay overlay; + overlay.setRefreshRate(144); + QCOMPARE(overlay.refreshRate(), 144); + overlay.setRefreshRate(0); + QCOMPARE(overlay.refreshRate(), 1); +} + +void tst_Demo::rotationAxisPersistsAfterCommit() +{ + DemoScene scene; + scene.loadPreset(QStringLiteral("transform")); + + // Set rotation to X axis with 30 degrees. + scene.setRotationSelected(30, 0); + QCOMPARE(scene.selectedProps().value(QStringLiteral("rotationAxis")).toInt(), 0); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal() - 30.0) < 0.1); + + // After a scale change, rotation axis and angle should still be X/30. + scene.setScaleSelected(2, 3); + QCOMPARE(scene.selectedProps().value(QStringLiteral("rotationAxis")).toInt(), 0); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal() - 30.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sx")).toReal() - 2.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sy")).toReal() - 3.0) < 0.1); + + // Set to Y axis with 60 degrees — scale should be preserved. + scene.setRotationSelected(60, 1); + QCOMPARE(scene.selectedProps().value(QStringLiteral("rotationAxis")).toInt(), 1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal() - 60.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sx")).toReal() - 2.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sy")).toReal() - 3.0) < 0.1); + + // Change rotation angle again — scale should not change. + scene.setRotationSelected(45, 1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal() - 45.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sx")).toReal() - 2.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sy")).toReal() - 3.0) < 0.1); + + // Switch to Z axis. + scene.setRotationSelected(90, 2); + QCOMPARE(scene.selectedProps().value(QStringLiteral("rotationAxis")).toInt(), 2); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal() - 90.0) < 0.1); + + // Visual nodes should include perspective components. + scene.loadDemoScene(QStringLiteral("rotation")); + const QVariantMap visual = scene.visualNodesModel()->getRow(0); + QVERIFY(visual.contains(QStringLiteral("m13"))); + QVERIFY(visual.contains(QStringLiteral("m23"))); + QVERIFY(visual.contains(QStringLiteral("m33"))); +} +void tst_Demo::transformInspectorExposesMatrix() +{ + DemoScene scene; + scene.loadPreset(QStringLiteral("transform")); + QVERIFY(scene.selectedProps().value(QStringLiteral("isTransform")).toBool()); + QCOMPARE(scene.selectedProps().value(QStringLiteral("tx")).toReal(), 40.0); + QCOMPARE(scene.selectedProps().value(QStringLiteral("ty")).toReal(), 30.0); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal()) < 0.01); + QCOMPARE(scene.selectedProps().value(QStringLiteral("sx")).toReal(), 1.0); + QCOMPARE(scene.selectedProps().value(QStringLiteral("sy")).toReal(), 1.0); + + scene.setRotationSelected(45, 2); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("rotation")).toReal() - 45.0) < 0.1); + QCOMPARE(scene.selectedProps().value(QStringLiteral("rotationAxis")).toInt(), 2); + QCOMPARE(scene.selectedProps().value(QStringLiteral("tx")).toReal(), 40.0); + QCOMPARE(scene.selectedProps().value(QStringLiteral("ty")).toReal(), 30.0); + scene.setScaleSelected(2, 3); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sx")).toReal() - 2.0) < 0.1); + QVERIFY(qAbs(scene.selectedProps().value(QStringLiteral("sy")).toReal() - 3.0) < 0.1); + QVERIFY(!scene.damageFrames().isEmpty()); + + scene.loadDemoScene(QStringLiteral("rotation")); + const QVariantMap rotatedVisual = scene.visualNodesModel()->getRow(0); + QVERIFY(rotatedVisual.contains(QStringLiteral("localWidth"))); + QVERIFY(rotatedVisual.contains(QStringLiteral("m11"))); + QVERIFY(rotatedVisual.contains(QStringLiteral("m22"))); +} + +static QRect damageBoundingRect(const QVariantList &rects) +{ + QRect bounds; + for (const QVariant &item : rects) { + const QVariantMap m = item.toMap(); + bounds |= QRect(m.value(QStringLiteral("x")).toInt(), + m.value(QStringLiteral("y")).toInt(), + m.value(QStringLiteral("w")).toInt(), + m.value(QStringLiteral("h")).toInt()); + } + return bounds; +} + +void tst_Demo::backdropExpansionDilatesDamage() +{ + DemoScene scene; + scene.loadDemoScene(QStringLiteral("backdrop")); + scene.setDemoRunning(false); + + const quint64 wall = scene.treeNodes().at(1).toMap() + .value(QStringLiteral("id")).toULongLong(); + const quint64 glass = scene.selectedId(); + QVERIFY(scene.selectedProps().value(QStringLiteral("isBackdrop")).toBool()); + QCOMPARE(scene.selectedProps().value(QStringLiteral("expansion")).toInt(), 0); + + scene.setSelectedId(wall); + scene.markSelectedContentDirtyAt(160, 120, 10, 10); + const QRect tight = damageBoundingRect(scene.damageRects()); + QVERIFY(!tight.isEmpty()); + + scene.setSelectedId(glass); + scene.setExpansionSelected(24); + QCOMPARE(scene.selectedProps().value(QStringLiteral("expansion")).toInt(), 24); + scene.setSelectedId(wall); + scene.markSelectedContentDirtyAt(160, 120, 10, 10); + const QRect dilated = damageBoundingRect(scene.damageRects()); + QVERIFY(dilated.contains(tight)); + QVERIFY(dilated.width() > tight.width() || dilated.height() > tight.height()); +} + +void tst_Demo::clipSceneKeepsDamageInsideViewport() +{ + DemoScene scene; + scene.loadDemoScene(QStringLiteral("clip")); + QCOMPARE(scene.demoSceneName(), QStringLiteral("clip")); + QVERIFY(scene.selectedProps().value(QStringLiteral("isClip")).toBool()); + + bool sawClip = false; + for (const QVariant &item : scene.treeNodes()) { + if (item.toMap().value(QStringLiteral("type")).toString() == QStringLiteral("Clip")) + sawClip = true; + } + QVERIFY(sawClip); + + scene.setDemoRunning(true); + scene.stepDemoFrame(); + const QRect clip(40, 50, 280, 300); + const QRect damage = damageBoundingRect(scene.damageRects()); + QVERIFY(!damage.isEmpty()); + QVERIFY(clip.contains(damage)); +} + +void tst_Demo::addClipCreatesClipNode() +{ + DemoScene scene; + scene.addClip(); + QVERIFY(scene.selectedProps().value(QStringLiteral("isClip")).toBool()); + QCOMPARE(scene.selectedProps().value(QStringLiteral("type")).toString(), + QStringLiteral("Clip")); + QCOMPARE(scene.selectedProps().value(QStringLiteral("w")).toReal(), 280.0); + QCOMPARE(scene.selectedProps().value(QStringLiteral("h")).toReal(), 200.0); +} + +QTEST_MAIN(tst_Demo) +#include "tst_wdamagegraph_demo.moc" diff --git a/waylib/tests/unit_tests/test_wsgdamage/CMakeLists.txt b/waylib/tests/unit_tests/test_wsgdamage/CMakeLists.txt new file mode 100644 index 0000000000..4ac3ae67bf --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +find_package(Qt6 REQUIRED COMPONENTS Test Quick) +find_package(X11) + + +add_executable(test_wsgdamage + main.cpp + scenetest.cpp +) + +target_link_libraries(test_wsgdamage + PRIVATE + Waylib::WaylibServer + Qt::Test + Qt::Quick + Qt6::QuickPrivate +) +if (X11_FOUND) + target_link_libraries(test_wsgdamage PRIVATE X11::X11) + target_compile_definitions(test_wsgdamage PRIVATE TEST_HAS_X11) +endif() + +target_include_directories(test_wsgdamage PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/server/qtquick/scenegraph" +) + +add_test(NAME test_wsgdamage COMMAND test_wsgdamage) + +set_tests_properties(test_wsgdamage PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=rhi" + TIMEOUT 120 +) diff --git a/waylib/tests/unit_tests/test_wsgdamage/main.cpp b/waylib/tests/unit_tests/test_wsgdamage/main.cpp new file mode 100644 index 0000000000..d70f3d06f3 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage/main.cpp @@ -0,0 +1,267 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wregionhighlightitem.h" +#include "wsgcontext_p.h" +#include "wrenderbuffernode_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +WAYLIB_SERVER_USE_NAMESPACE + +class WSGDamageTrackerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void renderBuffer_needsRender_skipsIdleUnlessPendingHits(); + void highlight_recordsContentUntilFade(); + void highlight_newerOverlapRemovesOlder(); + void highlight_olderFrameIsGreener(); +}; + +void WSGDamageTrackerTest::renderBuffer_needsRender_skipsIdleUnlessPendingHits() +{ + QQuickItem item; + item.setSize(QSizeF(100, 40)); + std::unique_ptr node(WRenderBufferNode::createSoftwareNode(&item)); + node->resize(item.size()); + + node->applyFrame(WPixmanRegion(), QRect(0, 0, 100, 40), QMatrix4x4()); + QVERIFY(!node->needsSourceCopy()); + QVERIFY(!node->needsRender(WPixmanRegion(), false)); + QVERIFY(node->needsRender(WPixmanRegion(QRect(0, 0, 100, 40)), false)); + QVERIFY(node->needsRender(WPixmanRegion(QRect(50, 0, 10, 10)), false)); + QVERIFY(!node->needsRender(WPixmanRegion(QRect(200, 0, 10, 10)), false)); + QVERIFY(node->needsRender(WPixmanRegion(), true)); + + node->setDamageExpansion(8); + node->setClipDamageExpansion(false); + QVERIFY(node->needsRender(WPixmanRegion(QRect(-4, 0, 2, 2)), false)); + node->setClipDamageExpansion(true); + QVERIFY(!node->needsRender(WPixmanRegion(QRect(-4, 0, 2, 2)), false)); + + node->applyFrame(WPixmanRegion(QRect(0, 0, 100, 40)), QRect(0, 0, 100, 40), QMatrix4x4()); + QVERIFY(node->needsSourceCopy()); + QVERIFY(node->needsRender(WPixmanRegion(), false)); +} + +void WSGDamageTrackerTest::highlight_recordsContentUntilFade() +{ + WRegionOverlay overlay; + overlay.addFrame(WPixmanRegion(QRect(0, 0, 10, 10)), false, 0); + QCOMPARE(overlay.entries().size(), 1); + QVERIFY(overlay.needsAnotherFrame()); + + overlay.addFrame(WPixmanRegion(), false, WRegionOverlay::fadeOutMs - 1); + QCOMPARE(overlay.entries().size(), 1); + + overlay.addFrame(WPixmanRegion(), false, WRegionOverlay::fadeOutMs); + QVERIFY(overlay.entries().isEmpty()); + QVERIFY(!overlay.needsAnotherFrame()); +} + +void WSGDamageTrackerTest::highlight_newerOverlapRemovesOlder() +{ + WRegionOverlay debug; + debug.addFrame(WPixmanRegion(QRect(0, 0, 20, 20)), false, 0); + debug.addFrame(WPixmanRegion(QRect(0, 0, 10, 10)), false, 10); + + QCOMPARE(debug.entries().size(), 2); + QCOMPARE(debug.entries().at(0).region, WPixmanRegion(QRect(0, 0, 10, 10))); + QCOMPARE(debug.entries().at(1).region, WPixmanRegion(QRect(0, 0, 20, 20)) - QRect(0, 0, 10, 10)); +} + + +void WSGDamageTrackerTest::highlight_olderFrameIsGreener() +{ + QList entries; + WRegionOverlay::Entry newer; + newer.region = WPixmanRegion(QRect(2, 2, 12, 12)); + newer.whenMs = 10; + WRegionOverlay::Entry older; + older.region = WPixmanRegion(QRect(22, 2, 12, 12)); + older.whenMs = 0; + entries << newer << older; + + QImage img(40, 20, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + QPainter painter(&img); + WRegionOverlay::paint(&painter, QTransform(), entries, 10); + painter.end(); + + const QRgb newPx = img.pixel(8, 8); + const QRgb oldPx = img.pixel(28, 8); + QVERIFY(qRed(newPx) > qGreen(newPx)); + QVERIFY(qGreen(oldPx) > qRed(oldPx)); + QVERIFY(qRed(newPx) > qRed(oldPx)); + QVERIFY(qGreen(oldPx) > qGreen(newPx)); +} + +QStringList damageSceneTestFunctionNames(); + +namespace { + +QSet testSlots(const QMetaObject *meta) +{ + QSet names; + for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) { + const QMetaMethod method = meta->method(i); + if (method.methodType() == QMetaMethod::Slot) + names.insert(QString::fromLatin1(method.name())); + } + return names; +} + +// QTest options that consume the following argv element as a value. +bool optionTakesValue(const QString &option) +{ + static const QSet valued{ + QStringLiteral("-o"), QStringLiteral("-outdir"), + QStringLiteral("-eventdelay"), QStringLiteral("-keydelay"), + QStringLiteral("-mousedelay"), QStringLiteral("-maxwarnings"), + QStringLiteral("-randomseed"), + }; + return valued.contains(option); +} + +struct SuiteArgs +{ + std::vector storage; + std::vector argv; +}; + +SuiteArgs suiteArgs(const QString &program, const QStringList &qtOptions, + const QStringList &functions) +{ + SuiteArgs out; + out.storage.reserve(1 + qtOptions.size() + functions.size()); + out.storage.push_back(program.toLocal8Bit()); + for (const QString &option : qtOptions) + out.storage.push_back(option.toLocal8Bit()); + for (const QString &function : functions) + out.storage.push_back(function.toLocal8Bit()); + out.argv.reserve(out.storage.size()); + for (QByteArray &blob : out.storage) + out.argv.push_back(blob.data()); + return out; +} + +} // namespace + +int runDamageSceneTests(int argc, char **argv); + +int runDamageTests(int argc, char **argv) +{ + qputenv("QT_QPA_PLATFORM", "offscreen"); + qputenv("QT_QUICK_BACKEND", "rhi"); + if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) + qputenv("QSG_RHI_BACKEND", "null"); + QGuiApplication app(argc, argv); + WSGContext::ensureInstalled(); + + // Split CLI selectors per suite: forwarding every positional filter to + // both qExec calls makes the non-owning suite fail with "Unknown test + // function". Qt options go to both; function names only to their owner. + QStringList qtOptions; + QStringList positionals; + const QStringList cli = app.arguments().mid(1); + for (int i = 0; i < cli.size(); ++i) { + if (cli.at(i).startsWith(QLatin1Char('-'))) { + qtOptions << cli.at(i); + if (optionTakesValue(cli.at(i)) && i + 1 < cli.size()) + qtOptions << cli.at(++i); + } else { + positionals << cli.at(i); + } + } + const QSet trackerSlots = testSlots(&WSGDamageTrackerTest::staticMetaObject); + const QStringList sceneNames = damageSceneTestFunctionNames(); + const QSet sceneSlots(sceneNames.begin(), sceneNames.end()); + QStringList trackerFunctions; + QStringList sceneFunctions; + bool trackerOwns = false; + bool sceneOwns = false; + bool anyUnowned = false; + for (const QString &function : positionals) { + const bool inTracker = trackerSlots.contains(function); + const bool inScene = sceneSlots.contains(function); + if (inTracker && !inScene) { + trackerFunctions << function; + trackerOwns = true; + } else if (inScene && !inTracker) { + sceneFunctions << function; + sceneOwns = true; + } else { + // Shared (init/cleanup) or unknown: both suites see it, exactly + // like the old shared-argv behavior. + trackerFunctions << function; + sceneFunctions << function; + if (!inTracker && !inScene) + anyUnowned = true; + } + } + + // A selector owned by one suite must not run the other suite's full set. + const bool runTracker = positionals.isEmpty() || trackerOwns || anyUnowned; + const bool runScene = positionals.isEmpty() || sceneOwns || anyUnowned; + const QString program = app.arguments().constFirst(); + int status = 0; + if (runTracker) { + WSGDamageTrackerTest tracker; + SuiteArgs args = suiteArgs(program, qtOptions, trackerFunctions); + status |= QTest::qExec(&tracker, int(args.argv.size()), args.argv.data()); + } + if (runScene) { + SuiteArgs args = suiteArgs(program, qtOptions, sceneFunctions); + status |= runDamageSceneTests(int(args.argv.size()), args.argv.data()); + } + return status; +} + +int main(int argc, char **argv) +{ + if (!qEnvironmentVariableIsSet("WSG_DAMAGE_TEST_CHILD") + && !qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) { + QCoreApplication app(argc, argv); + int status = 0; + const QStringList args = app.arguments().mid(1); + const char *backends[] = { "null", "opengl", "vulkan" }; + for (const char *backend : backends) { + QProcess process; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QSG_RHI_BACKEND"), QLatin1String(backend)); + env.insert(QStringLiteral("WSG_DAMAGE_TEST_CHILD"), QStringLiteral("1")); + process.setProcessEnvironment(env); + process.setProcessChannelMode(QProcess::ForwardedChannels); + fprintf(stderr, "\n===== QSG_RHI_BACKEND=%s =====\n", backend); + process.start(app.applicationFilePath(), args); + if (!process.waitForStarted(15000) || !process.waitForFinished(-1)) { + fprintf(stderr, "QSG_RHI_BACKEND=%s failed to run: %s\n", + backend, qPrintable(process.errorString())); + return 1; + } + const int code = process.exitStatus() == QProcess::NormalExit ? process.exitCode() : 1; + fprintf(stderr, "===== QSG_RHI_BACKEND=%s exit %d =====\n", backend, code); + status |= code; + } + return status; + } + return runDamageTests(argc, argv); +} + +#include "main.moc" diff --git a/waylib/tests/unit_tests/test_wsgdamage/scenetest.cpp b/waylib/tests/unit_tests/test_wsgdamage/scenetest.cpp new file mode 100644 index 0000000000..feb9ec3a98 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage/scenetest.cpp @@ -0,0 +1,695 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wsgbatchrenderer_p.h" +#include "wsgcontext_p.h" +#include "wsgdamagelog_p.h" +#include "wsgdamagetracker_p.h" +#include "wsgviewport_p.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef TEST_HAS_X11 +#include +#endif + +WAYLIB_SERVER_USE_NAMESPACE + +namespace { + +constexpr int kSceneWidth = 800; +constexpr int kSceneHeight = 480; +constexpr int kBoundSlack = 16; + +static void applySceneDamage(WSGBatchRenderer::Renderer *batch, + const QRect &sceneOutputRect, + const WPixmanRegion &extraGpu = {}) +{ + WPixmanRegion scene; + auto *tracker = batch->damageTracker(); + if (tracker) { + auto viewport = std::make_shared(); + tracker->addViewport(viewport); + tracker->commit(); + scene = viewport->damageRegion().region; + viewport->finishFrame(); + tracker->removeViewport(viewport.get()); + if (!sceneOutputRect.isEmpty()) + scene &= sceneOutputRect; + } + WPixmanRegion gpu = scene; + gpu += extraGpu; + const auto coversOutput = [&](const WPixmanRegion &r) { + return !sceneOutputRect.isEmpty() && (WPixmanRegion(sceneOutputRect) - r).isEmpty(); + }; + const WDamageRegion flushDamage = + coversOutput(scene) ? WDamageRegion(true) : WDamageRegion(scene, false); + const WDamageRegion renderDamage = + coversOutput(gpu) ? WDamageRegion(true) : WDamageRegion(gpu, false); + batch->setDamage(flushDamage, renderDamage, sceneOutputRect); +} + +const char *kSceneQml = R"QML( +import QtQuick + +Item { + id: root + width: 800 + height: 480 + clip: true + + property alias wallpaper: wallpaper + property alias sentinel: sentinel + property alias target: target + property alias field: field + property alias cursor: cursor + property alias btnA: btnA + property alias btnB: btnB + property var spawned: null + + Rectangle { + id: wallpaper + objectName: "wallpaper" + anchors.fill: parent + color: "#202020" + } + + Rectangle { + id: sentinel + objectName: "sentinel" + x: 700; y: 20; width: 80; height: 40 + color: "#3366aa" + } + + Rectangle { + id: target + objectName: "target" + x: 80; y: 80; width: 48; height: 48 + color: "#cc3333" + transformOrigin: Item.Center + } + + Rectangle { + id: field + objectName: "field" + x: 200; y: 200; width: 220; height: 30 + color: "#404040" + Rectangle { + id: cursor + objectName: "cursor" + x: 8; y: 6; width: 1; height: 18 + color: "white" + } + } + + Rectangle { + id: btnA + objectName: "btnA" + x: 640; y: 420; width: 36; height: 36 + color: "#555555" + radius: 8 + } + Rectangle { + id: btnB + objectName: "btnB" + x: 684; y: 420; width: 36; height: 36 + color: "#555555" + radius: 8 + } + + function spawnItem() { + spawned = Qt.createQmlObject( + 'import QtQuick; Rectangle { objectName: "spawned"; x: 400; y: 60; width: 40; height: 40; color: "#33cc66" }', + root) + return spawned + } + + function despawnItem() { + if (spawned) { + spawned.destroy() + spawned = null + } + } + + function translateTarget(dx, dy) { + var t = Qt.createQmlObject( + 'import QtQuick; Translate { x: ' + dx + '; y: ' + dy + ' }', + target) + target.transform = [t] + } + + function resetScene() { + wallpaper.visible = true + target.opacity = 1 + target.x = 80 + target.y = 80 + target.rotation = 0 + target.scale = 1 + target.color = "#cc3333" + if (target.transform.length) + target.transform = [] + cursor.opacity = 1 + despawnItem() + } +} +)QML"; + +struct Snapshot +{ + bool valid = false; + bool full = false; + WPixmanRegion region; + + QString dump() const + { + if (!valid) + return QStringLiteral("invalid-renderer"); + return WSGDamageLog::describe(region, full); + } + + int area() const + { + int total = 0; + int count = 0; + const pixman_box32_t *boxes = region.rectangles(&count); + for (int i = 0; i < count && boxes; ++i) + total += (boxes[i].x2 - boxes[i].x1) * (boxes[i].y2 - boxes[i].y1); + return total; + } +}; + +QRect sceneRect(QQuickItem *item) +{ + if (!item) + return { }; + return item->mapRectToScene(item->boundingRect()).toAlignedRect(); +} + +QRect inflatedUnion(const QRect &a, const QRect &b, int slack) +{ + return a.united(b).adjusted(-slack, -slack, slack, slack); +} + +} // namespace + +class WSGDamageSceneTest : public QObject +{ + Q_OBJECT + +private: + QQuickItem *named(const char *name) const + { + return m_root ? m_root->findChild(QLatin1String(name)) : nullptr; + } + + WSGBatchRenderer::Renderer *renderer() const + { + if (!m_window) + return nullptr; + return dynamic_cast( + QQuickWindowPrivate::get(m_window.get())->renderer); + } + + Snapshot take() + { + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + m_rc->polishItems(); + m_rc->beginFrame(); + m_rc->sync(); + + Snapshot snap; + auto *batch = renderer(); + if (batch) { + if (batch->deviceRect().isEmpty()) + batch->setDeviceRect(QRect(0, 0, kSceneWidth, kSceneHeight)); + applySceneDamage(batch, QRect(0, 0, kSceneWidth, kSceneHeight)); + snap.valid = true; + snap.full = batch->lastRenderRegion().isFull; + snap.region = batch->lastRenderRegion().region; + } + m_rc->endFrame(); + return snap; + } + + void expectValid(const Snapshot &snap, const char *what) + { + QVERIFY2(snap.valid, + qPrintable(QStringLiteral("%1: %2").arg(QLatin1String(what), snap.dump()))); + } + + void expectNotFull(const Snapshot &snap, const char *what) + { + expectValid(snap, what); + QVERIFY2(!snap.full, + qPrintable(QStringLiteral("%1: expected partial damage, got %2") + .arg(QLatin1String(what), snap.dump()))); + } + + void expectCovers(const Snapshot &snap, const QRect &rect, const char *what) + { + expectNotFull(snap, what); + if (rect.isEmpty()) + return; + QRect probe = rect.adjusted(1, 1, -1, -1); + if (probe.isEmpty()) + probe = rect; + QVERIFY2((WPixmanRegion(probe) - snap.region).isEmpty(), + qPrintable(QStringLiteral("%1: damage %2 missed %3,%4 %5x%6") + .arg(QLatin1String(what), snap.dump()) + .arg(rect.x()) + .arg(rect.y()) + .arg(rect.width()) + .arg(rect.height()))); + } + + void expectAvoids(const Snapshot &snap, const QRect &rect, const char *what) + { + expectNotFull(snap, what); + QVERIFY2(!snap.region.intersects(rect), + qPrintable(QStringLiteral("%1: damage %2 leaked into %3,%4 %5x%6") + .arg(QLatin1String(what), snap.dump()) + .arg(rect.x()) + .arg(rect.y()) + .arg(rect.width()) + .arg(rect.height()))); + } + + void expectBoundedBy(const Snapshot &snap, const QRect &box, const char *what) + { + expectNotFull(snap, what); + const QRect allowed = box.adjusted(-kBoundSlack, -kBoundSlack, kBoundSlack, kBoundSlack); + const QRect bound = snap.region.boundingRect(); + QVERIFY2( + allowed.contains(bound), + qPrintable(QStringLiteral("%1: bounding %2,%3 %4x%5 outside %6,%7 %8x%9 (damage %10)") + .arg(QLatin1String(what)) + .arg(bound.x()) + .arg(bound.y()) + .arg(bound.width()) + .arg(bound.height()) + .arg(allowed.x()) + .arg(allowed.y()) + .arg(allowed.width()) + .arg(allowed.height()) + .arg(snap.dump()))); + } + + std::unique_ptr m_rc; + std::unique_ptr m_vulkan; + std::unique_ptr m_window; + std::unique_ptr m_engine; + std::unique_ptr m_tex; + std::unique_ptr m_ds; + std::unique_ptr m_rt; + std::unique_ptr m_rp; + QQuickItem *m_root = nullptr; + + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void init(); + + void moveItem_unionsOldAndNew(); + void addItem_damagesOnlyNewBounds(); + void removeItem_damagesOnlyOldBounds(); + void rotateItem_unionsOldAndNew(); + void scaleItem_unionsOldAndNew(); + void transformItem_unionsOldAndNew(); + void caretBlink_doesNotCoverButtons(); + void idle_isEmptyOrTiny(); + void trackingDisabled_reportsFull(); + void viewportBufferDamage_onIdleTree(); + void rectangleColorChange_damagesOnlyTarget(); + void subtreeAggregation_damagesWholeBounds(); +}; + +void WSGDamageSceneTest::initTestCase() +{ + WSGContext::ensureInstalled(); + + // The vulkan child needs an explicit instance attached before the render + // control initializes; skip only when creation itself fails. + if (qgetenv("QSG_RHI_BACKEND") == "vulkan") { + m_vulkan = std::make_unique(); + if (!m_vulkan->create()) + QSKIP("QVulkanInstance::create() failed, skipping the Vulkan child"); + } + + m_rc = std::make_unique(); + m_window = std::make_unique(m_rc.get()); + m_window->setColor(QColor(32, 32, 32)); + if (m_vulkan) + m_window->setVulkanInstance(m_vulkan.get()); + m_window->resize(kSceneWidth, kSceneHeight); + m_window->contentItem()->setSize(QSizeF(kSceneWidth, kSceneHeight)); + +#ifdef TEST_HAS_X11 + // Broken GLX (BadValue on GLXCreateNewContext) aborts via the default + // X handler instead of failing QRhi. Ignore so initialize() can QSKIP. + if (qgetenv("QSG_RHI_BACKEND") == "opengl") + XSetErrorHandler([](Display *, XErrorEvent *) { return 0; }); +#endif + + if (!m_rc->initialize()) { + qWarning("QQuickRenderControl::initialize() failed (api=%d)", + int(m_window->rendererInterface() ? m_window->rendererInterface()->graphicsApi() + : QSGRendererInterface::Unknown)); + QSKIP("QQuickRenderControl::initialize() failed"); + } + + QRhi *rhi = m_rc->rhi(); + if (!rhi) + QSKIP("QQuickRenderControl has no QRhi"); + + const QSize pixelSize(kSceneWidth, kSceneHeight); + m_tex.reset(rhi->newTexture(QRhiTexture::RGBA8, pixelSize, 1, QRhiTexture::RenderTarget)); + if (!m_tex || !m_tex->create()) + QSKIP("Could not create offscreen color texture"); + m_ds.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, 1)); + if (!m_ds || !m_ds->create()) + QSKIP("Could not create offscreen depth buffer"); + QRhiTextureRenderTargetDescription rtDesc(QRhiColorAttachment(m_tex.get())); + rtDesc.setDepthStencilBuffer(m_ds.get()); + m_rt.reset(rhi->newTextureRenderTarget(rtDesc)); + if (!m_rt) + QSKIP("Could not create offscreen render target"); + m_rp.reset(m_rt->newCompatibleRenderPassDescriptor()); + m_rt->setRenderPassDescriptor(m_rp.get()); + if (!m_rt->create()) + QSKIP("Could not create offscreen render pass"); + m_window->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(m_rt.get())); + + m_engine = std::make_unique(); + QQmlComponent component(m_engine.get()); + component.setData(QByteArray(kSceneQml), QUrl(QStringLiteral("qrc:/wsgdamage/scene.qml"))); + QObject *obj = component.create(); + if (!obj) { + const QString errors = component.errorString(); + QFAIL(qPrintable(QStringLiteral("failed to create scene QML: %1").arg(errors))); + } + m_root = qobject_cast(obj); + QVERIFY(m_root); + m_root->setParentItem(m_window->contentItem()); + m_root->setSize(QSizeF(kSceneWidth, kSceneHeight)); + + for (int i = 0; i < 3; ++i) + take(); + + if (!renderer()) + QSKIP("WSGBatchRenderer was not installed for this scene graph"); +} + +void WSGDamageSceneTest::cleanupTestCase() +{ + if (m_root) { + m_root->setParentItem(nullptr); + delete m_root; + m_root = nullptr; + } + if (m_window) + m_window->setRenderTarget(QQuickRenderTarget()); + m_rt.reset(); + m_rp.reset(); + m_ds.reset(); + m_tex.reset(); + m_engine.reset(); + m_window.reset(); + m_rc.reset(); +} + +void WSGDamageSceneTest::init() +{ + QVERIFY(m_root); + QVERIFY(QMetaObject::invokeMethod(m_root, "resetScene", Qt::DirectConnection)); + take(); +} + +void WSGDamageSceneTest::addItem_damagesOnlyNewBounds() +{ + QVERIFY(QMetaObject::invokeMethod(m_root, "spawnItem", Qt::DirectConnection)); + auto *spawned = named("spawned"); + QVERIFY(spawned); + const QRect now = sceneRect(spawned); + const Snapshot snap = take(); + + expectCovers(snap, now, "add"); + expectAvoids(snap, sceneRect(named("sentinel")), "add sentinel"); + expectAvoids(snap, sceneRect(named("btnA")), "add button"); + expectBoundedBy(snap, now, "add"); +} + +void WSGDamageSceneTest::removeItem_damagesOnlyOldBounds() +{ + QVERIFY(QMetaObject::invokeMethod(m_root, "spawnItem", Qt::DirectConnection)); + auto *spawned = named("spawned"); + QVERIFY(spawned); + const QRect oldRect = sceneRect(spawned); + take(); + + QVERIFY(QMetaObject::invokeMethod(m_root, "despawnItem", Qt::DirectConnection)); + const Snapshot snap = take(); + + expectCovers(snap, oldRect, "remove"); + expectAvoids(snap, sceneRect(named("sentinel")), "remove sentinel"); + expectAvoids(snap, sceneRect(named("btnA")), "remove button"); + expectBoundedBy(snap, oldRect, "remove"); +} + +void WSGDamageSceneTest::moveItem_unionsOldAndNew() +{ + auto *target = named("target"); + QVERIFY(target); + const QRect oldRect = sceneRect(target); + target->setX(target->x() + 80); + target->setY(target->y() + 40); + const QRect now = sceneRect(target); + const Snapshot snap = take(); + + expectCovers(snap, oldRect, "move old"); + expectCovers(snap, now, "move new"); + expectAvoids(snap, sceneRect(named("sentinel")), "move sentinel"); + expectBoundedBy(snap, inflatedUnion(oldRect, now, 0), "move"); +} + +void WSGDamageSceneTest::rotateItem_unionsOldAndNew() +{ + auto *target = named("target"); + QVERIFY(target); + const QRect oldRect = sceneRect(target); + target->setRotation(45); + const QRect now = sceneRect(target); + const Snapshot snap = take(); + + expectCovers(snap, oldRect, "rotate old"); + expectCovers(snap, now, "rotate new"); + expectAvoids(snap, sceneRect(named("sentinel")), "rotate sentinel"); + expectAvoids(snap, sceneRect(named("btnA")), "rotate button"); + expectBoundedBy(snap, inflatedUnion(oldRect, now, 0), "rotate"); +} + +void WSGDamageSceneTest::scaleItem_unionsOldAndNew() +{ + auto *target = named("target"); + QVERIFY(target); + const QRect oldRect = sceneRect(target); + target->setScale(2); + const QRect now = sceneRect(target); + const Snapshot snap = take(); + + expectCovers(snap, oldRect, "scale old"); + expectCovers(snap, now, "scale new"); + expectAvoids(snap, sceneRect(named("sentinel")), "scale sentinel"); + expectAvoids(snap, sceneRect(named("btnA")), "scale button"); + expectBoundedBy(snap, inflatedUnion(oldRect, now, 0), "scale"); +} + +void WSGDamageSceneTest::transformItem_unionsOldAndNew() +{ + auto *target = named("target"); + QVERIFY(target); + const QRect oldRect = sceneRect(target); + QVERIFY(QMetaObject::invokeMethod(m_root, + "translateTarget", + Qt::DirectConnection, + Q_ARG(QVariant, 40), + Q_ARG(QVariant, 30))); + const QRect now = sceneRect(target); + const Snapshot snap = take(); + + expectCovers(snap, oldRect, "transform old"); + expectCovers(snap, now, "transform new"); + expectAvoids(snap, sceneRect(named("sentinel")), "transform sentinel"); + expectAvoids(snap, sceneRect(named("btnA")), "transform button"); + expectBoundedBy(snap, inflatedUnion(oldRect, now, 0), "transform"); +} + +void WSGDamageSceneTest::caretBlink_doesNotCoverButtons() +{ + auto *cursor = named("cursor"); + QVERIFY(cursor); + cursor->setOpacity(cursor->opacity() > 0.5 ? 0.0 : 1.0); + const Snapshot snap = take(); + + expectNotFull(snap, "caret"); + expectCovers(snap, sceneRect(cursor), "caret"); + expectAvoids(snap, sceneRect(named("btnA")), "caret button A"); + expectAvoids(snap, sceneRect(named("btnB")), "caret button B"); + expectAvoids(snap, sceneRect(named("sentinel")), "caret sentinel"); + QVERIFY2( + snap.area() <= 220 * 30 * 2, + qPrintable( + QStringLiteral("caret: area %1 too large (%2)").arg(snap.area()).arg(snap.dump()))); +} + +void WSGDamageSceneTest::idle_isEmptyOrTiny() +{ + const Snapshot snap = take(); + expectValid(snap, "idle"); + QVERIFY2(!snap.full, qPrintable(QStringLiteral("idle: full damage %1").arg(snap.dump()))); + QVERIFY2(snap.area() <= 64, + qPrintable(QStringLiteral("idle: unexpected damage %1 area=%2") + .arg(snap.dump()) + .arg(snap.area()))); +} + +void WSGDamageSceneTest::trackingDisabled_reportsFull() +{ + auto *batch = renderer(); + QVERIFY(batch); + const auto was = batch->damageMode(); + batch->setDamageMode(WSGBatchRenderer::Renderer::DamageMode::Off); + const QScopeGuard restore([batch, was] { + batch->setDamageMode(was); + }); + + QVERIFY(!batch->damageTracker()); + auto *target = named("target"); + QVERIFY(target); + target->setX(target->x() + 12); + const Snapshot snap = take(); + expectValid(snap, "tracking off"); + QVERIFY2( + snap.full, + qPrintable(QStringLiteral("tracking off: expected full damage, got %1").arg(snap.dump()))); + + batch->setDamageMode(was); + QVERIFY(batch->damageTracker()); + const Snapshot resumed = take(); + expectValid(resumed, "tracking resumed"); + QVERIFY2(resumed.full, + qPrintable(QStringLiteral("tracking resumed: expected full damage, got %1") + .arg(resumed.dump()))); +} + +void WSGDamageSceneTest::viewportBufferDamage_onIdleTree() +{ + const Snapshot idle = take(); + expectValid(idle, "viewport damage settle"); + QVERIFY2(!idle.full, qPrintable(QStringLiteral("settle was full: %1").arg(idle.dump()))); + + auto *batch = renderer(); + QVERIFY(batch); + const QRect bufferRect(50, 50, 40, 40); + applySceneDamage(batch, QRect(0, 0, kSceneWidth, kSceneHeight), WPixmanRegion(bufferRect)); + + Snapshot snap; + snap.valid = true; + snap.full = batch->lastRenderRegion().isFull; + snap.region = batch->lastRenderRegion().region; + expectCovers(snap, bufferRect, "viewport buffer damage"); + QVERIFY2(!batch->lastFlushRegion().isFull, + "swapchain buffer damage must not mark the ring full"); + QVERIFY2(batch->lastFlushRegion().region.isEmpty(), + qPrintable(QStringLiteral("flush leaked buffer damage: %1") + .arg(WSGDamageLog::describe(batch->lastFlushRegion().region, false)))); +} + +void WSGDamageSceneTest::rectangleColorChange_damagesOnlyTarget() +{ + auto *target = named("target"); + QVERIFY(target); + target->setProperty("color", QColor("#22aa66")); + const Snapshot snap = take(); + + expectCovers(snap, sceneRect(target), "color change"); + expectAvoids(snap, sceneRect(named("sentinel")), "color sentinel"); + expectAvoids(snap, sceneRect(named("btnA")), "color button"); +} + +void WSGDamageSceneTest::subtreeAggregation_damagesWholeBounds() +{ + auto *field = named("field"); + auto *cursor = named("cursor"); + auto *batch = renderer(); + QVERIFY(field); + QVERIFY(cursor); + QVERIFY(batch); + + QSGNode *subtreeRoot = QQuickItemPrivate::get(field)->itemNode(); + QVERIFY(subtreeRoot); + QVERIFY(batch->setDamageSubtreeAggregation(subtreeRoot, true, field->boundingRect())); + bool aggregationEnabled = true; + const QScopeGuard restore([batch, subtreeRoot, &aggregationEnabled] { + if (aggregationEnabled) + batch->setDamageSubtreeAggregation(subtreeRoot, false); + }); + take(); + + cursor->setOpacity(0); + const Snapshot aggregated = take(); + const QRect fieldRect = sceneRect(field); + expectCovers(aggregated, fieldRect, "aggregate descendant content"); + expectAvoids(aggregated, sceneRect(named("sentinel")), "aggregate sentinel"); + expectAvoids(aggregated, sceneRect(named("btnA")), "aggregate button"); + expectBoundedBy(aggregated, fieldRect, "aggregate bounds"); + + QVERIFY(batch->setDamageSubtreeAggregation(subtreeRoot, false)); + aggregationEnabled = false; + take(); + + cursor->setOpacity(1); + const Snapshot exact = take(); + expectCovers(exact, sceneRect(cursor), "restored exact descendant"); + expectAvoids(exact, sceneRect(named("btnA")), "restored exact button"); + expectBoundedBy(exact, sceneRect(cursor), "restored exact bounds"); +} + + +QStringList damageSceneTestFunctionNames() +{ + QStringList names; + const QMetaObject *meta = &WSGDamageSceneTest::staticMetaObject; + for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) { + const QMetaMethod method = meta->method(i); + if (method.methodType() == QMetaMethod::Slot) + names << QString::fromLatin1(method.name()); + } + return names; +} + +int runDamageSceneTests(int argc, char **argv) +{ + WSGDamageSceneTest test; + return QTest::qExec(&test, argc, argv); +} + +#include "scenetest.moc" diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/CMakeLists.txt b/waylib/tests/unit_tests/test_wsgdamage_visual/CMakeLists.txt new file mode 100644 index 0000000000..7caa619b5a --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/CMakeLists.txt @@ -0,0 +1,113 @@ +# Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +# DamageDebug overlay visual test: headless wlroots output, dump the +# committed wlr_buffer to QImage, grab the QML scene without overlay, paint +# the same highlight from recent damage entries, and compare. + +find_package(Qt6 REQUIRED COMPONENTS Test Core Concurrent Qml Quick QuickControls2) + +if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10) + find_package(Qt6 REQUIRED COMPONENTS GuiPrivate QuickPrivate) +endif() +find_package(PkgConfig REQUIRED) +pkg_search_module(PIXMAN REQUIRED IMPORTED_TARGET pixman-1) +pkg_search_module(WAYLAND REQUIRED IMPORTED_TARGET wayland-server) +pkg_search_module(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) +pkg_check_modules(WAYLAND_PROTOCOLS REQUIRED wayland-protocols) +pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) +pkg_get_variable(WAYLAND_SCANNER_BIN wayland-scanner wayland_scanner) +if(NOT WAYLAND_SCANNER_BIN) + find_program(WAYLAND_SCANNER_BIN NAMES wayland-scanner REQUIRED) +endif() + +if(QT_KNOWN_POLICY_QTP0001) + qt_policy(SET QTP0001 NEW) +endif() +if(POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +set(XDG_SHELL_XML "${WAYLAND_PROTOCOLS_DATADIR}/stable/xdg-shell/xdg-shell.xml") +set(XDG_SHELL_CLIENT_H "${CMAKE_CURRENT_BINARY_DIR}/xdg-shell-client-protocol.h") +set(XDG_SHELL_CLIENT_C "${CMAKE_CURRENT_BINARY_DIR}/xdg-shell-client-protocol.c") +add_custom_command( + OUTPUT ${XDG_SHELL_CLIENT_H} ${XDG_SHELL_CLIENT_C} + COMMAND ${WAYLAND_SCANNER_BIN} client-header ${XDG_SHELL_XML} ${XDG_SHELL_CLIENT_H} + COMMAND ${WAYLAND_SCANNER_BIN} private-code ${XDG_SHELL_XML} ${XDG_SHELL_CLIENT_C} + DEPENDS ${XDG_SHELL_XML} + COMMENT "Generating xdg-shell client protocol for test_wsgdamage_visual" + VERBATIM +) + +add_executable(test_wsgdamage_visual + main.cpp + helper.cpp + damageclient.c + ${XDG_SHELL_CLIENT_C} +) +set_source_files_properties(damageclient.c ${XDG_SHELL_CLIENT_C} PROPERTIES + LANGUAGE C + SKIP_AUTOMOC ON + SKIP_AUTOUIC ON +) + +# Reuse the glass example photo instead of adding a second wallpaper. +set(WSG_DAMAGE_WALLPAPER + "${CMAKE_CURRENT_SOURCE_DIR}/../../../../examples/test_glass/assets/default-glass-background.jpg") +if(NOT EXISTS "${WSG_DAMAGE_WALLPAPER}") + message(FATAL_ERROR + "test_wsgdamage_visual needs examples/test_glass/assets/default-glass-background.jpg") +endif() +set_source_files_properties("${WSG_DAMAGE_WALLPAPER}" + PROPERTIES QT_RESOURCE_ALIAS "default-glass-background.jpg") + +qt_add_qml_module(test_wsgdamage_visual + URI WSGDamageVisual + VERSION "1.0" + QML_FILES + TestWindow.qml + DamageScene.qml + LockScene.qml + RoundBlur.qml + RESOURCES + "${WSG_DAMAGE_WALLPAPER}" + SOURCES + helper.h + IMPORTS + Waylib.Server +) + +target_compile_definitions(test_wsgdamage_visual + PRIVATE + WLR_USE_UNSTABLE + WAYLIB_QML_IMPORT_PATH="${PROJECT_BINARY_DIR}/src/server" +) + +target_include_directories(test_wsgdamage_visual PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_BINARY_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/server/qtquick" + "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/server/qtquick/private" + "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/server/qtquick/scenegraph" +) + +target_link_libraries(test_wsgdamage_visual + PRIVATE + Waylib::WaylibServer + Qt::Test + Qt::Concurrent + Qt::Qml + Qt::Quick + Qt::QuickControls2 + Qt6::QuickPrivate + PkgConfig::PIXMAN + PkgConfig::WAYLAND + PkgConfig::WAYLAND_CLIENT +) + +add_test(NAME test_wsgdamage_visual COMMAND test_wsgdamage_visual) +set_tests_properties(test_wsgdamage_visual PROPERTIES + ENVIRONMENT "WLR_BACKENDS=headless;QT_QML_IMPORT_PATH=${PROJECT_BINARY_DIR}/src/server;QT_QUICK_CONTROLS_STYLE=Basic" + TIMEOUT 1200 +) diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/DamageScene.qml b/waylib/tests/unit_tests/test_wsgdamage_visual/DamageScene.qml new file mode 100644 index 0000000000..89c59893d7 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/DamageScene.qml @@ -0,0 +1,238 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Effects +import QtQuick.Controls +import Waylib.Server + +Item { + id: root + anchors.fill: parent + + readonly property int shadowPad: 20 + readonly property int blurPad: 16 + property alias target: target + property alias sentinel: sentinel + property alias blurPanel: blurPanel + property alias frostGlass: frostGlass + property alias frostBehind: frostBehind + property alias lockScene: lockScene + + // Same photo as examples/test_glass, not a flat color or star grid. + Image { + id: wallpaper + objectName: "wallpaper" + anchors.fill: parent + source: "default-glass-background.jpg" + fillMode: Image.PreserveAspectCrop + asynchronous: false + cache: true + } + + // Matches waylib/examples/blur: copy the already-drawn output (photo + + // frostBehind) and MultiEffect-blur that texture. Off by default so + // dump-vs-grabToImage tests are not comparing a compositor blit. + Image { + id: frostBehind + objectName: "frostBehind" + visible: frostGlass.visible + x: 520 + y: 240 + width: 80 + height: 80 + source: "default-glass-background.jpg" + fillMode: Image.PreserveAspectCrop + asynchronous: false + } + + RenderBufferBlitter { + id: frostGlass + objectName: "frostGlass" + visible: false + x: 500 + y: 220 + width: 200 + height: 160 + + MultiEffect { + anchors.fill: parent + source: frostGlass.content + autoPaddingEnabled: false + blurEnabled: true + blur: 1.0 + blurMax: 64 + saturation: 0.2 + } + } + + Item { + id: blurPanel + objectName: "blurPanel" + x: 430 + y: 70 + width: 170 + height: 110 + + Image { + id: blurSource + anchors.fill: parent + visible: false + source: "default-glass-background.jpg" + fillMode: Image.PreserveAspectCrop + asynchronous: false + } + + MultiEffect { + anchors.fill: parent + source: blurSource + autoPaddingEnabled: true + blurEnabled: true + blur: 0.7 + blurMax: 16 + } + + Rectangle { + anchors.fill: parent + color: "#44ffffff" + border.color: "#88999999" + radius: 8 + } + } + + Rectangle { + id: sentinel + objectName: "sentinel" + x: 700 + y: 20 + width: 80 + height: 40 + color: "#3366aa" + radius: 4 + } + + Item { + id: target + objectName: "target" + x: 80 + y: 80 + width: 80 + height: 80 + + Rectangle { + id: card + anchors.fill: parent + radius: 10 + color: "#cc3333" + visible: false + layer.enabled: true + } + + MultiEffect { + anchors.fill: parent + source: card + autoPaddingEnabled: true + shadowEnabled: true + shadowBlur: 0.8 + shadowColor: "#99000000" + shadowVerticalOffset: 6 + blurMax: 20 + } + + Rectangle { + anchors.fill: parent + radius: 10 + color: "#cc3333" + border.color: "#882222" + border.width: 1 + } + } + + Popup { + id: testPopup + objectName: "testPopup" + modal: false + dim: false + padding: 0 + x: 220 + y: 250 + width: 160 + height: 80 + + Rectangle { + objectName: "popupPanel" + implicitWidth: 160 + implicitHeight: 80 + color: "#227744" + border.color: "#115522" + } + } + + function openPopup() { + testPopup.open() + } + + function closePopup() { + testPopup.close() + } + + LockScene { + id: lockScene + objectName: "lockScene" + visible: false + } + + function showLockscreen() { + target.visible = false + blurPanel.visible = false + frostGlass.visible = false + lockScene.visible = true + // Settle chip geometry in the frame it changes: the 150ms expand + // Behaviors must not race damage frame capture. + lockScene.testMode = true + lockScene.resetLock() + lockScene.passwordField.forceActiveFocus() + } + + function openSessionPopup() { + lockScene.openSessionPopup() + } + + function closeSessionPopup() { + lockScene.closeSessionPopup() + } + + function scrollSessionList(dy) { + lockScene.scrollSessionList(dy) + } + + function blinkLockCaret() { + lockScene.blinkCaret() + } + + function startLockHideSource() { + lockScene.startHideSource() + } + + function stopLockHideSource() { + lockScene.stopHideSource() + } + + function resetScene() { + target.x = 80 + target.y = 80 + target.rotation = 0 + target.scale = 1 + target.visible = true + blurPanel.x = 430 + blurPanel.y = 70 + blurPanel.visible = true + frostBehind.x = 520 + frostBehind.y = 240 + frostGlass.visible = false + testPopup.close() + lockScene.testMode = true + lockScene.resetLock() + lockScene.visible = false + } +} diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/LockScene.qml b/waylib/tests/unit_tests/test_wsgdamage_visual/LockScene.qml new file mode 100644 index 0000000000..52bd93130d --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/LockScene.qml @@ -0,0 +1,462 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Controls +import Waylib.Server + +// Replica of treeland lockscreen UserInput + ControlAction session chip. +// Qt Quick Controls + waylib RoundBlur (RenderBufferBlitter). No compositor +// plugin QML: those types are not available in this unit test. No DTK: +// waylib-only CI images do not ship org.deepin.dtk. +Item { + id: root + anchors.fill: parent + + readonly property alias passwordField: passwordField + readonly property alias passwordBlur: passwordBlur + readonly property Item caret: lockCaretItem + readonly property alias sessionButton: sessionButton + readonly property alias sessionPopup: sessionPopup + readonly property alias sessionList: sessionList + readonly property alias sessionGlass: sessionGlass + + property Item lockCaretItem + + // LoginAnimation replica: ShaderEffectSource { hideSource: true } + // renders lock content into a Qt RT that is not a wlr buffer. + property bool loginHideSource: false + + // Replica of DTK UserList/SessionList. Keep the panel in the output + // tree (not Qt Popup): Popup.Item + opacity/scale snapshots the subtree + // to a QSGLayer, so the 220x280 blitter never extra-QRhi copies. + property bool popupEnterAnimation: false + // Deterministic damage tests set testMode during setup so the 150ms + // chip expand Behaviors below never run: geometry settles in the frame + // it changes instead of racing frame capture. + property bool testMode: false + + Item { + id: lockContent + objectName: "lockContent" + anchors.fill: parent + + Rectangle { + id: behindPulse + objectName: "lockBehindPulse" + x: 48 + y: 96 + width: 160 + height: 160 + z: 0 + color: Qt.rgba(0.2, 0.6, 1.0, 0.45) + } + + // UserInput: 220x300, vertically centered in the right half. + Item { + id: userInput + objectName: "lockUserInput" + width: 220 + height: 300 + x: Math.round(root.width * 0.42) + y: Math.round((root.height - height) / 2) + + Column { + id: userCol + spacing: 15 + anchors.centerIn: parent + width: parent.width + + Rectangle { + objectName: "lockAvatar" + width: 120 + height: 120 + anchors.horizontalCenter: parent.horizontalCenter + radius: 20 + color: Qt.rgba(1, 1, 1, 0.12) + border.width: 2 + border.color: Qt.rgba(1, 1, 1, 0.1) + } + + Text { + objectName: "lockUsername" + text: "User" + font.bold: true + font.pixelSize: 20 + color: "white" + anchors.horizontalCenter: parent.horizontalCenter + } + + TextField { + id: passwordField + objectName: "lockPassword" + width: userInput.width + implicitWidth: userInput.width + height: 30 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: TextInput.AlignHCenter + echoMode: showPasswordBtn.hiddenPWD ? TextInput.Password : TextInput.Normal + rightPadding: 22 + leftPadding: 22 + maximumLength: 510 + placeholderText: qsTr("Password") + placeholderTextColor: Qt.rgba(1.0, 1.0, 1.0, 0.6) + color: palette.windowText + font.pixelSize: 12 + onContentWidthChanged: updateLeftPadding() + onWidthChanged: updateLeftPadding() + Component.onCompleted: updateLeftPadding() + + property bool capsIndicatorVisible: false + + function updateLeftPadding() { + var remaining = width - contentWidth - rightPadding + if (capsIndicator.visible) + leftPadding = rightPadding + else + leftPadding = Math.max(8, remaining > rightPadding ? rightPadding : remaining) + } + + // Copied from src/plugins/lockscreen/qml/UserInput.qml. + // Tests drive opacity; keep the 600ms timer off (interval 0). + cursorDelegate: Rectangle { + id: cursor + objectName: "lockCaret" + width: 1 + height: 18 + color: palette.windowText + visible: parent.activeFocus && !parent.readOnly + && parent.selectionStart === parent.selectionEnd + Component.onCompleted: root.lockCaretItem = cursor + + Connections { + target: cursor.parent + function onCursorPositionChanged() { + cursor.opacity = 1 + if (cursorTimer.interval != 0) + cursorTimer.restart() + } + } + + Timer { + id: cursorTimer + running: cursor.parent.activeFocus && !cursor.parent.readOnly && interval != 0 + repeat: true + interval: 0 + onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0 + onRunningChanged: cursor.opacity = 1 + } + } + + Item { + id: capsIndicator + height: parent.height + anchors { + left: parent.left + leftMargin: 3 + verticalCenter: parent.verticalCenter + } + visible: passwordField.capsIndicatorVisible + onVisibleChanged: passwordField.updateLeftPadding() + implicitWidth: 16 + implicitHeight: 16 + } + + Button { + id: showPasswordBtn + anchors { + right: parent.right + rightMargin: 3 + verticalCenter: parent.verticalCenter + } + property bool hiddenPWD: true + implicitWidth: 16 + implicitHeight: 16 + padding: 0 + hoverEnabled: true + display: AbstractButton.TextOnly + text: hiddenPWD ? "·" : "o" + + background: Rectangle { + anchors.fill: parent + radius: 4 + color: showPasswordBtn.hovered ? Qt.rgba(0, 0, 0, 0.1) : "transparent" + } + + onClicked: hiddenPWD = !hiddenPWD + } + + background: RoundBlur { + id: passwordBlur + objectName: "lockPasswordBlur" + color: Qt.rgba(1, 1, 1, 0.4) + radius: 6 + } + } + } + + Button { + id: loginBtn + objectName: "lockLoginBtn" + display: AbstractButton.TextOnly + text: ">" + padding: 0 + height: passwordField.height + width: height + anchors { + left: userCol.right + bottom: userCol.bottom + leftMargin: 20 + } + enabled: passwordField.length != 0 + font.pixelSize: 12 + background: RoundBlur { + anchors.fill: parent + color: Qt.rgba(1.0, 1.0, 1.0, 0.4) + radius: parent.height / 2 + } + } + } + + // Always in lockContent so updatePaintNode runs during settle, like + // the password blitter. A hideable Item never put this node in the + // output batches (no extra-QRhi copy, no skip log). + RoundBlur { + id: alwaysGlass + objectName: "lockAlwaysGlass" + x: 36 + y: 80 + width: 220 + height: 280 + z: 1 + color: Qt.rgba(1, 1, 1, 0.08) + radius: 12 + transformOrigin: Item.Bottom + } + + // ControlAction session chip, bottom-right. Expand copies RoundBlur + // like the real 150ms width/height Behavior. + Button { + id: sessionButton + objectName: "lockSessionButton" + property bool expand: false + width: expand ? 94 : 88 + height: expand ? 36 : 30 + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: 30 + anchors.bottomMargin: 30 + text: "Treeland" + palette.buttonText: "white" + Behavior on width { enabled: !root.testMode; NumberAnimation { duration: 150 } } + Behavior on height { enabled: !root.testMode; NumberAnimation { duration: 150 } } + + background: RoundBlur { + id: sessionBtnBlur + objectName: "lockSessionBtnBlur" + color: Qt.rgba(1, 1, 1, 0.3) + radius: sessionButton.height / 2 + } + + onClicked: sessionPopup.open() + } + + Button { + id: userButton + objectName: "lockUserButton" + property bool expand: false + width: expand ? 36 : 30 + height: expand ? 36 : 30 + anchors.right: sessionButton.left + anchors.bottom: parent.bottom + anchors.rightMargin: 15 + anchors.bottomMargin: 30 + Behavior on width { enabled: !root.testMode; NumberAnimation { duration: 150 } } + Behavior on height { enabled: !root.testMode; NumberAnimation { duration: 150 } } + + background: RoundBlur { + objectName: "lockUserBtnBlur" + color: Qt.rgba(1, 1, 1, 0.3) + radius: userButton.height / 2 + } + } + } + + Loader { + id: loginHideLoader + active: loginHideSource + sourceComponent: ShaderEffectSource { + live: true + hideSource: true + visible: true + sourceItem: lockContent + width: lockContent.width + height: lockContent.height + x: lockContent.x + y: lockContent.y + } + } + + Item { + id: sessionPopup + objectName: "lockSessionPopup" + visible: false + width: 220 + height: 280 + transformOrigin: Item.Bottom + layer.enabled: false + x: Math.max(8, Math.min(sessionButton.x + (sessionButton.width - width) / 2, + root.width - width - 8)) + y: sessionButton.y - height - 10 + + function open() { + visible = true + sessionButton.expand = true + userButton.expand = true + } + function close() { + visible = false + sessionButton.expand = false + userButton.expand = false + } + + RoundBlur { + id: sessionGlass + objectName: "lockSessionGlass" + anchors.fill: parent + color: Qt.rgba(1, 1, 1, 0.08) + radius: 12 + } + + ListView { + id: sessionList + objectName: "lockSessionList" + anchors.fill: parent + anchors.margins: 10 + clip: true + spacing: 1 + model: [ + "Sway", "Treeland User", "Weston", "deepin", + "GNOME", "KDE", "Xfce", "LXQt", + "i3", "Hyprland", "labwc", "Cage" + ] + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + } + delegate: Item { + required property string modelData + width: sessionList.width + height: 60 + Rectangle { + anchors.fill: parent + radius: 15 + color: Qt.rgba(0, 0, 0, 0.05) + } + Row { + anchors.fill: parent + anchors.leftMargin: 5 + anchors.verticalCenter: parent.verticalCenter + spacing: 5 + Rectangle { + width: 36 + height: 36 + radius: 6 + color: Qt.rgba(1, 1, 1, 0.85) + anchors.verticalCenter: parent.verticalCenter + } + Text { + text: modelData + color: "white" + width: sessionList.width - 60 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + function openSessionPopup() { + popupEnterAnim.stop() + popupExitAnim.stop() + if (popupEnterAnimation) { + sessionPopup.opacity = 0 + sessionPopup.scale = 0.85 + sessionPopup.open() + popupEnterAnim.start() + } else { + sessionPopup.opacity = 1 + sessionPopup.scale = 1 + sessionPopup.open() + } + } + + function closeSessionPopup() { + popupEnterAnim.stop() + if (popupEnterAnimation && sessionPopup.visible) { + popupExitAnim.restart() + } else { + popupExitAnim.stop() + sessionPopup.opacity = 1 + sessionPopup.scale = 1 + sessionPopup.close() + } + } + + ParallelAnimation { + id: popupEnterAnim + NumberAnimation { target: sessionPopup; property: "opacity"; to: 1; duration: 240 } + NumberAnimation { target: sessionPopup; property: "scale"; to: 1; duration: 240 } + } + ParallelAnimation { + id: popupExitAnim + NumberAnimation { target: sessionPopup; property: "opacity"; to: 0; duration: 240 } + NumberAnimation { target: sessionPopup; property: "scale"; to: 0.85; duration: 240 } + onFinished: { + sessionPopup.close() + sessionPopup.opacity = 1 + sessionPopup.scale = 1 + } + } + + function scrollSessionList(dy) { + sessionList.contentY += dy + } + + function blinkCaret() { + if (lockCaretItem) + lockCaretItem.opacity = lockCaretItem.opacity > 0.5 ? 0 : 1 + } + + function startHideSource() { + loginHideSource = true + } + + function stopHideSource() { + // Unhide before destroying the ShaderEffectSource. If the Loader + // drops the effect while hideSource is still true, lockContent stays + // hidden and chip-vs-rerender compares wallpaper to wallpaper. + var fx = loginHideLoader.item + if (fx) { + fx.hideSource = false + fx.sourceItem = null + } + loginHideSource = false + lockContent.visible = true + } + + function resetLock() { + stopHideSource() + popupEnterAnimation = false + popupEnterAnim.stop() + popupExitAnim.stop() + sessionPopup.opacity = 1 + sessionPopup.scale = 1 + sessionPopup.close() + sessionList.contentY = 0 + if (lockCaretItem) + lockCaretItem.opacity = 1 + passwordField.text = "" + passwordField.focus = false + } +} diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/RoundBlur.qml b/waylib/tests/unit_tests/test_wsgdamage_visual/RoundBlur.qml new file mode 100644 index 0000000000..97b714f8a4 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/RoundBlur.qml @@ -0,0 +1,65 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import Waylib.Server + +// Same structure as src/core/qml/Effects/Blur.qml (no extra overlay +// item) plus the color overlay from src/plugins/lockscreen/qml/RoundBlur.qml. +// Helper.config is not available here; blur amounts match the lockscreen +// RoundBlur defaults (blurMax: 64, blur fully on). +RenderBufferBlitter { + id: blitter + smooth: true + z: parent && parent.z ? parent.z - 1 : -1 + + property color color: Qt.rgba(1, 1, 1, 0.1) + property real radius: 0 + readonly property bool radiusEnabled: radius > 0 + + Item { + anchors.fill: parent + + MultiEffect { + id: blur + anchors.fill: parent + layer.enabled: blitter.radiusEnabled + smooth: blitter.radiusEnabled + opacity: blitter.radiusEnabled ? 0 : 1 + source: blitter.content + autoPaddingEnabled: false + blurEnabled: true + blur: 1.0 + blurMax: 64 + saturation: 0.2 + brightness: 0 + } + + Loader { + x: blur.x + y: blur.y + active: blitter.radiusEnabled + sourceComponent: Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + strokeWidth: 0 + fillItem: blur + PathRectangle { + width: blur.width + height: blur.height + radius: blitter.radius + } + } + } + } + } + + Rectangle { + anchors.fill: parent + radius: blitter.radius + color: blitter.color + } +} diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/TestWindow.qml b/waylib/tests/unit_tests/test_wsgdamage_visual/TestWindow.qml new file mode 100644 index 0000000000..8cb7b3335d --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/TestWindow.qml @@ -0,0 +1,79 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import Waylib.Server + +Item { + id: root + width: 800 + height: 480 + + OutputRenderWindow { + id: renderWindow + objectName: "renderWindow" + width: 800 + height: 480 + + DynamicCreatorComponent { + creator: Helper.outputCreator + + OutputItem { + required property WaylandOutput waylandOutput + output: waylandOutput + layout: Helper.outputLayout + devicePixelRatio: waylandOutput.scale + + cursorDelegate: Cursor { + id: cursorItem + objectName: "outputCursor" + required property QtObject outputCursor + readonly property point position: parent.mapFromGlobal(cursor.position.x, cursor.position.y) + + cursor: outputCursor.cursor + output: outputCursor.output.output + x: position.x - hotSpot.x + y: position.y - hotSpot.y + // Hardware-cursor tests hide this item (the cursor lives on + // another plane). Binding must include the flag so C++ + // setVisible(false) is not overwritten by outputCursor.visible. + property bool simulateHardwareCursor: false + visible: valid && outputCursor.visible && !simulateHardwareCursor + // Tests switch OutputLayer / hardware at runtime. Default + // matches treeland: independent cursor layer. + OutputLayer.enabled: true + OutputLayer.keepLayer: true + OutputLayer.outputs: [outputViewport] + OutputLayer.flags: OutputLayer.Cursor + OutputLayer.cursorHotSpot: hotSpot + } + + OutputViewport { + id: outputViewport + objectName: "outputViewport" + output: waylandOutput + devicePixelRatio: parent.devicePixelRatio + anchors.fill: parent + } + + DamageScene { + id: scene + objectName: "scene" + anchors.fill: parent + } + + DynamicCreatorComponent { + creator: Helper.xdgShellCreator + + XdgToplevelSurfaceItem { + objectName: "clientSurface" + required property WaylandXdgSurface waylandSurface + shellSurface: waylandSurface + x: 20 + y: 360 + } + } + } + } + } +} diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/damageclient.c b/waylib/tests/unit_tests/test_wsgdamage_visual/damageclient.c new file mode 100644 index 0000000000..4c5b5c47b9 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/damageclient.c @@ -0,0 +1,643 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#define _GNU_SOURCE + +#include "damageclient.h" +#include "xdg-shell-client-protocol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { + CMD_DAMAGE = 1, + CMD_STOP = 2, + kBufferCount = 2 +}; + +static const uint32_t kBasePixel = 0xFF2A8F4E; +static const uint32_t kDamagePixel = 0xFFE8C44A; + +struct DamageCmd { + int op; + int x; + int y; + int w; + int h; +}; + +struct ShmBuffer { + struct wl_buffer *wl; + uint32_t *pixels; + size_t size; + int busy; +}; + +struct DamageClient { + int connect_fd; + int cmd_rd; + int cmd_wr; + int ack_rd; + int ack_wr; + atomic_int mapped; + atomic_int running; + int width; + int height; + int pending_width; + int pending_height; + int configured; + pthread_mutex_t err_mutex; + char error[128]; + + struct wl_display *display; + struct wl_registry *registry; + struct wl_compositor *compositor; + struct wl_shm *shm; + struct xdg_wm_base *wm_base; + struct wl_surface *surface; + struct xdg_surface *xdg_surface; + struct xdg_toplevel *xdg_toplevel; + struct ShmBuffer buffers[kBufferCount]; + uint32_t *shadow; + size_t shadow_size; +}; + +static void set_error(struct DamageClient *c, const char *msg) +{ + pthread_mutex_lock(&c->err_mutex); + snprintf(c->error, sizeof(c->error), "%s", msg); + pthread_mutex_unlock(&c->err_mutex); +} + +static int write_full(int fd, const void *buf, size_t n) +{ + const char *p = buf; + size_t left = n; + while (left) { + ssize_t w = write(fd, p, left); + if (w < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += (size_t)w; + left -= (size_t)w; + } + return 0; +} + +static int read_full(int fd, void *buf, size_t n) +{ + char *p = buf; + size_t left = n; + while (left) { + ssize_t r = read(fd, p, left); + if (r < 0) { + if (errno == EINTR) + continue; + return -1; + } + if (r == 0) + return -1; + p += (size_t)r; + left -= (size_t)r; + } + return 0; +} + +static int create_shm_fd(size_t size) +{ + int fd = memfd_create("wsg-damage-client", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd < 0) + return -1; + if (ftruncate(fd, (off_t)size) < 0) { + close(fd); + return -1; + } + return fd; +} + +static void buffer_release(void *data, struct wl_buffer *buffer) +{ + (void)buffer; + struct ShmBuffer *b = data; + b->busy = 0; +} + +static const struct wl_buffer_listener buffer_listener = { + .release = buffer_release, +}; + +static int create_shm_buffer(struct DamageClient *c, struct ShmBuffer *out) +{ + if (c->width <= 0 || c->height <= 0) + return -1; + const size_t size = (size_t)c->width * 4u * (size_t)c->height; + if (size == 0 || size > (size_t)INT32_MAX) + return -1; + const int stride = (int)((size_t)c->width * 4u); + int fd = create_shm_fd(size); + if (fd < 0) + return -1; + + uint32_t *pixels = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (pixels == MAP_FAILED) { + close(fd); + return -1; + } + + for (size_t i = 0; i < size / sizeof(uint32_t); ++i) + pixels[i] = (uint32_t)kBasePixel; + + struct wl_shm_pool *pool = wl_shm_create_pool(c->shm, fd, (int32_t)size); + close(fd); + if (!pool) { + munmap(pixels, size); + return -1; + } + + struct wl_buffer *buffer = wl_shm_pool_create_buffer( + pool, 0, c->width, c->height, stride, WL_SHM_FORMAT_XRGB8888); + wl_shm_pool_destroy(pool); + if (!buffer) { + munmap(pixels, size); + return -1; + } + + out->wl = buffer; + out->pixels = pixels; + out->size = size; + out->busy = 0; + wl_buffer_add_listener(buffer, &buffer_listener, out); + return 0; +} + +static void destroy_pixel_storage(struct DamageClient *c) +{ + for (int i = 0; i < kBufferCount; ++i) { + if (c->buffers[i].wl) { + wl_buffer_destroy(c->buffers[i].wl); + c->buffers[i].wl = NULL; + } + if (c->buffers[i].pixels) { + munmap(c->buffers[i].pixels, c->buffers[i].size); + c->buffers[i].pixels = NULL; + c->buffers[i].size = 0; + } + c->buffers[i].busy = 0; + } + free(c->shadow); + c->shadow = NULL; + c->shadow_size = 0; +} + +static int create_pixel_storage(struct DamageClient *c) +{ + if (c->width <= 0 || c->height <= 0) + return -1; + for (int i = 0; i < kBufferCount; ++i) { + if (create_shm_buffer(c, &c->buffers[i]) < 0) { + destroy_pixel_storage(c); + return -1; + } + } + c->shadow_size = (size_t)c->width * (size_t)c->height * sizeof(uint32_t); + c->shadow = malloc(c->shadow_size); + if (!c->shadow) { + destroy_pixel_storage(c); + return -1; + } + for (size_t i = 0; i < c->shadow_size / sizeof(uint32_t); ++i) + c->shadow[i] = (uint32_t)kBasePixel; + return 0; +} + +/* A configure only stages the new size; the old buffers stay valid until the + * next commit recreates every pixel store up front, so in-flight pixels and + * shadow indexing never disagree about the dimensions. */ +static int apply_pending_resize(struct DamageClient *c) +{ + const int width = c->pending_width > 0 ? c->pending_width : c->width; + const int height = c->pending_height > 0 ? c->pending_height : c->height; + c->pending_width = 0; + c->pending_height = 0; + if (width == c->width && height == c->height) + return c->shadow ? 0 : -1; + if (width <= 0 || height <= 0) + return -1; + destroy_pixel_storage(c); + c->width = width; + c->height = height; + if (create_pixel_storage(c) < 0) { + set_error(c, "pixel storage recreate failed"); + return -1; + } + return 0; +} + +static struct ShmBuffer *pick_buffer(struct DamageClient *c) +{ + for (int i = 0; i < kBufferCount; ++i) { + if (!c->buffers[i].busy) + return &c->buffers[i]; + } + return NULL; +} + +static struct ShmBuffer *acquire_buffer(struct DamageClient *c) +{ + struct ShmBuffer *buf = pick_buffer(c); + if (buf || !c->display || !atomic_load(&c->running)) + return buf; + + /* Deliver already-queued releases without blocking. */ + wl_display_dispatch_pending(c->display); + buf = pick_buffer(c); + if (buf) + return buf; + + /* Never block here: the caller sits in damage_client_commit_damage waiting + * for our ACK while the in-process server needs that same thread to make + * progress, so waiting for a release would deadlock both sides. Drain only + * what already arrived and let the caller retry after pumping instead. */ + if (wl_display_prepare_read(c->display) == 0) { + struct pollfd pfd; + pfd.fd = wl_display_get_fd(c->display); + pfd.events = POLLIN; + pfd.revents = 0; + if (poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN)) { + if (wl_display_read_events(c->display) == 0) + wl_display_dispatch_pending(c->display); + else + wl_display_cancel_read(c->display); + } else { + wl_display_cancel_read(c->display); + } + } else { + wl_display_dispatch_pending(c->display); + } + return pick_buffer(c); +} + +static void fill_rect_pixels(uint32_t *pixels, int width, int x, int y, int w, int h, uint32_t pixel) +{ + for (int row = y; row < y + h; ++row) { + uint32_t *line = pixels + row * width + x; + for (int col = 0; col < w; ++col) + line[col] = pixel; + } +} + +static void xdg_wm_base_ping(void *data, struct xdg_wm_base *wm_base, uint32_t serial) +{ + (void)data; + xdg_wm_base_pong(wm_base, serial); +} + +static const struct xdg_wm_base_listener wm_base_listener = { + .ping = xdg_wm_base_ping, +}; + +static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial) +{ + struct DamageClient *c = data; + xdg_surface_ack_configure(xdg_surface, serial); + c->configured = 1; +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = xdg_surface_configure, +}; + +static void xdg_toplevel_configure(void *data, struct xdg_toplevel *toplevel, + int32_t width, int32_t height, struct wl_array *states) +{ + (void)toplevel; + (void)states; + struct DamageClient *c = data; + if (width > 0) + c->pending_width = width; + if (height > 0) + c->pending_height = height; +} + +static void xdg_toplevel_close(void *data, struct xdg_toplevel *toplevel) +{ + (void)data; + (void)toplevel; +} + +static void xdg_toplevel_configure_bounds(void *data, struct xdg_toplevel *toplevel, + int32_t width, int32_t height) +{ + (void)data; + (void)toplevel; + (void)width; + (void)height; +} + +static void xdg_toplevel_wm_capabilities(void *data, struct xdg_toplevel *toplevel, + struct wl_array *capabilities) +{ + (void)data; + (void)toplevel; + (void)capabilities; +} + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + .configure = xdg_toplevel_configure, + .close = xdg_toplevel_close, + .configure_bounds = xdg_toplevel_configure_bounds, + .wm_capabilities = xdg_toplevel_wm_capabilities, +}; + +static void registry_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + struct DamageClient *c = data; + if (strcmp(interface, wl_compositor_interface.name) == 0) { + const uint32_t v = version < 4 ? version : 4; + c->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, v); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + c->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { + const uint32_t v = version < 5 ? version : 5; + c->wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, v); + xdg_wm_base_add_listener(c->wm_base, &wm_base_listener, c); + } +} + +static void registry_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + (void)data; + (void)registry; + (void)name; +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_global, + .global_remove = registry_global_remove, +}; + +static void destroy_wayland(struct DamageClient *c) +{ + destroy_pixel_storage(c); + if (c->xdg_toplevel) { + xdg_toplevel_destroy(c->xdg_toplevel); + c->xdg_toplevel = NULL; + } + if (c->xdg_surface) { + xdg_surface_destroy(c->xdg_surface); + c->xdg_surface = NULL; + } + if (c->surface) { + wl_surface_destroy(c->surface); + c->surface = NULL; + } + if (c->wm_base) { + xdg_wm_base_destroy(c->wm_base); + c->wm_base = NULL; + } + if (c->shm) { + wl_shm_destroy(c->shm); + c->shm = NULL; + } + if (c->compositor) { + wl_compositor_destroy(c->compositor); + c->compositor = NULL; + } + if (c->registry) { + wl_registry_destroy(c->registry); + c->registry = NULL; + } + if (c->display) { + wl_display_disconnect(c->display); + c->display = NULL; + } +} + +static int commit_damage(struct DamageClient *c, int x, int y, int w, int h) +{ + if (apply_pending_resize(c) < 0) + return -1; + if (x < 0 || y < 0 || w <= 0 || h <= 0) + return -1; + if (x > c->width || y > c->height) + return -1; + if (w > c->width - x || h > c->height - y) + return -1; + + struct ShmBuffer *buf = acquire_buffer(c); + if (!buf || !c->shadow) + return -1; + fill_rect_pixels(c->shadow, c->width, x, y, w, h, (uint32_t)kDamagePixel); + memcpy(buf->pixels, c->shadow, c->shadow_size); + buf->busy = 1; + wl_surface_attach(c->surface, buf->wl, 0, 0); + wl_surface_damage(c->surface, x, y, w, h); + wl_surface_commit(c->surface); + return wl_display_flush(c->display) < 0 ? -1 : 0; +} + +struct DamageClient *damage_client_create(int fd) +{ + if (fd < 0) + return NULL; + + struct DamageClient *c = calloc(1, sizeof(*c)); + if (!c) { + close(fd); + return NULL; + } + if (pthread_mutex_init(&c->err_mutex, NULL) != 0) { + close(fd); + free(c); + return NULL; + } + + c->connect_fd = fd; + c->width = DAMAGE_CLIENT_WIDTH; + c->height = DAMAGE_CLIENT_HEIGHT; + c->cmd_rd = c->cmd_wr = c->ack_rd = c->ack_wr = -1; + + int cmd[2] = { -1, -1 }; + int ack[2] = { -1, -1 }; + if (pipe(cmd) < 0 || pipe(ack) < 0) { + if (cmd[0] >= 0) + close(cmd[0]); + if (cmd[1] >= 0) + close(cmd[1]); + if (ack[0] >= 0) + close(ack[0]); + if (ack[1] >= 0) + close(ack[1]); + close(fd); + pthread_mutex_destroy(&c->err_mutex); + free(c); + return NULL; + } + c->cmd_rd = cmd[0]; + c->cmd_wr = cmd[1]; + c->ack_rd = ack[0]; + c->ack_wr = ack[1]; + atomic_store(&c->running, 1); + return c; +} + +void damage_client_run(struct DamageClient *c) +{ + if (!c) + return; + + c->display = wl_display_connect_to_fd(c->connect_fd); + c->connect_fd = -1; + if (!c->display) { + set_error(c, "wl_display_connect_to_fd failed"); + atomic_store(&c->running, 0); + return; + } + + c->registry = wl_display_get_registry(c->display); + wl_registry_add_listener(c->registry, ®istry_listener, c); + if (wl_display_roundtrip(c->display) < 0) { + set_error(c, "registry roundtrip failed"); + goto out; + } + if (!c->compositor || !c->shm || !c->wm_base) { + set_error(c, "missing wl_compositor, wl_shm, or xdg_wm_base"); + goto out; + } + + c->surface = wl_compositor_create_surface(c->compositor); + c->xdg_surface = xdg_wm_base_get_xdg_surface(c->wm_base, c->surface); + xdg_surface_add_listener(c->xdg_surface, &xdg_surface_listener, c); + c->xdg_toplevel = xdg_surface_get_toplevel(c->xdg_surface); + xdg_toplevel_add_listener(c->xdg_toplevel, &xdg_toplevel_listener, c); + xdg_toplevel_set_title(c->xdg_toplevel, "wsg-damage-client"); + wl_surface_commit(c->surface); + if (wl_display_roundtrip(c->display) < 0) { + set_error(c, "xdg configure roundtrip failed"); + goto out; + } + if (!c->configured) { + set_error(c, "xdg configure not received"); + goto out; + } + + if (c->pending_width > 0) + c->width = c->pending_width; + if (c->pending_height > 0) + c->height = c->pending_height; + c->pending_width = 0; + c->pending_height = 0; + + if (create_pixel_storage(c) < 0) { + set_error(c, "shm buffer create failed"); + goto out; + } + + struct ShmBuffer *buf = pick_buffer(c); + buf->busy = 1; + wl_surface_attach(c->surface, buf->wl, 0, 0); + wl_surface_damage(c->surface, 0, 0, c->width, c->height); + wl_surface_commit(c->surface); + if (wl_display_flush(c->display) < 0) { + set_error(c, "map flush failed"); + goto out; + } + atomic_store(&c->mapped, 1); + + while (atomic_load(&c->running)) { + struct DamageCmd cmd; + if (read_full(c->cmd_rd, &cmd, sizeof(cmd)) < 0) + break; + if (cmd.op == CMD_STOP) + break; + if (cmd.op == CMD_DAMAGE) { + int status = commit_damage(c, cmd.x, cmd.y, cmd.w, cmd.h); + write_full(c->ack_wr, &status, sizeof(status)); + wl_display_dispatch_pending(c->display); + } + } + +out: + atomic_store(&c->mapped, 0); + atomic_store(&c->running, 0); + destroy_wayland(c); +} + +int damage_client_is_mapped(const struct DamageClient *client) +{ + return client && atomic_load(&client->mapped); +} + +const char *damage_client_error(const struct DamageClient *client) +{ + static _Thread_local char snapshot[sizeof(((struct DamageClient *)0)->error)]; + if (!client) + return "null client"; + pthread_mutex_lock((pthread_mutex_t *)&client->err_mutex); + snprintf(snapshot, sizeof(snapshot), "%s", client->error); + pthread_mutex_unlock((pthread_mutex_t *)&client->err_mutex); + return snapshot[0] ? snapshot : ""; +} + +int damage_client_commit_damage(struct DamageClient *client, int x, int y, int w, int h) +{ + if (!client || !atomic_load(&client->mapped)) + return -1; + + struct DamageCmd cmd = { + .op = CMD_DAMAGE, + .x = x, + .y = y, + .w = w, + .h = h, + }; + if (write_full(client->cmd_wr, &cmd, sizeof(cmd)) < 0) + return -1; + + int status = -1; + if (read_full(client->ack_rd, &status, sizeof(status)) < 0) + return -1; + return status; +} + +void damage_client_stop(struct DamageClient *client) +{ + if (!client) + return; + if (atomic_exchange(&client->running, 0)) { + struct DamageCmd cmd = { .op = CMD_STOP }; + write_full(client->cmd_wr, &cmd, sizeof(cmd)); + } +} + +void damage_client_destroy(struct DamageClient *client) +{ + if (!client) + return; + damage_client_stop(client); + if (client->cmd_rd >= 0) + close(client->cmd_rd); + if (client->cmd_wr >= 0) + close(client->cmd_wr); + if (client->ack_rd >= 0) + close(client->ack_rd); + if (client->ack_wr >= 0) + close(client->ack_wr); + if (client->connect_fd >= 0) + close(client->connect_fd); + pthread_mutex_destroy(&client->err_mutex); + free(client); +} diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/damageclient.h b/waylib/tests/unit_tests/test_wsgdamage_visual/damageclient.h new file mode 100644 index 0000000000..f3ebd55752 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/damageclient.h @@ -0,0 +1,31 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef DAMAGECLIENT_H +#define DAMAGECLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct DamageClient; + +enum { + DAMAGE_CLIENT_WIDTH = 64, + DAMAGE_CLIENT_HEIGHT = 64 +}; + +/* Takes ownership of `fd`. Call damage_client_run() on a worker thread. */ +struct DamageClient *damage_client_create(int fd); +void damage_client_run(struct DamageClient *client); +int damage_client_is_mapped(const struct DamageClient *client); +const char *damage_client_error(const struct DamageClient *client); +int damage_client_commit_damage(struct DamageClient *client, int x, int y, int w, int h); +void damage_client_stop(struct DamageClient *client); +void damage_client_destroy(struct DamageClient *client); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/helper.cpp b/waylib/tests/unit_tests/test_wsgdamage_visual/helper.cpp new file mode 100644 index 0000000000..3f136caf56 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/helper.cpp @@ -0,0 +1,161 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "helper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +namespace { +constexpr int kSceneWidth = 800; +constexpr int kSceneHeight = 480; +} + +VisualHelper::VisualHelper(QObject *parent) + : QObject(parent) + , m_server(new WServer(this)) + , m_outputCreator(new WQmlCreator(this)) + , m_xdgShellCreator(new WQmlCreator(this)) + , m_outputLayout(new WQuickOutputLayout(m_server)) + , m_cursor(new WCursor(this)) +{ + m_seat = m_server->attach(); + m_seat->setCursor(m_cursor); + m_cursor->setLayout(m_outputLayout); + m_cursor->setCursor(QCursor(Qt::ArrowCursor)); + m_cursor->setVisible(true); +} + +void VisualHelper::initProtocols(WOutputRenderWindow *window, QQmlEngine *qmlEngine) +{ + m_backend = m_server->attach(); + m_server->start(); + + m_renderer = WRenderHelper::createRenderer(m_backend->handle()); + if (!m_renderer) { + qWarning("Failed to create wlroots renderer"); + return; + } + + connect(m_backend, &WBackend::outputAdded, this, [this, qmlEngine](WOutput *output) { + auto props = qmlEngine->newObject(); + props.setProperty("waylandOutput", qmlEngine->toScriptValue(output)); + props.setProperty("layout", qmlEngine->toScriptValue(m_outputLayout)); + props.setProperty("x", qmlEngine->toScriptValue(m_outputLayout->implicitWidth())); + m_outputCreator->add(output, props); + }); + connect(m_backend, &WBackend::outputRemoved, this, [this](WOutput *output) { + m_outputCreator->removeByOwner(output); + }); + connect(m_backend, &WBackend::inputAdded, this, [this](WInputDevice *device) { + m_seat->attachInputDevice(device); + }); + connect(m_backend, &WBackend::inputRemoved, this, [this](WInputDevice *device) { + m_seat->detachInputDevice(device); + }); + + m_socket = new WSocket(false, this); + if (m_socket->autoCreate()) { + m_server->addSocket(m_socket); + } else { + delete m_socket; + m_socket = nullptr; + qCritical("Failed to create Wayland socket for the visual damage test"); + } + + m_allocator = wlr_allocator_autocreate(m_backend->handle(), m_renderer); + if (!m_allocator) { + qWarning("Failed to create wlroots allocator"); + return; + } + wlr_renderer_init_wl_display(m_renderer, m_server->handle()); + m_compositor = wlr_compositor_create(m_server->handle(), 6, m_renderer); + wlr_subcompositor_create(m_server->handle()); + + auto *xdgShell = m_server->attach(5); + connect(xdgShell, &WXdgShell::toplevelSurfaceAdded, this, + [this, qmlEngine](WXdgToplevelSurface *surface) { + connect(surface->surface(), &WSurface::commit, surface, [surface] { + if (surface->handle() && surface->handle()->base + && surface->handle()->base->initial_commit) + wlr_xdg_toplevel_set_size(surface->handle(), 0, 0); + }, Qt::DirectConnection); + + auto props = qmlEngine->newObject(); + props.setProperty("waylandSurface", qmlEngine->toScriptValue(surface)); + m_xdgShellCreator->add(surface, props); + }); + connect(xdgShell, &WXdgShell::toplevelSurfaceRemoved, + m_xdgShellCreator, &WQmlCreator::removeByOwner); + + connect(window, &WOutputRenderWindow::outputViewportInitialized, this, + [](WOutputViewport *viewport) { + auto *output = viewport->output(); + if (output->property("_Enabled").toBool()) + return; + output->setProperty("_Enabled", true); + auto *wlrOutput = output->handle(); + wlr_output_state newState; + wlr_output_state_init(&newState); + wlr_output_state_set_custom_mode(&newState, kSceneWidth, kSceneHeight, 0); + wlr_output_state_set_enabled(&newState, true); + if (!wlr_output_commit_state(wlrOutput, &newState)) + qCritical("commit failed on output %s", wlrOutput->name); + wlr_output_state_finish(&newState); + }); + + window->init(m_renderer, m_allocator); + wlr_backend_start(m_backend->handle()); +} + +QString VisualHelper::waylandSocketName() const +{ + return m_socket ? m_socket->fullServerName() : QString(); +} + +int VisualHelper::createInProcessClientFd() +{ + if (!m_socket) + return -1; + + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds) < 0) + return -1; + if (!m_socket->addClient(fds[0])) { + close(fds[0]); + close(fds[1]); + return -1; + } + return fds[1]; +} + +void VisualHelper::dispatchWaylandEvents() +{ + if (!m_server || !m_server->handle()) + return; + struct wl_event_loop *loop = wl_display_get_event_loop(m_server->handle()); + wl_event_loop_dispatch(loop, 0); + wl_event_loop_dispatch_idle(loop); + wl_display_flush_clients(m_server->handle()); +} diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/helper.h b/waylib/tests/unit_tests/test_wsgdamage_visual/helper.h new file mode 100644 index 0000000000..c34f01d0a0 --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/helper.h @@ -0,0 +1,66 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +#include +#include +#include +#include + +Q_MOC_INCLUDE() +Q_MOC_INCLUDE() + +WAYLIB_SERVER_BEGIN_NAMESPACE +class WServer; +class WSocket; +class WOutputRenderWindow; +class WQuickOutputLayout; +class WCursor; +class WSeat; +class WBackend; +WAYLIB_SERVER_END_NAMESPACE + +struct wlr_renderer; +struct wlr_allocator; +struct wlr_compositor; + +WAYLIB_SERVER_USE_NAMESPACE + +class VisualHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(WQmlCreator *outputCreator MEMBER m_outputCreator CONSTANT) + Q_PROPERTY(WQmlCreator *xdgShellCreator MEMBER m_xdgShellCreator CONSTANT) + Q_PROPERTY(WQuickOutputLayout *outputLayout READ outputLayout CONSTANT) + Q_PROPERTY(WCursor *cursor READ cursor CONSTANT) + QML_NAMED_ELEMENT(Helper) + QML_SINGLETON + +public: + explicit VisualHelper(QObject *parent = nullptr); + + void initProtocols(WOutputRenderWindow *window, QQmlEngine *qmlEngine); + wlr_renderer *renderer() const { return m_renderer; } + WQuickOutputLayout *outputLayout() const { return m_outputLayout; } + WCursor *cursor() const { return m_cursor; } + QString waylandSocketName() const; + int createInProcessClientFd(); + void dispatchWaylandEvents(); + +private: + WServer *m_server = nullptr; + WSocket *m_socket = nullptr; + WQmlCreator *m_outputCreator = nullptr; + WQmlCreator *m_xdgShellCreator = nullptr; + WBackend *m_backend = nullptr; + wlr_renderer *m_renderer = nullptr; + wlr_allocator *m_allocator = nullptr; + wlr_compositor *m_compositor = nullptr; + WQuickOutputLayout *m_outputLayout = nullptr; + WCursor *m_cursor = nullptr; + QPointer m_seat; +}; diff --git a/waylib/tests/unit_tests/test_wsgdamage_visual/main.cpp b/waylib/tests/unit_tests/test_wsgdamage_visual/main.cpp new file mode 100644 index 0000000000..68e023290c --- /dev/null +++ b/waylib/tests/unit_tests/test_wsgdamage_visual/main.cpp @@ -0,0 +1,1722 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "damageclient.h" +#include "helper.h" +#include "wbufferrenderer_p.h" +#include "woutputviewport_p.h" +#include "wsgbatchrenderer_p.h" +#include "wregionhighlightitem.h" +#include "wsgdamagelog_p.h" +#include "wpixmanregion.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_CONFIG(opengl) +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +WAYLIB_SERVER_USE_NAMESPACE + +namespace { + +constexpr int kRgbTolerance = 40; +constexpr int kMaxMismatchPixels = 96; +constexpr int kCursorPad = 8; +const QPointF kCursorHome(240, 300); +const QPointF kCursorMoved(400, 360); + +QByteArray nvidiaRenderNode() +{ + const QDir drm(QStringLiteral("/sys/class/drm")); + const QStringList nodes = drm.entryList(QStringList{ QStringLiteral("renderD*") }, + QDir::Dirs | QDir::System); + for (const QString &name : nodes) { + QFile vendor(drm.filePath(name) + QStringLiteral("/device/vendor")); + if (!vendor.open(QIODevice::ReadOnly)) + continue; + if (vendor.readAll().trimmed() == "0x10de") + return QByteArrayLiteral("/dev/dri/") + name.toLatin1(); + } + return {}; +} + +QByteArray drainGlErrors(QQuickWindow *window = nullptr) +{ +#if QT_CONFIG(opengl) + QOpenGLContext *ctx = QOpenGLContext::currentContext(); + if (!ctx && window) { + if (auto *ri = window->rendererInterface()) { + ctx = static_cast( + ri->getResource(window, QSGRendererInterface::OpenGLContextResource)); + } + } + if (!ctx) + return {}; + if (ctx->surface()) + ctx->makeCurrent(ctx->surface()); + QByteArray out; + auto *fns = ctx->functions(); + for (;;) { + const GLenum err = fns->glGetError(); + if (err == GL_NO_ERROR) + break; + if (!out.isEmpty()) + out += ' '; + if (err == GL_INVALID_ENUM) + out += "GL_INVALID_ENUM"; + else + out += QByteArray::number(err, 16); + } + return out; +#else + return {}; +#endif +} + +int rgbDistance(QRgb a, QRgb b) +{ + return std::abs(qRed(a) - qRed(b)) + + std::abs(qGreen(a) - qGreen(b)) + + std::abs(qBlue(a) - qBlue(b)); +} + +int regionDiffCount(const QImage &a, const QImage &b, const QRegion ®ion, int minDistance, + QPoint *firstMismatch = nullptr) +{ + if (a.size() != b.size()) + return -1; + int count = 0; + for (const QRect &part : region) { + const QRect bounded = part.intersected(a.rect()); + for (int y = bounded.top(); y <= bounded.bottom(); ++y) { + for (int x = bounded.left(); x <= bounded.right(); ++x) { + if (rgbDistance(a.pixel(x, y), b.pixel(x, y)) > minDistance) { + if (count == 0 && firstMismatch) + *firstMismatch = QPoint(x, y); + ++count; + } + } + } + } + return count; +} + +int regionDiffCount(const QImage &a, const QImage &b, const QRect ®ion, int minDistance, + QPoint *firstMismatch = nullptr) +{ + return regionDiffCount(a, b, QRegion(region), minDistance, firstMismatch); +} + +struct FrameSnap { + bool valid = false; + bool full = false; + WPixmanRegion region; + QImage buffer; + QImage grab; + QList entries; + qint64 nowMs = 0; +}; + +QImage toArgb32(const QImage &image) +{ + return image.convertToFormat(QImage::Format_ARGB32); +} + +} // namespace + +class WSGDamageDebugVisualTest : public QObject +{ + Q_OBJECT + +public: + static void setGlobals(WOutputRenderWindow *window, WOutputViewport *viewport, + QQuickItem *scene, VisualHelper *helper) + { + m_window = window; + m_viewport = viewport; + m_scene = scene; + m_helper = helper; + } + +private: + static inline WOutputRenderWindow *m_window = nullptr; + static inline WOutputViewport *m_viewport = nullptr; + static inline QQuickItem *m_scene = nullptr; + static inline VisualHelper *m_helper = nullptr; + + enum class CursorPath { + Hardware, + SoftwareLayer, + SoftwareInline, + }; + + QQuickItem *named(const char *name) const + { + return m_scene ? m_scene->findChild(QLatin1String(name)) : nullptr; + } + + QQuickItem *namedAnywhere(const char *name) const + { + if (auto *item = named(name)) + return item; + return m_window ? m_window->findChild(QLatin1String(name)) : nullptr; + } + + QQuickItem *cursorItem() const + { + if (auto *item = m_window->findChild(QStringLiteral("outputCursor"))) + return item; + auto *outputItem = m_window->findChild(); + if (!outputItem || outputItem->cursorItems().isEmpty()) + return nullptr; + return outputItem->cursorItems().constFirst(); + } + + WOutputLayer *cursorLayer() const + { + QQuickItem *item = cursorItem(); + if (!item) + return nullptr; + return qobject_cast(qmlAttachedPropertiesObject(item, false)); + } + + QString cursorPathDebug() const + { + auto *item = cursorItem(); + auto *layer = cursorLayer(); + auto *output = m_viewport ? m_viewport->output() : nullptr; + const bool onHw = layer && m_viewport && layer->inOutputsByHardware().contains(m_viewport); + return QStringLiteral("visible=%1 simulateHw=%2 enabled=%3 keepLayer=%4 inHardware=%5 forceSoftware=%6 disableHwLayers=%7") + .arg(item ? item->isVisible() : false) + .arg(item ? item->property("simulateHardwareCursor").toBool() : false) + .arg(layer ? layer->enabled() : false) + .arg(layer ? layer->keepLayer() : false) + .arg(onHw) + .arg(output ? output->forceSoftwareCursor() : false) + .arg(m_viewport ? m_viewport->disableHardwareLayers() : false); + } + + bool cursorPathMatches(CursorPath path) const + { + auto *item = cursorItem(); + auto *layer = cursorLayer(); + if (!item || !layer || !m_viewport) + return false; + const bool simulateHw = item->property("simulateHardwareCursor").toBool(); + const bool onHw = layer->inOutputsByHardware().contains(m_viewport); + switch (path) { + case CursorPath::Hardware: + return simulateHw && !item->isVisible(); + case CursorPath::SoftwareLayer: + return !simulateHw && item->isVisible() && layer->enabled() && layer->keepLayer() && !onHw + && m_viewport->layers().contains(layer); + case CursorPath::SoftwareInline: + return !simulateHw && item->isVisible() && !layer->enabled() && !onHw; + } + return false; + } + + bool configureCursorPath(CursorPath path) + { + auto *output = m_viewport ? m_viewport->output() : nullptr; + auto *item = cursorItem(); + auto *layer = cursorLayer(); + if (!output || !item || !layer) + return false; + + item->setProperty("simulateHardwareCursor", path == CursorPath::Hardware); + + switch (path) { + case CursorPath::Hardware: + // Cursor is not drawn into the output buffer (hardware plane). + output->setForceSoftwareCursor(false); + m_viewport->setDisableHardwareLayers(false); + layer->setKeepLayer(true); + layer->setEnabled(true); + break; + case CursorPath::SoftwareLayer: + output->setForceSoftwareCursor(true); + m_viewport->setDisableHardwareLayers(true); + layer->setKeepLayer(true); + layer->setEnabled(true); + break; + case CursorPath::SoftwareInline: + output->setForceSoftwareCursor(true); + m_viewport->setDisableHardwareLayers(true); + layer->setEnabled(false); + break; + } + + parkCursor(); + for (int i = 0; i < 8; ++i) { + if (!captureOutput(false).valid) + return false; + if (cursorPathMatches(path)) + return true; + } + return cursorPathMatches(path); + } + + void restoreCursorDefaults() + { + if (auto *output = m_viewport ? m_viewport->output() : nullptr) + output->setForceSoftwareCursor(false); + if (m_viewport) + m_viewport->setDisableHardwareLayers(false); + if (auto *item = cursorItem()) + item->setProperty("simulateHardwareCursor", false); + if (auto *layer = cursorLayer()) { + layer->setKeepLayer(true); + layer->setEnabled(true); + } + } + + void parkCursor() + { + auto *cursor = m_helper->cursor(); + cursor->setVisible(true); + if (cursor->position() == kCursorHome) + cursor->setPosition(kCursorHome + QPointF(1, 1)); + cursor->setPosition(kCursorHome); + } + + QRegion cursorIgnoreRegion() const + { + QRegion region; + if (auto *item = cursorItem()) { + const QRect rect = outputRect(item); + if (!rect.isEmpty()) + region += rect.adjusted(-kCursorPad, -kCursorPad, kCursorPad, kCursorPad); + } + // grabToImage is the QML scene without the cursor sibling, so dump vs + // grab must ignore every slot this test parks/moves the cursor through. + const QSize slot(48, 48); + region += QRect(kCursorHome.toPoint(), slot).adjusted(-kCursorPad, -kCursorPad, kCursorPad, kCursorPad); + region += QRect(kCursorMoved.toPoint(), slot).adjusted(-kCursorPad, -kCursorPad, kCursorPad, kCursorPad); + return region; + } + + QRect itemSceneRect(QQuickItem *item) const + { + if (!item) + return {}; + return item->mapRectToScene(QRectF(0, 0, item->width(), item->height())).toAlignedRect(); + } + + QRect itemOutputRect(QQuickItem *item) const + { + if (!item || !m_viewport) + return {}; + const QRect mapped = + m_viewport->mapToOutput(item, QRectF(0, 0, item->width(), item->height())).toAlignedRect(); + return mapped.isEmpty() ? itemSceneRect(item) : mapped; + } + + QRect outputRect(QQuickItem *item) const + { + if (!item || !m_viewport) + return {}; + return m_viewport->mapToOutput(item, item->boundingRect()).toAlignedRect(); + } + + QRect sceneRect(QQuickItem *item) const + { + if (!item) + return {}; + return item->mapRectToScene(item->boundingRect()).toAlignedRect(); + } + + QQuickItem *popupPanel() const + { + return m_window->findChild(QStringLiteral("popupPanel")); + } + + void restoreViewportTransform() + { + if (!m_viewport) + return; + m_viewport->setRotation(0); + m_viewport->setScale(1); + m_viewport->rotateOutput(WOutput::Normal); + m_viewport->setOutputScale(1.f); + } + + bool applyViewportTransform(qreal rotation, qreal scale, WOutput::Transform transform, + float outputScale = 1.f) + { + restoreViewportTransform(); + m_viewport->setRotation(rotation); + m_viewport->setScale(scale); + m_viewport->rotateOutput(transform); + m_viewport->setOutputScale(outputScale); + for (int i = 0; i < 4; ++i) { + if (!captureOutput(false).valid) + return false; + } + return true; + } + + bool expectItemMoveIsPartial(QQuickItem *item, qreal dx, qreal dy, const char *what) + { + if (!item) + return false; + const QRect oldRect = sceneRect(item); + item->setX(item->x() + dx); + item->setY(item->y() + dy); + const QRect now = sceneRect(item); + + const FrameSnap snap = captureOrSkip(); + const int pad = item == named("blurPanel") ? blurPad() : shadowPad(); + const bool ok = expectRegionCovers(snap, oldRect.adjusted(-pad, -pad, pad, pad), + qPrintable(QStringLiteral("%1 old").arg(QLatin1String(what)))) + && expectRegionCovers(snap, now.adjusted(-pad, -pad, pad, pad), + qPrintable(QStringLiteral("%1 new").arg(QLatin1String(what)))) + && expectRegionAvoids(snap, sceneRect(named("sentinel")), + qPrintable(QStringLiteral("%1 sentinel").arg(QLatin1String(what)))) + && expectRegionNotFullscreen(snap, what); + return ok; + } + + bool expectTargetMoveIsPartial(const char *what) + { + return expectItemMoveIsPartial(named("target"), 55, 20, what); + } + + int shadowPad() const + { + return m_scene ? m_scene->property("shadowPad").toInt() : 28; + } + + int blurPad() const + { + return m_scene ? m_scene->property("blurPad").toInt() : 20; + } + + FrameSnap captureDirtyOutput() + { + if (m_window) + m_window->update(); + return captureOutput(false, false); + } + + FrameSnap captureOutput(bool grabScene, bool forceViewportRender = true) + { + FrameSnap snap; + WBufferDumper::DumpResult result = WBufferDumper::DumpResult::InvalidBuffer; + wlr_buffer *pendingDump = nullptr; + bool gotBatchFlush = false; + + const QMetaObject::Connection conn = connect( + m_window, &QQuickWindow::afterRendering, this, [&] { + auto *br = WOutputViewportPrivate::get(m_viewport)->bufferRenderer; + if (!br) + return; + pendingDump = br->currentBuffer(); + if (!pendingDump) + pendingDump = br->lastBuffer(); + if (auto *batch = dynamic_cast(br->renderer())) { + snap.full = batch->lastRenderRegion().isFull; + snap.region = batch->lastRenderRegion().region; + gotBatchFlush = true; + } + }, + Qt::DirectConnection); + if (forceViewportRender) { + m_viewport->render(true); + } else if (auto *output = m_viewport->output()) { + wlr_output_send_frame(output->handle()); + } + disconnect(conn); + + // afterRendering runs before QQuickRenderControl::endFrame(). Vulkan + // submits the pass there, so the dump must wait until render() returns. + auto *br = WOutputViewportPrivate::get(m_viewport)->bufferRenderer; + if (br && !gotBatchFlush) { + snap.full = br->lastRenderRegion().isFull; + snap.region = br->lastRenderRegion().region; + } + if (auto *hl = m_window->findChild(QStringLiteral("damageHighlight"))) { + if (const auto *debug = hl->overlay()) { + snap.entries = debug->entries(); + snap.nowMs = debug->currentTimeMs(); + } + } + wlr_buffer *buffer = br ? br->lastBuffer() : nullptr; + if (!buffer) + buffer = pendingDump; + if (buffer) + result = WBufferDumper::dumpBufferToImage(buffer, m_helper->renderer(), snap.buffer); + snap.valid = result == WBufferDumper::DumpResult::Success && !snap.buffer.isNull(); + if (snap.valid) + snap.buffer = toArgb32(snap.buffer); + else + qWarning("dumpBufferToImage failed: %s", + qPrintable(WBufferDumper::dumpResultToString(result))); + + if (snap.valid && qEnvironmentVariableIsSet("WSG_DAMAGE_SAVE_SCREENS")) { + static int screenDumpSeq = 0; + const char *fn = QTest::currentTestFunction(); + saveScreen(snap.buffer, + QStringLiteral("%1-%2") + .arg(QLatin1String(fn ? fn : "unknown")) + .arg(++screenDumpSeq)); + } + + if (grabScene && snap.valid) { + // grabToImage refs the item as an effect source and would dirty + // this frame's flushRegion, so it runs on a follow-up frame. + const auto previous = m_window->damageVisual(); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + const auto grab = m_scene->grabToImage(); + if (!grab) { + qWarning("grabToImage could not be initiated"); + } else { + QSignalSpy grabSpy(grab.data(), &QQuickItemGrabResult::ready); + m_viewport->render(true); + if (grabSpy.count() == 0) + grabSpy.wait(5000); + snap.grab = toArgb32(grab->image()); + if (snap.grab.isNull()) + qWarning("grabToImage did not produce an image"); + } + m_window->setDamageVisual(previous); + } + return snap; + } + + QImage expectedOverlayImage(const QImage &clean, const FrameSnap &snap) const + { + QImage painted = clean; + if (painted.size() != snap.buffer.size()) + painted = painted.scaled(snap.buffer.size(), Qt::IgnoreAspectRatio, Qt::FastTransformation); + QPainter painter(&painted); + WRegionOverlay::paint(&painter, QTransform(), snap.entries, snap.nowMs); + painter.end(); + return toArgb32(painted); + } + + bool waitUntilNoClientSurface() + { + if (!m_window || !m_helper) + return true; + + QElapsedTimer timer; + timer.start(); + while (m_window->findChild(QStringLiteral("clientSurface")) + && timer.elapsed() < 2000) { + m_helper->dispatchWaylandEvents(); + QTest::qWait(0); + } + if (m_window->findChild(QStringLiteral("clientSurface"))) + return false; + + // The xdg item is a sibling of DamageScene, so leftover pixels stay in + // the dumped output until unmap damage is actually redrawn. + for (int i = 0; i < 3; ++i) { + if (!captureOutput(false).valid) + return false; + } + return true; + } + + bool resetPlainScene() + { + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + restoreViewportTransform(); + parkCursor(); + if (!QMetaObject::invokeMethod(m_scene, "resetScene", Qt::DirectConnection)) + return false; + for (int i = 0; i < 3; ++i) { + if (!captureOutput(false).valid) + return false; + } + return true; + } + + bool showLockscreenAndSettle() + { + if (!resetPlainScene()) { + qWarning("showLockscreen: resetPlainScene failed"); + return false; + } + if (!QMetaObject::invokeMethod(m_scene, "showLockscreen", Qt::DirectConnection)) { + qWarning("showLockscreen: invokeMethod failed"); + return false; + } + for (int i = 0; i < 6; ++i) { + if (!captureOutput(false).valid) { + qWarning("showLockscreen: capture %d failed", i); + return false; + } + } + auto *lock = named("lockScene"); + if (!lock || !lock->isVisible()) { + qWarning("showLockscreen: lockScene %s visible=%d", + lock ? "present" : "missing", lock && lock->isVisible()); + return false; + } + for (const char *name : { "lockPassword", "lockCaret", "lockPasswordBlur", + "lockSessionButton" }) { + if (!named(name)) { + qWarning("showLockscreen: missing %s", name); + return false; + } + } + return true; + } + + bool expectRegionHeightAtMost(const FrameSnap &snap, int maxHeight, const char *what) + { + if (!usesTrackedPartialDamage()) + return true; + const QString prefix = QLatin1String(what); + if (!QTest::qVerify(!snap.full, + "!snap.full", + qPrintable(QStringLiteral("%1: expected partial damage, got full") + .arg(prefix)), + __FILE__, __LINE__)) { + return false; + } + const int height = snap.region.boundingRect().height(); + return QTest::qVerify(height <= maxHeight, + "region height", + qPrintable(QStringLiteral("%1: damage height %2 (max %3), %4") + .arg(prefix) + .arg(height) + .arg(maxHeight) + .arg(WSGDamageLog::describe(snap.region, snap.full))), + __FILE__, __LINE__); + } + + FrameSnap captureOrSkip(bool grabScene = false, bool forceViewportRender = true) + { + FrameSnap snap = captureOutput(grabScene, forceViewportRender); + if (!snap.valid) + QTest::qSkip("Could not dump the committed output buffer to QImage", __FILE__, __LINE__); + return snap; + } + + bool forceFullFlush() + { + auto *privateViewport = WOutputViewportPrivate::get(m_viewport); + if (!privateViewport->bufferRenderer) + return false; + privateViewport->viewport->markFull(); + return true; + } + + + bool usesTrackedPartialDamage() const + { + // QSGSoftwareRenderer / pixman does not use WSGDamageTracker. Its + // flushRegion is often the whole paint device, so partial-damage + // assertions are RHI-only. Overlay paint and rerender still run. + if (WRenderHelper::getGraphicsApi() == QSGRendererInterface::Software) + return false; + return WSGBatchRenderer::Renderer::defaultDamageMode() != WSGBatchRenderer::Renderer::DamageMode::Off; + } + + bool vulkanSkipsMultiEffectGrab() const + { + // grabToImage of live MultiEffect (blur / drop shadow) is unreliable + // on Vulkan after those items move. Region checks still run. + return WRenderHelper::getGraphicsApi() == QSGRendererInterface::Vulkan; + } + + bool expectRegionCovers(const FrameSnap &snap, const QRect &rect, const char *what) + { + if (!usesTrackedPartialDamage()) + return true; + const QString prefix = QLatin1String(what); + if (!QTest::qVerify(!snap.full, + "!snap.full", + qPrintable(QStringLiteral("%1: expected partial damage, got full").arg(prefix)), + __FILE__, __LINE__)) { + return false; + } + QRect probe = rect.adjusted(1, 1, -1, -1); + if (probe.isEmpty()) + probe = rect; + return QTest::qVerify((WPixmanRegion(probe) - snap.region).isEmpty(), + "region covers", + qPrintable(QStringLiteral("%1: damage %2 missed %3,%4 %5x%6") + .arg(prefix, WSGDamageLog::describe(snap.region, snap.full)) + .arg(rect.x()) + .arg(rect.y()) + .arg(rect.width()) + .arg(rect.height())), + __FILE__, __LINE__); + } + + bool expectRegionAvoids(const FrameSnap &snap, const QRect &rect, const char *what) + { + if (!usesTrackedPartialDamage()) + return true; + const QString prefix = QLatin1String(what); + if (!QTest::qVerify(!snap.full, + "!snap.full", + qPrintable(QStringLiteral("%1: expected partial damage, got full").arg(prefix)), + __FILE__, __LINE__)) { + return false; + } + return QTest::qVerify(!snap.region.intersects(rect), + "region avoids", + qPrintable(QStringLiteral("%1: damage %2 leaked into %3,%4 %5x%6") + .arg(prefix, WSGDamageLog::describe(snap.region, snap.full)) + .arg(rect.x()) + .arg(rect.y()) + .arg(rect.width()) + .arg(rect.height())), + __FILE__, __LINE__); + } + + bool expectRegionNotFullscreen(const FrameSnap &snap, const char *what) + { + if (!usesTrackedPartialDamage()) + return true; + const QString prefix = QLatin1String(what); + if (!QTest::qVerify(!snap.full, + "!snap.full", + qPrintable(QStringLiteral("%1: expected partial damage, got full (fullscreen red)") + .arg(prefix)), + __FILE__, __LINE__)) { + return false; + } + const QRect bounds = snap.region.boundingRect(); + const int area = bounds.width() * bounds.height(); + // Quarter of the committed output buffer, not a hard-coded 800x480. + const int kMaxArea = snap.buffer.width() * snap.buffer.height() / 4; + return QTest::qVerify(area <= kMaxArea, + "region not fullscreen", + qPrintable(QStringLiteral("%1: damage %2 covers %3 px (max %4)") + .arg(prefix, WSGDamageLog::describe(snap.region, snap.full)) + .arg(area) + .arg(kMaxArea)), + __FILE__, __LINE__); + } + + bool expectImagesMatch(const QImage &actual, const QImage &expected, const char *what, + bool ignoreCursor = true, const QRegion &limit = {}) + { + QPoint first; + QRegion compared = limit.isEmpty() ? QRegion(expected.rect()) : limit; + if (ignoreCursor) + compared -= cursorIgnoreRegion(); + int mismatches = regionDiffCount(actual, expected, compared, kRgbTolerance, &first); + if (mismatches > kMaxMismatchPixels) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + const QImage flipped = expected.flipped(Qt::Vertical); +#else + const QImage flipped = expected.mirrored(false, true); +#endif + QPoint flippedFirst; + const int flippedMismatches = + regionDiffCount(actual, flipped, compared, kRgbTolerance, &flippedFirst); + if (flippedMismatches < mismatches) { + mismatches = flippedMismatches; + first = flippedFirst; + } + } + if (mismatches > kMaxMismatchPixels) { + const QString dir = QDir::temp().filePath(QStringLiteral("wsg-damage-fail")); + QDir().mkpath(dir); + const QString stamp = QString::fromLatin1(what).replace(QLatin1Char(' '), QLatin1Char('_')); + actual.save(dir + QLatin1Char('/') + stamp + QStringLiteral("-actual.png")); + expected.save(dir + QLatin1Char('/') + stamp + QStringLiteral("-expected.png")); + } + return QTest::qVerify(mismatches >= 0 && mismatches <= kMaxMismatchPixels, + "images match", + qPrintable(QStringLiteral("%1: mismatch %2 pixels, first %3,%4") + .arg(QLatin1String(what)) + .arg(mismatches) + .arg(first.x()) + .arg(first.y())), + __FILE__, __LINE__); + } + + QString screenDumpDir() const + { + const QString dir = QDir::temp().filePath(QStringLiteral("wsg-damage-screens")); + QDir().mkpath(dir); + return dir; + } + + void saveScreen(const QImage &img, const QString &name, const QRect &crop = {}) + { + if (img.isNull()) + return; + QImage out = crop.isEmpty() ? img : img.copy(crop.intersected(img.rect())); + const QString path = screenDumpDir() + QLatin1Char('/') + name + QStringLiteral(".png"); + out.save(path); + qInfo("saved %s (%dx%d)", qPrintable(path), out.width(), out.height()); + } + + bool expectOverlayMatchesGrab(const FrameSnap &snap, const char *what, bool ignoreCursor = true) + { + const QString prefix = QLatin1String(what); + if (!QTest::qVerify(snap.valid, "snap.valid", qPrintable(prefix), __FILE__, __LINE__)) + return false; + if (!QTest::qVerify(!snap.grab.isNull(), + "!snap.grab.isNull()", + qPrintable(QStringLiteral("%1: grabToImage failed").arg(prefix)), + __FILE__, __LINE__)) { + return false; + } + // Highlight is a separate OutputLayer. The primary buffer must match + // the clean grab — same as highlight off. + return expectImagesMatch(snap.buffer, snap.grab, what, ignoreCursor); + } + +private Q_SLOTS: + void initTestCase(); + void cleanup(); + void idle_twoCapturesMatch(); + void highlight_moveRegionAndOverlay(); + void highlight_blurMoveRegionAndOverlay(); + void frosted_behindMoveMatchesRerender(); + void highlight_hardwareCursorMoveRegion(); + void highlight_softwareLayerCursorMoveRegion(); + void highlight_softwareInlineCursorMoveRegion(); + void highlight_fadeFramesMatchPainter(); + void highlight_idleCompositorErasesOverlay(); + void highlight_popupOpenRegion(); + void highlight_popupCloseRegion(); + void highlight_viewportRotation_data(); + void highlight_viewportRotation(); + void highlight_viewportScale_data(); + void highlight_viewportScale(); + void highlight_viewportRotatedScaledItemMoves(); + void client_commitDamageRegion(); + void lockscreen_popupOpen_damageIsPopupNotFullscreen(); + void lockscreen_userListPopup_closeDuringEnterDoesNotCrash(); + void lockscreen_listScroll_damageClippedToList_matchesRerender(); + void lockscreen_caretBlink_doesNotMarkWholeInput(); + void lockscreen_cursorMove_doesNotMarkBlur(); + void lockscreen_idlePasswordBlur_stableAcrossFrames(); + void lockscreen_sessionChipBlur_afterHideSourceMatchesRerender(); +}; + +void WSGDamageDebugVisualTest::initTestCase() +{ + QVERIFY(m_window); + QVERIFY(m_viewport); + QVERIFY(m_scene); + QVERIFY(named("target")); + QVERIFY(named("sentinel")); + QVERIFY(named("blurPanel")); + qInfo("WLR_RENDERER=%s graphicsApi=%d", + qgetenv("WLR_RENDERER").constData(), + int(WRenderHelper::getGraphicsApi())); + if (!usesTrackedPartialDamage()) + qInfo("software/pixman: overlay and rerender checks only (no WSGDamageTracker)"); + if (vulkanSkipsMultiEffectGrab()) + qInfo("Vulkan: skipping MultiEffect (blur/shadow) grabToImage comparisons"); + + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + parkCursor(); + for (int i = 0; i < 4; ++i) + QVERIFY2(captureOutput(false).valid, "initial output dump failed"); + + const FrameSnap idle = captureOrSkip(); + const QRect target = outputRect(named("target")); + const QRect sentinel = outputRect(named("sentinel")); + QVERIFY(idle.buffer.rect().contains(target)); + QVERIFY(idle.buffer.rect().contains(sentinel)); +} + +void WSGDamageDebugVisualTest::cleanup() +{ + restoreCursorDefaults(); + restoreViewportTransform(); + parkCursor(); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + if (m_window && m_window->findChild(QStringLiteral("clientSurface"))) + QVERIFY(waitUntilNoClientSurface()); + for (int i = 0; i < 4; ++i) { + QVERIFY(captureOutput(false).valid); + auto *hl = m_window->findChild(QStringLiteral("damageHighlight")); + if (!hl || !hl->overlay() || !hl->overlay()->needsAnotherFrame()) + break; + } +} + +void WSGDamageDebugVisualTest::idle_twoCapturesMatch() +{ + QVERIFY(resetPlainScene()); + const FrameSnap a = captureOrSkip(true); + const FrameSnap b = captureOrSkip(true); + QCOMPARE(regionDiffCount(a.buffer, b.buffer, a.buffer.rect(), kRgbTolerance), 0); + QVERIFY2(!a.grab.isNull() && !b.grab.isNull(), "idle grabToImage failed"); + QVERIFY(expectImagesMatch(a.buffer, a.grab, "idle dump vs grab")); + QVERIFY(expectImagesMatch(b.buffer, b.grab, "idle dump vs grab (2)")); +} + +void WSGDamageDebugVisualTest::highlight_moveRegionAndOverlay() +{ + QVERIFY(resetPlainScene()); + auto *target = named("target"); + const QRect oldRect = outputRect(target); + target->setX(target->x() + 70); + target->setY(target->y() + 30); + const QRect now = outputRect(target); + + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const FrameSnap snap = captureOrSkip(true); + + const int pad = shadowPad(); + QVERIFY(expectRegionCovers(snap, oldRect.adjusted(-pad, -pad, pad, pad), "move old")); + QVERIFY(expectRegionCovers(snap, now.adjusted(-pad, -pad, pad, pad), "move new")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "move sentinel")); + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect (shadow) grabToImage is skipped"); + QVERIFY(expectOverlayMatchesGrab(snap, "move overlay")); +} + +void WSGDamageDebugVisualTest::highlight_blurMoveRegionAndOverlay() +{ + QVERIFY(resetPlainScene()); + auto *panel = named("blurPanel"); + const QRect oldRect = outputRect(panel); + panel->setX(panel->x() + 40); + panel->setY(panel->y() + 20); + const QRect now = outputRect(panel); + + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const FrameSnap snap = captureOrSkip(true); + + const int pad = blurPad(); + QVERIFY(expectRegionCovers(snap, oldRect.adjusted(-pad, -pad, pad, pad), "blur old")); + QVERIFY(expectRegionCovers(snap, now.adjusted(-pad, -pad, pad, pad), "blur new")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "blur sentinel")); + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect (blur) grabToImage is skipped"); + QVERIFY(expectOverlayMatchesGrab(snap, "blur overlay")); +} + +void WSGDamageDebugVisualTest::frosted_behindMoveMatchesRerender() +{ + QVERIFY(resetPlainScene()); + auto *frost = named("frostGlass"); + auto *behind = named("frostBehind"); + QVERIFY(frost); + QVERIFY(behind); + frost->setVisible(true); + for (int i = 0; i < 4; ++i) + QVERIFY(captureOutput(false).valid); + + behind->setX(behind->x() + 45); + behind->setY(behind->y() + 25); + + const FrameSnap partial = captureOrSkip(false); + QVERIFY(expectRegionCovers(partial, itemOutputRect(behind), "frost blitter behind")); + QVERIFY(expectRegionAvoids(partial, itemOutputRect(named("sentinel")), "frost sentinel")); + + QVERIFY(forceFullFlush()); + const FrameSnap full = captureOrSkip(false); + + const QRect frostRect = outputRect(frost); + QVERIFY2(!frostRect.isEmpty(), "frost blitter has no output rect"); + QVERIFY(expectImagesMatch(partial.buffer, full.buffer, "frosted partial vs rerender", + true, QRegion(frostRect))); +} + +void WSGDamageDebugVisualTest::highlight_hardwareCursorMoveRegion() +{ + QVERIFY(resetPlainScene()); + QVERIFY2(configureCursorPath(CursorPath::Hardware), + qPrintable(QStringLiteral("hardware cursor hide did not apply: %1").arg(cursorPathDebug()))); + QVERIFY2(!cursorItem()->isVisible(), + "hardware cursor must hide the QML cursor item (not drawn into the output)"); + + auto *cursor = m_helper->cursor(); + cursor->setVisible(true); + cursor->setPosition(kCursorHome); + for (int i = 0; i < 3; ++i) + QVERIFY(captureOutput(false).valid); + QVERIFY2(!cursorItem()->isVisible(), + "QML cursor became visible while simulating the hardware plane"); + + cursor->setPosition(kCursorMoved); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const FrameSnap snap = captureOrSkip(true); + + QVERIFY2(!cursorItem()->isVisible(), + qPrintable(QStringLiteral("hardware cursor simulation lost after move: %1").arg(cursorPathDebug()))); + QVERIFY(expectRegionNotFullscreen(snap, "hardware cursor move")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "hardware cursor sentinel")); + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect grabToImage is skipped"); + QVERIFY(expectOverlayMatchesGrab(snap, "hardware cursor overlay", false)); + + cursor->setPosition(kCursorHome); + QVERIFY(captureOutput(false).valid); +} + +void WSGDamageDebugVisualTest::highlight_softwareLayerCursorMoveRegion() +{ + QVERIFY(resetPlainScene()); + QVERIFY2(configureCursorPath(CursorPath::SoftwareLayer), + qPrintable(QStringLiteral("software cursor layer did not enable: %1").arg(cursorPathDebug()))); + QVERIFY2(!cursorLayer()->inOutputsByHardware().contains(m_viewport), + "software layer cursor must not be in hardware"); + QVERIFY2(m_viewport->layers().contains(cursorLayer()), + "independent software cursor must stay an OutputLayer"); + + auto *cursor = m_helper->cursor(); + cursor->setVisible(true); + cursor->setPosition(kCursorHome); + for (int i = 0; i < 3; ++i) + QVERIFY(captureOutput(false).valid); + + cursor->setPosition(kCursorMoved); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const FrameSnap snap = captureOrSkip(true); + + QVERIFY2(cursorPathMatches(CursorPath::SoftwareLayer), + qPrintable(QStringLiteral("software cursor layer dropped after move: %1").arg(cursorPathDebug()))); + QVERIFY(expectRegionNotFullscreen(snap, "software layer cursor move")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "software layer cursor sentinel")); + // The cursor is a separate OutputLayer, so grabToImage(m_scene) and the + // primary output dump do not share overlay pixels the way inline items do. + + cursor->setPosition(kCursorHome); + QVERIFY(captureOutput(false).valid); +} + +void WSGDamageDebugVisualTest::highlight_softwareInlineCursorMoveRegion() +{ + QVERIFY(resetPlainScene()); + QVERIFY2(configureCursorPath(CursorPath::SoftwareInline), + qPrintable(QStringLiteral("inline software cursor did not enable: %1").arg(cursorPathDebug()))); + QVERIFY2(!cursorLayer()->enabled(), "inline software cursor must disable OutputLayer"); + + auto *item = cursorItem(); + QVERIFY2(item, "cursor item was not created"); + + auto *cursor = m_helper->cursor(); + cursor->setVisible(true); + cursor->setPosition(kCursorHome); + for (int i = 0; i < 3; ++i) + QVERIFY(captureOutput(false).valid); + + const QRect oldRect = outputRect(item); + QVERIFY2(!oldRect.isEmpty(), "inline software cursor has empty bounds at home"); + cursor->setPosition(kCursorMoved); + const QRect now = outputRect(item); + QVERIFY2(oldRect != now, "WCursor::setPosition did not move the cursor item"); + + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const FrameSnap snap = captureOrSkip(true); + + QVERIFY(expectRegionCovers(snap, oldRect, "inline software cursor old")); + QVERIFY(expectRegionCovers(snap, now, "inline software cursor new")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "inline software cursor sentinel")); + QVERIFY(expectRegionNotFullscreen(snap, "inline software cursor move")); + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect grabToImage is skipped"); + QVERIFY(expectOverlayMatchesGrab(snap, "inline software cursor overlay")); + + cursor->setPosition(kCursorHome); + QVERIFY(captureOutput(false).valid); +} + +void WSGDamageDebugVisualTest::highlight_fadeFramesMatchPainter() +{ + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect (shadow) grabToImage is skipped"); + QVERIFY(resetPlainScene()); + auto *target = named("target"); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + target->setX(target->x() + 70); + QVERIFY(captureOutput(false).valid); + + for (int i = 0; i < 5; ++i) { + QTest::qWait(40); + const FrameSnap snap = captureOrSkip(true); + QVERIFY(expectOverlayMatchesGrab(snap, "fade frame")); + } + + QTest::qWait(WRegionOverlay::fadeOutMs); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + for (int i = 0; i < 3; ++i) + QVERIFY(captureOutput(false).valid); + const FrameSnap faded = captureOrSkip(true); + QVERIFY2(!faded.grab.isNull(), "fade grabToImage failed"); + QVERIFY(expectImagesMatch(faded.buffer, faded.grab, "fade left overlay residue")); +} + +void WSGDamageDebugVisualTest::highlight_idleCompositorErasesOverlay() +{ + if (!usesTrackedPartialDamage()) + QSKIP("idle overlay residue is an RHI PreserveColorContents swapchain path"); + + // Highlight is a WOutputLayer. Idle compositor must not leave overlay + // pixels in the primary swapchain. Layer fade is independent. + QVERIFY(resetPlainScene()); + auto *target = named("target"); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + target->setX(target->x() + 70); + + const FrameSnap painted = captureOrSkip(false); + QVERIFY2(!painted.entries.isEmpty(), "highlight did not record overlay entries"); + + QTest::qWait(WRegionOverlay::fadeOutMs + 50); + + FrameSnap idle; + for (int i = 0; i < 4; ++i) { + idle = captureOutput(false, false); + QVERIFY2(idle.valid, "compositor idle frame could not dump the output buffer"); + } + idle = captureOrSkip(true, false); + QVERIFY2(!idle.grab.isNull(), "idle compositor grabToImage failed"); + + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect grabToImage is skipped"); + + FrameSnap paintedWithGrab = painted; + paintedWithGrab.grab = idle.grab; + QVERIFY(expectOverlayMatchesGrab(paintedWithGrab, "overlay before idle compositor")); + QVERIFY(expectImagesMatch(idle.buffer, idle.grab, "idle compositor left overlay residue")); + + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); +} + +void WSGDamageDebugVisualTest::highlight_popupOpenRegion() +{ + QVERIFY(resetPlainScene()); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + QVERIFY(QMetaObject::invokeMethod(m_scene, "openPopup", Qt::DirectConnection)); + const FrameSnap snap = captureOrSkip(); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + + auto *panel = popupPanel(); + QVERIFY2(panel, "popupPanel was not in the scene"); + QVERIFY(panel->isVisible()); + QVERIFY(expectRegionCovers(snap, sceneRect(panel), "popup open")); + QVERIFY(expectRegionAvoids(snap, sceneRect(named("sentinel")), "popup open sentinel")); + QVERIFY(expectRegionNotFullscreen(snap, "popup open")); + // grabToImage(m_scene) does not include QQuickOverlay, so dump vs grab + // cannot verify the popup pixels. Region checks above are the contract. +} + +void WSGDamageDebugVisualTest::highlight_popupCloseRegion() +{ + QVERIFY(resetPlainScene()); + QVERIFY(QMetaObject::invokeMethod(m_scene, "openPopup", Qt::DirectConnection)); + QVERIFY(captureOutput(false).valid); + + auto *panel = popupPanel(); + QVERIFY2(panel, "popupPanel was not in the scene"); + const QRect oldRect = sceneRect(panel); + + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + QVERIFY(QMetaObject::invokeMethod(m_scene, "closePopup", Qt::DirectConnection)); + const FrameSnap snap = captureOrSkip(true); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + + QVERIFY(expectRegionCovers(snap, oldRect, "popup close")); + QVERIFY(expectRegionAvoids(snap, sceneRect(named("sentinel")), "popup close sentinel")); + QVERIFY(expectRegionNotFullscreen(snap, "popup close")); + if (vulkanSkipsMultiEffectGrab()) + QSKIP("Vulkan: MultiEffect grabToImage is skipped"); + QVERIFY(expectOverlayMatchesGrab(snap, "popup close overlay")); +} + +void WSGDamageDebugVisualTest::highlight_viewportRotation_data() +{ + QTest::addColumn("itemRotation"); + QTest::addColumn("transform"); + QTest::newRow("0") << 0. << int(WOutput::Normal); + QTest::newRow("90") << 90. << int(WOutput::R90); + QTest::newRow("180") << 180. << int(WOutput::R180); + QTest::newRow("270") << -90. << int(WOutput::R270); +} + +void WSGDamageDebugVisualTest::highlight_viewportRotation() +{ + QFETCH(qreal, itemRotation); + QFETCH(int, transform); + + QVERIFY(resetPlainScene()); + QVERIFY2(applyViewportTransform(itemRotation, 1.0, WOutput::Transform(transform)), + "output dump failed after viewport rotation"); + QVERIFY(expectTargetMoveIsPartial("viewport rotation target")); + QVERIFY(captureOutput(false).valid); + QVERIFY(expectItemMoveIsPartial(named("blurPanel"), -35, 25, "viewport rotation blur")); +} + +void WSGDamageDebugVisualTest::highlight_viewportScale_data() +{ + QTest::addColumn("itemScale"); + QTest::addColumn("outputScale"); + QTest::newRow("item-0.8") << 0.8 << 1.f; + QTest::newRow("item-1.25") << 1.25 << 1.f; + QTest::newRow("item-1.5") << 1.5 << 1.f; + QTest::newRow("output-1.5") << 1.0 << 1.5f; +} + +void WSGDamageDebugVisualTest::highlight_viewportScale() +{ + QFETCH(qreal, itemScale); + QFETCH(float, outputScale); + + QVERIFY(resetPlainScene()); + QVERIFY2(applyViewportTransform(0, itemScale, WOutput::Normal, outputScale), + "output dump failed after viewport scale"); + QVERIFY(expectTargetMoveIsPartial("viewport scale target")); + QVERIFY(captureOutput(false).valid); + QVERIFY(expectItemMoveIsPartial(named("blurPanel"), -35, 25, "viewport scale blur")); +} + +void WSGDamageDebugVisualTest::highlight_viewportRotatedScaledItemMoves() +{ + QVERIFY(resetPlainScene()); + QVERIFY2(applyViewportTransform(90, 1.25, WOutput::R90, 1.f), + "output dump failed after viewport rotate+scale"); + QVERIFY(expectTargetMoveIsPartial("rotated scaled target")); + QVERIFY(captureOutput(false).valid); + QVERIFY(expectItemMoveIsPartial(named("blurPanel"), -35, 25, "rotated scaled blur")); +} + +void WSGDamageDebugVisualTest::client_commitDamageRegion() +{ + QVERIFY(resetPlainScene()); + const int clientFd = m_helper->createInProcessClientFd(); + QVERIFY2(clientFd >= 0, "failed to create in-process Wayland socketpair"); + + DamageClient *client = damage_client_create(clientFd); + QVERIFY2(client, "failed to create the C Wayland client"); + QFuture clientFuture = QtConcurrent::run([client] { + damage_client_run(client); + }); + const auto stopClient = qScopeGuard([&] { + damage_client_stop(client); + clientFuture.waitForFinished(); + damage_client_destroy(client); + waitUntilNoClientSurface(); + }); + + auto pumpServer = [this] { + m_helper->dispatchWaylandEvents(); + QTest::qWait(0); + }; + + QElapsedTimer timer; + timer.start(); + while (!damage_client_is_mapped(client) && timer.elapsed() < 5000) + pumpServer(); + if (!damage_client_is_mapped(client)) { + const QByteArray err = damage_client_error(client); + QFAIL(qPrintable(QStringLiteral("client did not map: %1").arg(QLatin1String(err)))); + } + + QQuickItem *surfaceItem = nullptr; + while (!(surfaceItem = m_window->findChild(QStringLiteral("clientSurface"))) + && timer.elapsed() < 5000) { + pumpServer(); + } + QVERIFY2(surfaceItem, "XdgToplevelSurfaceItem was not created for the client"); + + auto *shellItem = qobject_cast(surfaceItem); + QVERIFY2(shellItem && shellItem->surface(), "client surface item has no WSurface"); + + // First map reports the window AABB (DirtyNodeAdded), not buffer damage. + for (int i = 0; i < 4; ++i) { + pumpServer(); + QVERIFY2(captureOutput(false).valid, "output dump failed while idling the mapped client"); + } + + const QRect damage(8, 12, 16, 20); + + bool committed = false; + const QMetaObject::Connection commitConn = connect( + shellItem->surface(), &WSurface::commit, this, + [&] { committed = true; }, Qt::DirectConnection); + + if (damage_client_commit_damage(client, damage.x(), damage.y(), damage.width(), damage.height()) != 0) { + disconnect(commitConn); + QFAIL("client failed to commit wl_surface.damage"); + } + + QElapsedTimer flushTimer; + flushTimer.start(); + while (!committed && flushTimer.elapsed() < 2000) + pumpServer(); + disconnect(commitConn); + QVERIFY2(committed, "no scene-graph frame after the client damage commit"); + + // Reuse captureOutput: Vulkan only submits the pass in endFrame, after + // afterRendering, so reading lastRenderRegion off a raw render() misses it. + const FrameSnap snap = captureOutput(false); + QVERIFY2(snap.valid, "output dump failed after the client damage commit"); + + QQuickItem *content = shellItem->contentItem() ? shellItem->contentItem() : surfaceItem; + const QRect mapped = content->mapRectToScene(QRectF(damage)).toAlignedRect(); + + QVERIFY(expectRegionCovers(snap, mapped, "client damage")); + QVERIFY(expectRegionAvoids(snap, sceneRect(named("sentinel")), "client damage sentinel")); + QVERIFY(expectRegionAvoids(snap, sceneRect(named("target")), "client damage target")); + QVERIFY(expectRegionNotFullscreen(snap, "client damage")); +} + + +void WSGDamageDebugVisualTest::lockscreen_popupOpen_damageIsPopupNotFullscreen() +{ + QVERIFY(showLockscreenAndSettle()); + QVERIFY(QMetaObject::invokeMethod(named("lockScene"), "openSessionPopup", Qt::DirectConnection)); + + const FrameSnap snap = captureOrSkip(); + auto *sessionPanel = namedAnywhere("lockSessionGlass"); + auto *list = namedAnywhere("lockSessionList"); + QVERIFY2(sessionPanel && sessionPanel->isVisible(), "session panel was not shown"); + QVERIFY2(list && list->isVisible(), "session list was not shown"); + + const QRect popupRect = itemSceneRect(sessionPanel); + QVERIFY2(!popupRect.isEmpty(), "session popup has no scene rect"); + QVERIFY(expectRegionCovers(snap, popupRect, "lock popup open")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "lock popup sentinel")); + QVERIFY(expectRegionNotFullscreen(snap, "lock popup open")); + // Unclipped ListView content (~12*61) used to paint a full-output-height + // stripe the width of the popup. The popup itself is 280px tall. + QVERIFY(expectRegionHeightAtMost(snap, popupRect.height() + 48, "lock popup open")); +} + +void WSGDamageDebugVisualTest::lockscreen_userListPopup_closeDuringEnterDoesNotCrash() +{ + // Real lockscreen: UserList open, then click again during DTK FloatingPanel + // enter (scale/opacity). WAYLIB_DEBUG_DAMAGE=highlight + extra-QRhi copy + // aborted NVIDIA (GL_INVALID_ENUM, then free() in beginOffscreenFrame). + // Must force the viewport render: OutputRenderWindow::render() skips + // X11 while framePending, so highlight+extra-QRhi never ran. forceRender + // still uses damage tracking; it is not a full-scene redraw. + // + // showLockscreenAndSettle() -> resetPlainScene() forces highlight off. + // Turn it on after settle; the user crash is highlight-only. + QVERIFY(showLockscreenAndSettle()); + const auto previous = m_window->damageVisual(); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const auto restoreMode = qScopeGuard([previous] { + m_window->setDamageVisual(previous); + }); + auto *lock = named("lockScene"); + QVERIFY(lock); + auto *alwaysGlass = namedAnywhere("lockAlwaysGlass"); + QVERIFY2(alwaysGlass, "220x280 lockAlwaysGlass missing from lockContent"); + QVERIFY2(alwaysGlass->width() >= 200 && alwaysGlass->height() >= 200, + "lockAlwaysGlass has no size"); + auto *wallpaper = named("wallpaper"); + auto *pulse = namedAnywhere("lockBehindPulse"); + QVERIFY2(wallpaper, "wallpaper missing; behind-blitter damage cannot be forced"); + QVERIFY2(pulse, "lockBehindPulse missing; behind-blitter damage cannot be forced"); + + // Scale + rotation like DTK FloatingPanel enter (rotate-blit extra-QRhi). + alwaysGlass->setScale(0.85); + alwaysGlass->setRotation(6); + alwaysGlass->setOpacity(0.4); + for (int i = 0; i < 8; ++i) { + alwaysGlass->setScale(0.85 + i * (0.15 / 7.0)); + alwaysGlass->setRotation(6.0 - i * (6.0 / 7.0)); + alwaysGlass->setOpacity(0.4 + i * (0.6 / 7.0)); + alwaysGlass->update(); + wallpaper->setScale(1.0 + i * 0.002); + pulse->setX(48 + i * 4); + pulse->setY(96 + (i % 2) * 8); + QVERIFY2(m_window->damageVisual() == WOutputRenderWindow::DamageVisual::Highlight, + "highlight turned off during lockAlwaysGlass frames"); + QTest::qWait(16); + QVERIFY2(captureOutput(false, true).valid, + qPrintable(QStringLiteral("lockAlwaysGlass scale frame %1 failed").arg(i))); + const QByteArray glErrors = drainGlErrors(m_window); + QVERIFY2(glErrors.isEmpty(), + qPrintable(QStringLiteral("GL error after lockAlwaysGlass frame %1: %2") + .arg(i) + .arg(QString::fromLatin1(glErrors)))); + } + alwaysGlass->setScale(1); + alwaysGlass->setRotation(0); + alwaysGlass->setOpacity(1); + wallpaper->setScale(1); + pulse->setX(48); + pulse->setY(96); + lock->setProperty("popupEnterAnimation", true); + QVERIFY(QMetaObject::invokeMethod(lock, "openSessionPopup", Qt::DirectConnection)); + // Stay in the enter animation (240ms) before closing, like clicking the + // chip again while the DTK panel is still scaling in. + for (int i = 0; i < 6; ++i) { + QVERIFY2(m_window->damageVisual() == WOutputRenderWindow::DamageVisual::Highlight, + "highlight turned off during popup enter"); + QTest::qWait(16); + QVERIFY2(captureOutput(false, true).valid, + qPrintable(QStringLiteral("popup enter frame %1 failed").arg(i))); + const QByteArray glErrors = drainGlErrors(m_window); + QVERIFY2(glErrors.isEmpty(), + qPrintable(QStringLiteral("GL error after popup enter frame %1: %2") + .arg(i) + .arg(QString::fromLatin1(glErrors)))); + } + auto *sessionPanel = namedAnywhere("lockSessionGlass"); + QVERIFY2(sessionPanel && sessionPanel->isVisible(), "session popup was not shown during enter"); + QVERIFY2(sessionPanel->width() >= 200 && sessionPanel->height() >= 200, "session popup has no size"); + QVERIFY(QMetaObject::invokeMethod(lock, "closeSessionPopup", Qt::DirectConnection)); + for (int i = 0; i < 18; ++i) { + QVERIFY2(m_window->damageVisual() == WOutputRenderWindow::DamageVisual::Highlight, + "highlight turned off during popup close"); + QTest::qWait(16); + QVERIFY2(captureOutput(false, true).valid, + qPrintable(QStringLiteral("popup close-during-enter frame %1 failed").arg(i))); + const QByteArray glErrors = drainGlErrors(m_window); + QVERIFY2(glErrors.isEmpty(), + qPrintable(QStringLiteral("GL error after popup close frame %1: %2") + .arg(i) + .arg(QString::fromLatin1(glErrors)))); + if (i == 2) + QVERIFY(QMetaObject::invokeMethod(lock, "openSessionPopup", Qt::DirectConnection)); + if (i == 5) + QVERIFY(QMetaObject::invokeMethod(lock, "closeSessionPopup", Qt::DirectConnection)); + } + lock->setProperty("popupEnterAnimation", false); + QVERIFY(QMetaObject::invokeMethod(lock, "resetLock", Qt::DirectConnection)); + QVERIFY2(captureOutput(false, true).valid, "popup reset after close-during-enter failed"); +} + +void WSGDamageDebugVisualTest::lockscreen_listScroll_damageClippedToList_matchesRerender() +{ + QVERIFY(showLockscreenAndSettle()); + QVERIFY(QMetaObject::invokeMethod(named("lockScene"), "openSessionPopup", Qt::DirectConnection)); + for (int i = 0; i < 4; ++i) + QVERIFY(captureOutput(false).valid); + + auto *list = namedAnywhere("lockSessionList"); + auto *sessionPanel = namedAnywhere("lockSessionGlass"); + QVERIFY2(list && sessionPanel, "lock session widgets missing"); + QVERIFY2(list->isVisible(), "session list was not shown"); + + list->setProperty("contentY", list->property("contentY").toReal() + 90); + const FrameSnap partial = captureOrSkip(); + + const QRect listRect = itemSceneRect(list); + QVERIFY2(!listRect.isEmpty(), "session list has no scene rect"); + QVERIFY(expectRegionCovers(partial, listRect, "lock list scroll")); + QVERIFY(expectRegionAvoids(partial, outputRect(named("sentinel")), "lock list sentinel")); + QVERIFY(expectRegionAvoids(partial, itemOutputRect(named("lockPasswordBlur")), + "lock list password")); + QVERIFY(expectRegionNotFullscreen(partial, "lock list scroll")); + QVERIFY(expectRegionHeightAtMost(partial, listRect.height() + 32, "lock list scroll")); + + QVERIFY(forceFullFlush()); + const FrameSnap full = captureOrSkip(false); + + const QRect panelRect = itemOutputRect(sessionPanel); + QVERIFY2(!panelRect.isEmpty(), "session panel has no output rect"); + QVERIFY(expectImagesMatch(partial.buffer, full.buffer, "lock list scroll partial vs rerender", + true, QRegion(panelRect))); +} + +void WSGDamageDebugVisualTest::lockscreen_caretBlink_doesNotMarkWholeInput() +{ + QVERIFY(showLockscreenAndSettle()); + auto *caret = named("lockCaret"); + auto *blur = named("lockPasswordBlur"); + QVERIFY2(caret && blur, "lock password widgets missing"); + + QVERIFY(QMetaObject::invokeMethod(m_scene, "blinkLockCaret", Qt::DirectConnection)); + const FrameSnap snap = captureOrSkip(); + + const QRect caretRect = itemOutputRect(caret); + const QRect passwordRect = itemOutputRect(blur); + QVERIFY2(!caretRect.isEmpty(), "lock caret has no output rect"); + QVERIFY2(passwordRect.width() > 16 && passwordRect.height() > 8, + qPrintable(QStringLiteral("lock password mapped %1x%2, expected ~220x30") + .arg(passwordRect.width()) + .arg(passwordRect.height()))); + QVERIFY(expectRegionCovers(snap, caretRect, "lock caret")); + QVERIFY(expectRegionAvoids(snap, outputRect(named("sentinel")), "lock caret sentinel")); + QVERIFY(expectRegionNotFullscreen(snap, "lock caret")); + QVERIFY(expectRegionAvoids(snap, itemOutputRect(named("lockSessionButton")), + "lock caret session button")); + QVERIFY(expectRegionAvoids(snap, itemOutputRect(named("lockLoginBtn")), + "lock caret login button")); + + if (!usesTrackedPartialDamage()) + return; + QVERIFY2(snap.region.boundingRect().width() < passwordRect.width() / 2, + qPrintable(QStringLiteral("caret marked the whole password field: damage %1 field %2x%3+%4+%5") + .arg(WSGDamageLog::describe(snap.region, snap.full)) + .arg(passwordRect.width()) + .arg(passwordRect.height()) + .arg(passwordRect.x()) + .arg(passwordRect.y()))); +} + +void WSGDamageDebugVisualTest::lockscreen_cursorMove_doesNotMarkBlur() +{ + QVERIFY(showLockscreenAndSettle()); + QVERIFY2(configureCursorPath(CursorPath::SoftwareInline), + qPrintable(QStringLiteral("inline software cursor did not enable: %1").arg(cursorPathDebug()))); + + auto *cursor = m_helper->cursor(); + const QPointF home(520, 24); + const QPointF moved(640, 48); + cursor->setVisible(true); + cursor->setPosition(home); + for (int i = 0; i < 3; ++i) + QVERIFY(captureOutput(false).valid); + + cursor->setPosition(moved); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Highlight); + const FrameSnap snap = captureOrSkip(true); + m_window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + + QVERIFY(expectRegionNotFullscreen(snap, "lock cursor move")); + QVERIFY(expectRegionAvoids(snap, itemOutputRect(named("lockPasswordBlur")), + "lock cursor password blur")); + QVERIFY(expectRegionAvoids(snap, itemOutputRect(named("lockSessionButton")), + "lock cursor session button")); + QVERIFY(expectRegionAvoids(snap, itemOutputRect(named("lockAlwaysGlass")), + "lock cursor lockAlwaysGlass")); + QVERIFY(expectRegionAvoids(snap, itemOutputRect(named("lockUserButton")), + "lock cursor user button")); + + cursor->setPosition(home); + QVERIFY(captureOutput(false).valid); + restoreCursorDefaults(); + parkCursor(); +} + +void WSGDamageDebugVisualTest::lockscreen_idlePasswordBlur_stableAcrossFrames() +{ + QVERIFY(showLockscreenAndSettle()); + const FrameSnap a = captureOrSkip(); + const FrameSnap b = captureOrSkip(); + + auto *blur = named("lockPasswordBlur"); + QVERIFY2(blur, "lock password blur missing"); + const QRect blurRect = outputRect(blur); + QVERIFY2(!blurRect.isEmpty(), "lock password blur has no output rect"); + QVERIFY(expectImagesMatch(a.buffer, b.buffer, "lock idle password blur", true, QRegion(blurRect))); + + auto *chip = named("lockSessionBtnBlur"); + const QRect chipRect = chip ? itemOutputRect(chip) : QRect(); + const QRect corner(a.buffer.width() - 220, a.buffer.height() - 90, 210, 80); + if (qEnvironmentVariableIsSet("WSG_DAMAGE_SAVE_SCREENS")) { + saveScreen(a.buffer, QStringLiteral("lock-idle-full")); + saveScreen(a.buffer, QStringLiteral("lock-idle-password"), blurRect); + saveScreen(a.buffer, QStringLiteral("lock-idle-chip"), chipRect); + saveScreen(a.buffer, QStringLiteral("lock-idle-bottom-right"), corner); + } + + if (!usesTrackedPartialDamage()) + return; + QVERIFY2(!a.full && !b.full, "idle lockscreen reported full damage"); + const QRect blurCore = blurRect.adjusted(2, 2, -2, -2); + QVERIFY2(!a.region.intersects(blurCore) && !b.region.intersects(blurCore), + qPrintable(QStringLiteral("idle recopy of password blitter: %1 then %2") + .arg(WSGDamageLog::describe(a.region, a.full), + WSGDamageLog::describe(b.region, b.full)))); +} + +void WSGDamageDebugVisualTest::lockscreen_sessionChipBlur_afterHideSourceMatchesRerender() +{ + // Replica of LoginAnimation: first paints go through ShaderEffectSource + // (hideSource). After it stops, chips are idle — damage must still blit. + if (!resetPlainScene()) + QFAIL("resetPlainScene failed"); + QVERIFY(QMetaObject::invokeMethod(m_scene, "showLockscreen", Qt::DirectConnection)); + QVERIFY(QMetaObject::invokeMethod(m_scene, "startLockHideSource", Qt::DirectConnection)); + for (int i = 0; i < 6; ++i) + QVERIFY2(captureOutput(false).valid, "hideSource settle failed"); + QVERIFY(QMetaObject::invokeMethod(m_scene, "stopLockHideSource", Qt::DirectConnection)); + for (int i = 0; i < 4; ++i) { + QTest::qWait(16); + QVERIFY2(captureOutput(false).valid, "post-hideSource idle failed"); + } + + auto *chip = named("lockSessionBtnBlur"); + QVERIFY2(chip, "session chip blur missing"); + const QRect chipRect = itemOutputRect(chip); + QVERIFY2(!chipRect.isEmpty(), "session chip has no output rect"); + const FrameSnap partial = captureOrSkip(false); + // Bottom-right corner derived from the committed buffer, like the idle test above. + const QRect corner(partial.buffer.width() - 220, partial.buffer.height() - 90, 210, 80); + if (qEnvironmentVariableIsSet("WSG_DAMAGE_SAVE_SCREENS")) { + saveScreen(partial.buffer, QStringLiteral("lock-hidesource-partial-full")); + saveScreen(partial.buffer, QStringLiteral("lock-hidesource-partial-chip"), chipRect); + saveScreen(partial.buffer, QStringLiteral("lock-hidesource-partial-bottom-right"), corner); + } + + QVERIFY(forceFullFlush()); + const FrameSnap full = captureOrSkip(false); + if (qEnvironmentVariableIsSet("WSG_DAMAGE_SAVE_SCREENS")) { + saveScreen(full.buffer, QStringLiteral("lock-hidesource-rerender-full")); + saveScreen(full.buffer, QStringLiteral("lock-hidesource-rerender-chip"), chipRect); + saveScreen(full.buffer, QStringLiteral("lock-hidesource-rerender-bottom-right"), corner); + } + + QVERIFY(expectImagesMatch(partial.buffer, full.buffer, + "lock session chip after hideSource vs rerender", + true, QRegion(chipRect))); +} + +int runVisualTests(int argc, char *argv[]) +{ + if (!qEnvironmentVariableIsSet("WLR_BACKENDS")) + qputenv("WLR_BACKENDS", "headless"); + if (qgetenv("WLR_BACKENDS") == "headless") + qunsetenv("WAYLAND_DISPLAY"); + if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) + qputenv("QT_QUICK_CONTROLS_STYLE", "Basic"); + + WLog::init(); + WServer::initializeQPA(); + WRenderHelper::setupRendererBackend(); + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; +#define WSG_STR(x) #x +#define WSG_XSTR(x) WSG_STR(x) + engine.addImportPath(QLatin1String(WSG_XSTR(WAYLIB_QML_IMPORT_PATH))); +#undef WSG_XSTR +#undef WSG_STR + + QObject::connect(&engine, &QQmlEngine::warnings, [](const QList &warnings) { + for (const QQmlError &error : warnings) + fprintf(stderr, "QML: %s\n", qPrintable(error.toString())); + }); + + engine.loadFromModule("WSGDamageVisual", "TestWindow"); + if (engine.rootObjects().isEmpty()) { + fprintf(stderr, "QQmlApplicationEngine failed to load WSGDamageVisual/TestWindow\n"); + return 1; + } + + auto *helper = engine.singletonInstance("WSGDamageVisual", "Helper"); + if (!helper) + return 1; + + auto *window = engine.rootObjects().first()->findChild( + QStringLiteral("renderWindow")); + if (!window) + return 1; + helper->initProtocols(window, &engine); + window->setDamageVisual(WOutputRenderWindow::DamageVisual::Off); + if (!helper->renderer()) { + fprintf(stderr, "SKIP test_wsgdamage_visual: no wlroots renderer (no DRM device)\n"); + return 0; + } + window->setVisible(true); + + QSignalSpy initSpy(window, &WOutputRenderWindow::outputViewportInitialized); + if (initSpy.isEmpty()) + initSpy.wait(5000); + + auto *viewport = window->findChild(QStringLiteral("outputViewport")); + auto *scene = window->findChild(QStringLiteral("scene")); + if (!viewport || !scene) + return 1; + + QTest::qWait(200); + WSGDamageDebugVisualTest::setGlobals(window, viewport, scene, helper); + + WSGDamageDebugVisualTest test; + const int result = QTest::qExec(&test, argc, argv); + return result; +} + +int main(int argc, char *argv[]) +{ + if (!qEnvironmentVariableIsSet("WSG_DAMAGE_TEST_CHILD") + && !qEnvironmentVariableIsSet("WLR_RENDERER")) { + QCoreApplication app(argc, argv); + int status = 0; + const QStringList args = app.arguments().mid(1); + const auto runChild = [&](const QProcessEnvironment &env, const QStringList &childArgs, + const char *label) { + QProcess process; + process.setProcessEnvironment(env); + process.setProcessChannelMode(QProcess::ForwardedChannels); + fprintf(stderr, "\n===== %s =====\n", label); + process.start(app.applicationFilePath(), childArgs); + if (!process.waitForStarted(15000) || !process.waitForFinished(15 * 60 * 1000)) { + fprintf(stderr, "%s failed to run: %s\n", label, qPrintable(process.errorString())); + return 1; + } + const int code = process.exitStatus() == QProcess::NormalExit ? process.exitCode() : 1; + fprintf(stderr, "===== %s exit %d =====\n", label, code); + return code; + }; + + // Default to gles2 only: the full gles2/vulkan/pixman matrix triples + // CTest wall-clock time past the timeout. Opt back into the full + // matrix with WSG_DAMAGE_TEST_ALL_RENDERERS=1. + QStringList rendererList{ QStringLiteral("gles2") }; + if (qEnvironmentVariableIsSet("WSG_DAMAGE_TEST_ALL_RENDERERS")) + rendererList << QStringLiteral("vulkan") << QStringLiteral("pixman"); + for (const QString &renderer : rendererList) { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("WLR_RENDERER"), renderer); + env.insert(QStringLiteral("WSG_DAMAGE_TEST_CHILD"), QStringLiteral("1")); + if (renderer == QLatin1String("gles2")) { + const QByteArray nvidia = nvidiaRenderNode(); + if (!nvidia.isEmpty() && !qEnvironmentVariableIsSet("WLR_RENDER_DRM_DEVICE")) + env.insert(QStringLiteral("WLR_RENDER_DRM_DEVICE"), QString::fromLatin1(nvidia)); + } + status |= runChild(env, args, qPrintable(QStringLiteral("WLR_RENDERER=%1").arg(renderer))); + } + + // Headless GBM on NVIDIA does not match the abort (X11-2 XR24 + + // highlight + extra-QRhi). Run the lockscreen popup case on the X11 + // backend when a display and NVIDIA node exist. + if (qEnvironmentVariableIsSet("DISPLAY") && !nvidiaRenderNode().isEmpty()) { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("WLR_RENDERER"), QStringLiteral("gles2")); + env.insert(QStringLiteral("WSG_DAMAGE_TEST_CHILD"), QStringLiteral("1")); + env.insert(QStringLiteral("WLR_BACKENDS"), QStringLiteral("x11")); + env.insert(QStringLiteral("WLR_RENDER_DRM_DEVICE"), + QString::fromLatin1(nvidiaRenderNode())); + env.insert(QStringLiteral("QT_LOGGING_RULES"), + QStringLiteral("waylib.render.buffer.debug=true;waylib.output.buffer.debug=true")); + env.remove(QStringLiteral("WAYLAND_DISPLAY")); + status |= runChild(env, + { QStringLiteral("lockscreen_userListPopup_closeDuringEnterDoesNotCrash") }, + "WLR_BACKENDS=x11 NVIDIA gles2 lockscreen popup crash"); + } + return status; + } + return runVisualTests(argc, argv); +} + +#include "main.moc"